├── .github └── workflows │ └── pythonapp.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── cpm ├── __init__.py ├── api │ ├── __init__.py │ ├── build.py │ ├── clean.py │ ├── create.py │ ├── init.py │ ├── install.py │ ├── prep.py │ ├── publish.py │ ├── result.py │ └── test.py ├── argument_parser.py ├── domain │ ├── __init__.py │ ├── bit_download.py │ ├── bit_installer.py │ ├── cmake │ │ ├── __init__.py │ │ └── cmakelists_builder.py │ ├── cmake_builder.py │ ├── compilation_service.py │ ├── constants.py │ ├── creation_service.py │ ├── install_service.py │ ├── project │ │ ├── __init__.py │ │ ├── project.py │ │ ├── project_composer.py │ │ ├── project_descriptor.py │ │ ├── project_descriptor_editor.py │ │ ├── project_descriptor_parser.py │ │ ├── project_loader.py │ │ └── project_template.py │ ├── project_commands.py │ ├── project_packager.py │ ├── publish_service.py │ ├── sample_code.py │ ├── template_download.py │ ├── template_installer.py │ ├── template_packager.py │ └── test_service.py └── infrastructure │ ├── __init__.py │ ├── cpm_hub_connector_v1.py │ ├── cpm_user_configuration.py │ ├── filesystem.py │ ├── http_client.py │ └── yaml_parser.py ├── pyproject.toml ├── scripts ├── cpm └── cpm_bash_completion ├── setup.cfg ├── setup.py └── test ├── __init__.py ├── api ├── __init__.py ├── test_api_build.py ├── test_api_clean.py ├── test_api_create.py ├── test_api_init.py ├── test_api_install.py ├── test_api_prep.py ├── test_api_publish.py └── test_api_test.py ├── domain ├── __init__.py ├── file.yaml ├── test_bit_installer.py ├── test_bit_packager.py ├── test_cmakelists_builder.py ├── test_compilation_plan_parser.py ├── test_compilation_service.py ├── test_creation_service.py ├── test_project_composer.py ├── test_project_descriptor_editor.py ├── test_project_descriptor_parser.py ├── test_project_loader.py ├── test_publish_service.py ├── test_template_installer.py ├── test_template_packager.py └── test_test_service.py ├── e2e ├── __init__.py ├── conftest.py ├── environment │ ├── Dockerfile │ ├── __init__.py │ ├── cpmhub_mock.py │ ├── docker-compose-e2e.yaml │ ├── samples │ │ ├── base64_1.0.json │ │ ├── cest_1.0.json │ │ ├── sqlite3_3.32.3.json │ │ └── test_1.0.json │ ├── templates │ │ ├── .keep │ │ └── test_project_0.0.1.json │ └── user_include.h ├── test_cpm.py └── test_schema_validation.py └── infrastructure ├── __init__.py ├── test_cpm_hub_connector_v1.py └── test_cpm_user_configuration.py /.github/workflows/pythonapp.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Set up Python 3.7 13 | uses: actions/setup-python@v1 14 | with: 15 | python-version: 3.7 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install -e .[testing] 20 | sudo apt install -yq ninja-build g++ 21 | - name: Lint with flake8 22 | run: | 23 | pip install flake8 24 | # stop the build if there are Python syntax errors or undefined names 25 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 26 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 27 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 28 | - name: Test with pytest 29 | run: | 30 | pytest 31 | - name: Build dist package 32 | if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') 33 | run: | 34 | pip install pep517 35 | python -m pep517.build --source --binary --out-dir dist/ . 36 | - name: Publish package 37 | if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') 38 | uses: pypa/gh-action-pypi-publish@master 39 | with: 40 | user: __token__ 41 | password: ${{ secrets.pypi_token }} 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing to cpm 2 | 3 | You can contribute to cpm in several ways. 4 | 5 | ### Have some feedback? 6 | 7 | If you are a cpm user, we'd love to hear from you. What do you think about the tool? Do you have an idea on how to improve it? 8 | In this case, the best place to start is to open a new [discussion](https://github.com/jorsanpe/cpm/discussions/new) so we can 9 | give shape to your suggestion before creating an issue. 10 | 11 | ## Pull requests 12 | 13 | cpm is developed following TDD and some other practices. Before sending a pull request, make sure that your code is properly 14 | tested and please try to keep as much as possible the current shape of the code. 15 | 16 | ### Found a bug? 17 | 18 | Please, file an issue so that we can take care of it. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cpm: A modern project management tool for C/C++ projects ![CI](https://github.com/jorsanpe/cpm/workflows/CI/badge.svg) 2 | 3 | You can find the [documentation](https://cpmbits.com/documentation/getting-started.html) in the [cpmbits website](https://cpmbits.com). 4 | 5 | ## Installation 6 | `pip3 install cpm-cli` 7 | 8 | CPM depends on [CMake](https://cmake.org/) and [ninja](https://ninja-build.org/) for the build process. 9 | 10 | ## Getting started 11 | ``` 12 | cpm create DeathStartLaserBackend 13 | cd DeathStartLaserBackend 14 | cpm build 15 | ``` 16 | 17 | After creating the project, the binary will be available in the project `build` directory. 18 | ``` 19 | ./build/DeathStartLaserBackend 20 | ``` 21 | 22 | ### Manage dependencies 23 | CPM manages your project dependencies through CPM-Hub. In order to install a bit, declare it in the project descriptor: 24 | 25 | ```yaml 26 | build: 27 | bits: 28 | sqlite3: '3.32.3' 29 | test: 30 | bits: 31 | cest: '1.0' 32 | ``` 33 | 34 | Then use the command line to install all dependencies. 35 | 36 | ``` 37 | cpm install 38 | ``` 39 | 40 | ### Run your tests 41 | ``` 42 | cpm test 43 | ``` 44 | 45 | Test sources reside in the `tests` directory. `cpm` will consider as test suites any files that match the expression 46 | `test_*.cpp`. 47 | -------------------------------------------------------------------------------- /cpm/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.7 2 | import importlib 3 | import sys 4 | import pkgutil 5 | import pkg_resources 6 | import datetime 7 | import argparse 8 | 9 | from cpm.argument_parser import ArgumentParser 10 | from cpm.api.result import FAIL 11 | import cpm.api 12 | 13 | 14 | def handle_keyboard_interrupt(func): 15 | def wrapper(*args, **kwargs): 16 | try: 17 | result = func(*args, **kwargs) 18 | except KeyboardInterrupt: 19 | print(f'\rcpm: cancelled') 20 | sys.exit(FAIL) 21 | return result 22 | return wrapper 23 | 24 | 25 | @handle_keyboard_interrupt 26 | def main(): 27 | commands = available_commands() 28 | top_level_parser = argument_parser(commands) 29 | 30 | args = top_level_parser.parse_args(sys.argv[1:]) 31 | 32 | if not args.command: 33 | print_help(top_level_parser, commands) 34 | sys.exit(0) 35 | 36 | command_to_execute = args.command[0] 37 | command_arguments = args.command[1:] 38 | 39 | if command_to_execute == 'help': 40 | if len(command_arguments) == 0: 41 | print_help(top_level_parser, commands) 42 | else: 43 | commands[args.command[1]].print_help() 44 | print() 45 | sys.exit(0) 46 | 47 | if command_to_execute not in commands: 48 | print(f'cpm: error: unknown command \'{command_to_execute}\', see \'cpm help\' for a list of available commands') 49 | sys.exit(1) 50 | 51 | start_time = datetime.datetime.now() 52 | 53 | result = commands[command_to_execute].execute(command_arguments) 54 | 55 | elapsed_time = datetime.datetime.now() - start_time 56 | finish(result, elapsed_time) 57 | 58 | 59 | def available_commands(): 60 | commands = {} 61 | for api_action in api_commands(): 62 | module = importlib.import_module(api_action.name) 63 | if hasattr(module, 'execute'): 64 | commands[module_name(api_action)] = module 65 | return commands 66 | 67 | 68 | def argument_parser(commands): 69 | top_level_parser = ArgumentParser(description='cpm: a modern project management tool for C/C++ projects') 70 | top_level_parser.add_argument('-v', '--version', 71 | action='version', 72 | help='show version and exit', 73 | version=f'cpm version {pkg_resources.require("cpm-cli")[0].version}') 74 | top_level_parser.add_argument('command', 75 | choices=list(commands.keys()) + ['help'], 76 | nargs=argparse.REMAINDER) 77 | return top_level_parser 78 | 79 | 80 | def print_help(top_level_parser, commands): 81 | help_text = 'Usage: cpm [-h | --help] [-v | --version] [args]' 82 | 83 | help_text += '\n\nOptional Arguments:' 84 | for option in top_level_parser.options: 85 | if 'command' in option['flags']: 86 | continue 87 | help_text += f'\n\t{", ".join(option["flags"]):<20}{option["help"]}' 88 | 89 | help_text += '\n\nAvailable commands:' 90 | for command in sorted(commands.keys()): 91 | help_text += f'\n\t{command:<20}{commands[command].description()}' 92 | 93 | help_text += '\n\nYou can get more detailed help about any specific command by running:\ncpm help ' 94 | help_text += '\n\n' 95 | 96 | print(help_text) 97 | 98 | 99 | def module_name(api_command): 100 | return api_command.name.split('.')[-1] 101 | 102 | 103 | def api_commands(): 104 | return list(pkgutil.iter_modules(cpm.api.__path__, cpm.api.__name__+'.')) 105 | 106 | 107 | def finish(result, elapsed_time): 108 | print(f'cpm: {result.message} (took {__format(elapsed_time)})') 109 | sys.exit(result.status_code) 110 | 111 | 112 | def __format(elapsed_time): 113 | if elapsed_time.seconds >= 60: 114 | return '%dm %d.%ds' % (elapsed_time.seconds/60, elapsed_time.seconds % 60, elapsed_time.microseconds/1000) 115 | else: 116 | return '%d.%ds' % (elapsed_time.seconds, elapsed_time.microseconds/1000) 117 | 118 | 119 | if __name__ == '__main__': 120 | main() 121 | -------------------------------------------------------------------------------- /cpm/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpmbits/cpm/8b814f3c8a914ea4db9f03df2160e8eb88226f14/cpm/api/__init__.py -------------------------------------------------------------------------------- /cpm/api/build.py: -------------------------------------------------------------------------------- 1 | from cpm.argument_parser import ArgumentParser 2 | from cpm.api.result import Result, OK, FAIL 3 | from cpm.domain.cmake.cmakelists_builder import CMakeListsBuilder 4 | from cpm.domain.compilation_service import CompilationService 5 | from cpm.domain.project_commands import DockerImageNotFound 6 | from cpm.domain.project.project_descriptor_parser import ProjectDescriptorNotFound, ParseError 7 | from cpm.domain.project.project_loader import ProjectLoader, InvalidTarget 8 | from cpm.domain.project_commands import ProjectCommands, BuildError 9 | from cpm.api import install 10 | 11 | 12 | def build_project(compilation_service, target='default'): 13 | try: 14 | print(f'cpm: building target {target}') 15 | compilation_service.build(target) 16 | except ProjectDescriptorNotFound: 17 | return Result(FAIL, f'error: not a cpm project') 18 | except BuildError: 19 | return Result(FAIL, f'error: compilation failed') 20 | except InvalidTarget: 21 | return Result(FAIL, f'error: unknown target {target}') 22 | except DockerImageNotFound as e: 23 | return Result(FAIL, f'error: docker image {e.image_name} not found for target {target}') 24 | except ParseError as e: 25 | return Result(FAIL, f'error: {e.message}') 26 | 27 | return Result(OK, f'Build finished') 28 | 29 | 30 | def execute(argv): 31 | install.execute([]) 32 | 33 | create_parser = argument_parser() 34 | args = create_parser.parse_args(argv) 35 | 36 | project_loader = ProjectLoader() 37 | cmakelists_builder = CMakeListsBuilder() 38 | project_commands = ProjectCommands() 39 | service = CompilationService(project_loader, cmakelists_builder, project_commands) 40 | 41 | result = build_project(service, args.target) 42 | 43 | return result 44 | 45 | 46 | def argument_parser(): 47 | create_parser = ArgumentParser(prog='cpm build', description=description()) 48 | create_parser.add_argument('target', 49 | help='target to build', 50 | nargs='?', 51 | default='default') 52 | return create_parser 53 | 54 | 55 | def print_help(): 56 | return argument_parser().print_help() 57 | 58 | 59 | def description(): 60 | return 'build the project for the given target (use \'default\' if none is given)' 61 | -------------------------------------------------------------------------------- /cpm/api/clean.py: -------------------------------------------------------------------------------- 1 | from cpm.argument_parser import ArgumentParser 2 | from cpm.api.result import Result 3 | from cpm.api.result import OK 4 | from cpm.api.result import FAIL 5 | from cpm.domain.cmake.cmakelists_builder import CMakeListsBuilder 6 | from cpm.domain.project_commands import ProjectCommands 7 | from cpm.domain.project.project_descriptor_parser import ProjectDescriptorNotFound 8 | from cpm.domain.project.project_loader import ProjectLoader 9 | from cpm.domain.compilation_service import CompilationService 10 | 11 | 12 | def clean_project(compilation_service): 13 | try: 14 | compilation_service.clean() 15 | except ProjectDescriptorNotFound: 16 | return Result(FAIL, f'error: not a cpm project') 17 | 18 | return Result(OK, f'clean finished') 19 | 20 | 21 | def execute(argv): 22 | project_loader = ProjectLoader() 23 | cmakelists_builder = CMakeListsBuilder() 24 | project_builder = ProjectCommands() 25 | service = CompilationService(project_loader, cmakelists_builder, project_builder) 26 | 27 | result = clean_project(service) 28 | 29 | return result 30 | 31 | 32 | def argument_parser(): 33 | return ArgumentParser(prog='cpm clean', description=description()) 34 | 35 | 36 | def print_help(): 37 | return argument_parser().print_help() 38 | 39 | 40 | def description(): 41 | return 'clean all build files' 42 | -------------------------------------------------------------------------------- /cpm/api/create.py: -------------------------------------------------------------------------------- 1 | from cpm.argument_parser import ArgumentParser 2 | from cpm.api.result import Result 3 | from cpm.api.result import OK 4 | from cpm.api.result import FAIL 5 | from cpm.domain.creation_service import CreationService 6 | from cpm.domain.creation_service import CreationOptions 7 | from cpm.domain.project.project_loader import ProjectLoader 8 | from cpm.infrastructure.cpm_hub_connector_v1 import CpmHubConnectorV1, TemplateNotFound 9 | 10 | 11 | def new_project(creation_service, options=CreationOptions()): 12 | try: 13 | if creation_service.exists(options.directory): 14 | return Result(FAIL, f'error: directory {options.directory} already exists') 15 | creation_service.create(options) 16 | except TemplateNotFound: 17 | return Result(FAIL, f'error: template {options.template_name}:{options.template_version} not found') 18 | return Result(OK, f'Created project {options.project_name}') 19 | 20 | 21 | def execute(argv): 22 | create_parser = argument_parser() 23 | args = create_parser.parse_args(argv) 24 | 25 | project_loader = ProjectLoader() 26 | cpm_hub_connector = CpmHubConnectorV1(repository_url=args.repository_url) 27 | service = CreationService(project_loader, cpm_hub_connector=cpm_hub_connector) 28 | template_name, template_version = __template_to_use(args.template) 29 | 30 | options = CreationOptions( 31 | project_name=args.project_name, 32 | directory=args.project_name, 33 | init_from_existing_sources=False, 34 | init_from_template=True if args.template else False, 35 | template_name=template_name, 36 | template_version=template_version 37 | ) 38 | result = new_project(service, options) 39 | 40 | return result 41 | 42 | 43 | def argument_parser(): 44 | create_parser = ArgumentParser(prog='cpm create', description='Create a new cpm project') 45 | create_parser.add_argument('project_name', 46 | help='name of the project to create') 47 | create_parser.add_argument('-s', '--repository-url', 48 | required=False, 49 | action='store', 50 | arg_format='', 51 | help='URL of the cpm repository, used when creating project from template (default https://repo.cpmbits.com:8000)', 52 | default='https://repo.cpmbits.com:8000') 53 | create_parser.add_argument('-t', '--template', 54 | arg_format='