{ "cells": [ { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import os\n", "from pathlib import Path\n", "import platform\n", "import sys\n", "from typing import List, Optional, TextIO" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A directory contains a project if there is a `setup.py` file in it." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['./shared/schema',\n", " './shared/support',\n", " './shared/messaging/events',\n", " './apps/cli/executables/ingestion',\n", " './apps/cli/executables/weblog_thumbs',\n", " './apps/cli/executables/alma_reingester',\n", " './apps/cli/executables/alma_product_fetch',\n", " './apps/cli/executables/epilogue',\n", " './apps/cli/executables/datafetcher',\n", " './apps/cli/executables/vlba_grabber',\n", " './apps/cli/utilities/s_code_project_updater',\n", " './apps/cli/utilities/proprietary_setter',\n", " './apps/cli/utilities/mr_clean',\n", " './apps/cli/utilities/faultchecker',\n", " './apps/cli/utilities/mr_books',\n", " './apps/cli/utilities/dumplogs',\n", " './apps/cli/utilities/datafinder',\n", " './apps/cli/utilities/qa_results',\n", " './apps/cli/launchers/pymygdala',\n", " './apps/cli/launchers/wf',\n", " './services/archive']" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "projects = list(proj for (proj, subdirs, files) in os.walk(Path()) if 'setup.py' in files)\n", "projects" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here are some things we know about projects:\n", "\n", "- Every project has a name, which is the same as the name of the directory that contains it.\n", "- Every project has a version, which happens to be in a file in `src/$PROJECT/_version.py` in the same format\n", "- Every project has certain dependencies, which are enumerated in the setup.py as `install_requires`\n", "\n", "Let's build up this abstraction." ] }, { "cell_type": "code", "execution_count": 44, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "<Project name=datafetcher version=4.0.0a1.dev1>" ] }, "execution_count": 44, "metadata": {}, "output_type": "execute_result" } ], "source": [ "class Project:\n", " def __init__(self, path: Path):\n", " self.path = path\n", " self.name = path.name\n", " \n", " @property\n", " def version(self) -> str:\n", " \"\"\"Compute and return the version in the _version.py file under the project.\"\"\"\n", " # to compute the version, we must locate and parse the _version.py file\n", " version_file = self.path / 'src' / self.name / '_version.py'\n", " \n", " # this line is basically cribbed from the standard setup.py file\n", " return version_file.open().readlines()[-1].split()[-1].strip(\"\\\"'\")\n", " \n", " @property\n", " def dependencies(self) -> List[str]:\n", " \"\"\"Returns the list of dependencies under this project\"\"\"\n", " \n", " # to compute the dependencies, we have to do something a bit more gross\n", " # we have to open the setup.py file and look for a line with \"install_requires\"\n", " # once we have that line, we have to parse out the Python list\n", " # we can go ahead and cheat here with eval() for today.\n", " #\n", " # Unfortunately, this doesn't work if they span multiple lines, which is a\n", " # frequent occurrence\n", " setup_file = self.path / 'setup.py'\n", " \n", " for line in setup_file.open().readlines():\n", " if 'install_requires' in line:\n", " return eval(line.split('=')[1].strip().strip(','))\n", " \n", " # if we made it here, we never found that line, so this project \n", " # has no dependencies; let's make life easy on ourselves and \n", " # keep the API simple\n", " return []\n", " \n", " def __repr__(self) -> str:\n", " return f'<Project name={self.name} version={self.version}>'\n", " \n", "p1 = Project(Path('apps/cli/executables/datafetcher'))\n", "p1" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['requests', 'pycapo']" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p1.dependencies" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The Makefile we want to build is going to have a certain structure. Namely, it should:\n", "\n", "- Have a top-level entry point, probably a `.PHONY` that is easy for a parent Makefile to target\n", "- The top-level entry point is going to depend on all the projects we have located\n", "- Each project will be a target, a dependency of the top-level entry point\n", "\n", "Based on the project structure we also know this:\n", "\n", "- A project rule will depend on that project's dependencies\n", "\n", "This should cause the build order to fall out rather naturally." ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'x86_64'" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "alma_product_fetch-4.0.0a1.dev1.macosx-10.9-x86_64.tar.gz" ] }, { "cell_type": "code", "execution_count": 46, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ ".PHONY: all-python-projects\n", "all-python-projects: shared/schema/dist/schema-4.0.0a1.dev1.macosx-10.9-x86_64.tar.gz apps/cli/executables/alma_reingester/dist/alma_reingester-4.0.0a1.dev1.macosx-10.9-x86_64.tar.gz shared/messaging/events/dist/events-4.0.0a1.dev1.macosx-10.9-x86_64.tar.gz\n", "\n", "shared/schema/dist/schema-4.0.0a1.dev1.macosx-10.9-x86_64.tar.gz: \n", "\tcd shared/schema && python setup.py bdist\n", "\n", "apps/cli/executables/alma_reingester/dist/alma_reingester-4.0.0a1.dev1.macosx-10.9-x86_64.tar.gz: shared/messaging/events/dist/events-4.0.0a1.dev1.macosx-10.9-x86_64.tar.gz shared/schema/dist/schema-4.0.0a1.dev1.macosx-10.9-x86_64.tar.gz\n", "\tcd apps/cli/executables/alma_reingester && python setup.py bdist\n", "\n", "shared/messaging/events/dist/events-4.0.0a1.dev1.macosx-10.9-x86_64.tar.gz: \n", "\tcd shared/messaging/events && python setup.py bdist\n", "\n" ] } ], "source": [ "class Makefile:\n", " def __init__(self, projects: List[Project]):\n", " self.projects = projects\n", " \n", " def build(self):\n", " output = sys.stdout\n", " self.build_entry_point(output)\n", " self.build_projects(output)\n", " \n", " def build_entry_point(self, output: TextIO):\n", " \"\"\"This builds the toplevel entry point, ready for inclusion in another Makefile\"\"\"\n", " toplevels = ' '.join(self.project_target(project) for project in self.projects)\n", " output.write('.PHONY: all-python-projects\\n')\n", " output.write(f'all-python-projects: {toplevels}\\n\\n')\n", " \n", " def project_target(self, project: Project) -> str:\n", " \"\"\"This converts a project to a distfile name, so that Make can know if it is up to date\"\"\"\n", " return f'{project.path}/dist/{project.name}-{project.version}.macosx-10.9-x86_64.tar.gz'\n", " \n", " def build_projects(self, output: TextIO):\n", " \"\"\"This generates all of the targets for each project we know about.\"\"\"\n", " for project in self.projects:\n", " self.build_project(project, output)\n", " \n", " def build_project(self, project: Project, output: TextIO):\n", " dependencies = [self.lookup_dependency(dep) for dep in project.dependencies]\n", " target_dependencies = ' '.join(self.project_target(dep) for dep in dependencies if dep)\n", " output.write(f'{self.project_target(project)}: {target_dependencies}\\n')\n", " output.write(f'\\tcd {project.path} && python setup.py bdist\\n\\n')\n", " \n", " def lookup_dependency(self, project_name: str) -> Optional[Project]:\n", " return next((project for project in self.projects if project.name == project_name), None)\n", "\n", "def load_projects() -> List[Project]:\n", " # the next line is commented out due to the fragility of the setup.py parsing logic\n", " # return [Project(Path(proj)) for (proj, subdirs, files) in os.walk(Path()) if 'setup.py' in files]\n", " return [Project(Path('shared/schema')), Project(Path('apps/cli/executables/alma_reingester')), Project(Path('shared/messaging/events'))]\n", "\n", "m = Makefile(load_projects())\n", "m.build()" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.3" } }, "nbformat": 4, "nbformat_minor": 4 }