diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2ce603d35f68cc2643cb2bc8e4c5d07c368dfbd5..0e31a2b916d9c32f9c79b8e657256959992cfc17 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,6 +1,9 @@ include: - local: "{{cookiecutter.project_slug}}/.gitlab-ci.yml" +variables: + GIT_SUBMODULE_STRATEGY: recursive + trigger_prepare: stage: prepare trigger: @@ -11,9 +14,29 @@ default: # Bootstrap Cookiecutter template to test provided ci pipeline template before_script: - python --version # For debugging + - git config --global user.name "unit test" + - git config --global user.email "info@astron.nl" - cookiecutter --no-input --overwrite-if-exists --output-dir . . - cd my_awesome_app - - git init + +# Override unit test before script +.run_unit_test_version_base: + before_script: + - pip install cookiecutter + - !reference [default, before_script] + - python --version # For debugging + - python -m pip install --upgrade pip + - pip install --upgrade tox twine + +# override package files before script +package_files: + before_script: + - pip install cookiecutter + - !reference [default, before_script] + artifacts: + expire_in: 1w + paths: + - my_awesome_app/dist/* # Override artifact directories run_unit_tests_coverage: diff --git a/README.md b/README.md index 4ca5706d6538a159d9333c68b054baae61542c01..8f9500e829779b23e5bf1440fa0a8b2a2b3b63ad 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,34 @@ -# Example Python Package - - - - -An example repository of an CI/CD pipeline for building, testing and publishing a python package. - -If you find some missing functionality with regards to CI/CD, testing, linting or something else, feel free to make a merge request with the proposed changes. +# Example Python Binary Extension Wheel Package + + + + +An example repository of an CI/CD pipeline for building, testing and publishing +a python package that uses binary extensions compiled as wheels. + +If you find some missing functionality with regards to CI/CD, testing, linting +or something else, feel free to make a merge request with the proposed changes. + +### Features: + +- Compile C / C++11 and bind with Python + - Easily define any C / C++ dependency to be included using `before-all = "apt / yum xxx"` + https://cibuildwheel.readthedocs.io/en/stable/options/#examples +- Automatic versioning using semantic version tags (`git tag vX.X.X`) + - Versions from `setuptools-scm` in Python are bound and forwarded to the + C / C++ extension using the `DynamicVersion` git submodule. +- Run any task performed by CI/CD locally such as: + - linting (`tox -e black`, `tox -e pylint`, `tox -e pep8`) + - automatically format code (`tox -e format`) + - unit tests (`tox -e py37`) + - code coverage (`tox -e coverage`) + - building & packaging (`tox -e build-local`, `tox -e build-ci-linux`) +- Automatically publish new releases to the gitlab package repository +- CI/CD uses [docker base image](docker/ci-runner/Dockerfile) to speed up pipeline ## How to apply this template -This templates uses `cookiecutter` which can be +This templates uses `cookiecutter` which can be installed easily: ```bash pip install --user cookiecutter @@ -18,9 +37,10 @@ pip install --user cookiecutter Then you can create an instance of your template with: ```bash -cookiecutter https://git.astron.nl/templates/python-package.git +cookiecutter https://git.astron.nl/templates/python-binary-wheel-package.git # Next follow a set of prompts (such as the name and description of the package) ``` ## License -This project is licensed under the Apache License Version 2.0 + +This project is licensed under the Apache License Version 2.0 \ No newline at end of file diff --git a/cookiecutter.json b/cookiecutter.json index b61933c29cb84cb9615e7cfcad595d2083900fb5..4536dbab8d034a904465539e2a99ed12a3401786 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -1,5 +1,6 @@ { "project_name": "My Awesome App", "project_slug": "{{ cookiecutter.project_name.lower()|replace(' ', '_')|replace('-', '_')|replace('.', '_')|trim() }}", + "project_url": "git.astron.nl/{{cookiecutter.project_slug}}", "description": "An example package for CI/CD working group" } diff --git a/hooks/post_gen_project.sh b/hooks/post_gen_project.sh new file mode 100644 index 0000000000000000000000000000000000000000..3e5640ee722600763cc9cc1cf216908fe869b487 --- /dev/null +++ b/hooks/post_gen_project.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +git init +git submodule add https://github.com/LecrisUT/CMakeExtraUtils.git cmake/cmake-extra-utils +git add --all +git commit -m "initial commit" --no-edit +git tag v0.0.1 \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/.git_archival.txt b/{{cookiecutter.project_slug}}/.git_archival.txt new file mode 100644 index 0000000000000000000000000000000000000000..8fb235d7045be0330d94bcb3abd2ac43badaa197 --- /dev/null +++ b/{{cookiecutter.project_slug}}/.git_archival.txt @@ -0,0 +1,4 @@ +node: $Format:%H$ +node-date: $Format:%cI$ +describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$ +ref-names: $Format:%D$ diff --git a/{{cookiecutter.project_slug}}/.gitattributes b/{{cookiecutter.project_slug}}/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..00a7b00c94e08b86c765d47689b6523148c46eec --- /dev/null +++ b/{{cookiecutter.project_slug}}/.gitattributes @@ -0,0 +1 @@ +.git_archival.txt export-subst diff --git a/{{cookiecutter.project_slug}}/.gitignore b/{{cookiecutter.project_slug}}/.gitignore index 3de3f10df4bb74f8d30e7ed9ebf98e44387b96f4..5513c1bf7cbca7a522e1ea44f306bda82a80975d 100644 --- a/{{cookiecutter.project_slug}}/.gitignore +++ b/{{cookiecutter.project_slug}}/.gitignore @@ -6,8 +6,11 @@ dist/* coverage.xml htmlcov/* -# Setuptools SCM -{{cookiecutter.project_slug}}/_version.py +# Tox +.tox + +# setuptools-scm +src/{{cookiecutter.project_slug}}/_version.py # IDE configuration .vscode diff --git a/{{cookiecutter.project_slug}}/.gitlab-ci.yml b/{{cookiecutter.project_slug}}/.gitlab-ci.yml index ead4cc5f552641f2afa4599d8f743e44e6ba2196..0632a2348cfcd3007f19d5215f859d1df9f6c137 100644 --- a/{{cookiecutter.project_slug}}/.gitlab-ci.yml +++ b/{{cookiecutter.project_slug}}/.gitlab-ci.yml @@ -10,8 +10,6 @@ default: stages: - prepare - lint - # check if this needs to be a separate step - # - build_extensions - test - package - integration @@ -47,11 +45,6 @@ run_pylint: - tox -e pylint allow_failure: true -# build_extensions: -# stage: build_extensions -# script: -# - echo "build fortran/c/cpp extension source code" - # Basic setup for all Python versions for which we don't have a base image .run_unit_test_version_base: before_script: @@ -68,7 +61,7 @@ run_unit_tests: - tox -e py3${PY_VERSION} parallel: matrix: # use the matrix for testing - - PY_VERSION: [7, 8, 9, 10] + - PY_VERSION: [8, 9, 10, 11] # Run code coverage on the base image thus also performing unit tests run_unit_tests_coverage: @@ -86,30 +79,37 @@ run_unit_tests_coverage: package_files: stage: package + image: python:3.8 artifacts: expire_in: 1w paths: - dist/* script: - - tox -e build + - curl -sSL https://get.docker.com/ | sh + - python -m pip install cibuildwheel==2.13.1 cookiecutter + - cibuildwheel --platform linux --output-dir dist package_docs: stage: package + allow_failure: true artifacts: expire_in: 1w paths: - docs/* # update path to match the dest dir for documentation script: - echo "build and collect docs" + - exit 1 run_integration_tests: stage: integration + allow_failure: true needs: - package_files script: - echo "make sure to move out of source dir" - echo "install package from filesystem (or use the artefact)" - echo "run against foreign systems (e.g. databases, cwl etc.)" + - exit 1 publish_on_gitlab: stage: publish @@ -163,6 +163,7 @@ publish_on_pypi: publish_to_readthedocs: stage: publish + allow_failure: true environment: readthedocs needs: - package_docs @@ -171,3 +172,4 @@ publish_to_readthedocs: - if: $CI_COMMIT_TAG script: - echo "scp docs/* ???" + - exit 1 diff --git a/{{cookiecutter.project_slug}}/CMakeLists.txt b/{{cookiecutter.project_slug}}/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..ce8b8df75115ebd9a9fca2c573e62155c9316b22 --- /dev/null +++ b/{{cookiecutter.project_slug}}/CMakeLists.txt @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 3.15...3.26) + +# Include submodule to dynamically determine version +set( + CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/cmake-extra-utils/cmake" +) + +# Include script +include(DynamicVersion) +dynamic_version( + ${SKBUILD_PROJECT_NAME} + PROJECT_PREFIX PROJECT_VERSION_ + PROJECT_SOURCE $ENV{DYNAMIC_VERSION_SOURCE} +) + +# Get project name from scikit-build-core and version DynamicVersion +project(${SKBUILD_PROJECT_NAME} VERSION ${PROJECT_VERSION}) + +# Get required packages +find_package(Python REQUIRED COMPONENTS Interpreter Development.Module) +find_package(pybind11 CONFIG REQUIRED) + +# Build our Pythonextension +python_add_library(_core MODULE src/main.cpp WITH_SOABI) +target_link_libraries(_core PRIVATE pybind11::headers) + +# Emit Python determined version as definition +target_compile_definitions(_core PRIVATE VERSION_INFO=${PROJECT_VERSION}) + +# Install the library into the build directory +install(TARGETS _core DESTINATION python_binary_wheel) diff --git a/{{cookiecutter.project_slug}}/MANIFEST.in b/{{cookiecutter.project_slug}}/MANIFEST.in index c4a3399ac9b36be786467e020c84f95e09db0606..dbb5e5ffd4d3b24b8cc8d5597bf55dfc4f83706a 100644 --- a/{{cookiecutter.project_slug}}/MANIFEST.in +++ b/{{cookiecutter.project_slug}}/MANIFEST.in @@ -1,5 +1,8 @@ include LICENSE include README.md +include CMakeLists.txt + recursive-include docs * +recursive-include docker * recursive-exclude tests * diff --git a/{{cookiecutter.project_slug}}/README.md b/{{cookiecutter.project_slug}}/README.md index 4a0ae9083c5f75f291e3e193972b9e5aabe5651e..4e6e8c75c5bbc2a58e17e98bb27d8b7bf49143b0 100644 --- a/{{cookiecutter.project_slug}}/README.md +++ b/{{cookiecutter.project_slug}}/README.md @@ -1,11 +1,10 @@ # {{cookiecutter.project_name}} -<!-- TODO: replace --> - - -<!--  --> + + + -An example repository of an CI/CD pipeline for building, testing and publishing a python package. +{{cookiecutter.description}} ## Installation ``` diff --git a/{{cookiecutter.project_slug}}/build-requirements.txt b/{{cookiecutter.project_slug}}/build-requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..357a5ca22a4257b5aadfc73ae9ea9a3f97d12ef7 --- /dev/null +++ b/{{cookiecutter.project_slug}}/build-requirements.txt @@ -0,0 +1,3 @@ +cibuildwheel >= 2.12.3 # BSD +tox >= 4.0 # ? +build >= 0.8.0 # MIT diff --git a/{{cookiecutter.project_slug}}/pyproject.toml b/{{cookiecutter.project_slug}}/pyproject.toml index 183007c3eb92a098dc3cc38fee60fc47e5f5884e..19e2b25e391f1db8db31e3091203ff79156f2e50 100644 --- a/{{cookiecutter.project_slug}}/pyproject.toml +++ b/{{cookiecutter.project_slug}}/pyproject.toml @@ -1,13 +1,61 @@ [build-system] requires = [ - "setuptools>=45", - "setuptools_scm[toml]>=6.2", - "wheel" + "scikit-build-core>=0.3.3", + "pybind11", + "setuptools_scm[toml]>=6.2" ] -build-backend = "setuptools.build_meta" +build-backend = "scikit_build_core.build" + +[project] +name = "{{cookiecutter.project_slug}}" +dynamic = ["version"] +description="{{cookiecutter.description}}" +readme = "README.md" +requires-python = ">=3.8" +classifiers = [ + "Development Status :: 4 - Beta", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] + +[tool.scikit-build] +experimental=true +wheel.expand-macos-universal-tags = true +metadata.version.provider = "scikit_build_core.metadata.setuptools_scm" +sdist.include = ["src/{{cookiecutter.project_slug}}/_version.py"] [tool.setuptools_scm] -write_to = "{{cookiecutter.project_slug}}/_version.py" +write_to = "src/{{cookiecutter.project_slug}}/_version.py" + +[tool.cibuildwheel] +skip = "pp*" +build-verbosity = 1 + +[tool.cibuildwheel.macos] +archs = ["x86_64", "universal2", "arm64"] + +[tool.cibuildwheel.linux] +archs = ["x86_64", "i686"] + +[tool.cibuildwheel.windows] +archs = ["AMD64", "x86"] [tool.pylint] ignore = "_version.py" + +[tool.flake8] +max-line-length = 88 +extend-ignore = ["E203"] +exclude = ["_version.py"] + +[tool.black] +line-length = 88 +extend-exclude = ''' +( + _version.py +) +''' diff --git a/{{cookiecutter.project_slug}}/setup.cfg b/{{cookiecutter.project_slug}}/setup.cfg deleted file mode 100644 index 1741808bc860dd1e2294dcd1ec8e0d7e6ed8fd2d..0000000000000000000000000000000000000000 --- a/{{cookiecutter.project_slug}}/setup.cfg +++ /dev/null @@ -1,38 +0,0 @@ -[metadata] -name = {{cookiecutter.project_slug}} -description = An example package for CI/CD working group -long_description = file: README.md -long_description_content_type = text/markdown -url = https://git.astron.nl/templates/python-package -license = Apache License 2.0 -classifiers = - Development Status :: 3 - Alpha - Environment :: Web Environment - Intended Audience :: Developers - Intended Audience :: Science/Research - License :: OSI Approved :: Apache Software License - Operating System :: OS Independent - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 - Topic :: Internet :: WWW/HTTP - Topic :: Internet :: WWW/HTTP :: Dynamic Content - Topic :: Scientific/Engineering - Topic :: Scientific/Engineering :: Astronomy - -[options] -include_package_data = true -packages = find: -python_requires = >=3.7 -install_requires = - importlib-metadata>=0.12, <5.0;python_version<"3.8" - numpy - -[flake8] -max-line-length = 88 -extend-ignore = E203 diff --git a/{{cookiecutter.project_slug}}/setup.py b/{{cookiecutter.project_slug}}/setup.py deleted file mode 100644 index b908cbe55cb344569d32de1dfc10ca7323828dc5..0000000000000000000000000000000000000000 --- a/{{cookiecutter.project_slug}}/setup.py +++ /dev/null @@ -1,3 +0,0 @@ -import setuptools - -setuptools.setup() diff --git a/{{cookiecutter.project_slug}}/src/main.cpp b/{{cookiecutter.project_slug}}/src/main.cpp new file mode 100644 index 0000000000000000000000000000000000000000..7873bc3950cb57684b7994da98a57fad240590b5 --- /dev/null +++ b/{{cookiecutter.project_slug}}/src/main.cpp @@ -0,0 +1,37 @@ +#include <pybind11/pybind11.h> + +#define STRINGIFY(x) #x +#define MACRO_STRINGIFY(x) STRINGIFY(x) + +int add(int i, int j) { + return i + j; +} + +namespace py = pybind11; + +PYBIND11_MODULE(_core, m) { + m.doc() = R"pbdoc( + Pybind11 example plugin + ----------------------- + + .. currentmodule:: python_binary_wheel + + .. autosummary:: + :toctree: _generate + + add + subtract + )pbdoc"; + + m.def("add", &add, R"pbdoc( + Add two numbers + + Some other explanation about the add function. + )pbdoc"); + + m.def("subtract", [](int i, int j) { return i - j; }, R"pbdoc( + Subtract two numbers + + Some other explanation about the subtract function. + )pbdoc"); +} diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/__init__.py b/{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/__init__.py similarity index 100% rename from {{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/__init__.py rename to {{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/__init__.py diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/cool_module.py b/{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/cool_module.py similarity index 100% rename from {{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/cool_module.py rename to {{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/cool_module.py diff --git a/{{cookiecutter.project_slug}}/tests/requirements.txt b/{{cookiecutter.project_slug}}/tests/requirements.txt index f14d0b907ce977320ba250046dfab52a3101800a..82f1ee820e4b57fd3e720ad74078688cd87c2ead 100644 --- a/{{cookiecutter.project_slug}}/tests/requirements.txt +++ b/{{cookiecutter.project_slug}}/tests/requirements.txt @@ -2,6 +2,7 @@ autopep8 >= 1.7.0 # MIT black >= 22.0.0 # MIT build >= 0.8.0 # MIT flake8 >= 5.0.0 # MIT +pyproject-flake8 >= 5.0.4 # Unlicense pylint >= 2.15.0 # GPLv2 pytest >= 7.0.0 # MIT pytest-cov >= 3.0.0 # MIT diff --git a/{{cookiecutter.project_slug}}/tox.ini b/{{cookiecutter.project_slug}}/tox.ini index 389403d52064a498216d8eb281afa0e60af09d6a..4351731eb17c544963991d2a0a35d9848e64ed19 100644 --- a/{{cookiecutter.project_slug}}/tox.ini +++ b/{{cookiecutter.project_slug}}/tox.ini @@ -1,17 +1,17 @@ [tox] # Generative environment list to test all supported Python versions -envlist = py3{7,8,9,10},black,pep8,pylint +envlist = py3{8,9,10,11},black,pep8,pylint minversion = 3.18.0 [testenv] -usedevelop = True -package = wheel -wheel_build_env = .pkg +package = sdist # 'Source' package required for binary extension +use_develop = False # use_develop implies 'editable' package, not possible setenv = LANGUAGE=en_US LC_ALL=en_US.UTF-8 PYTHONWARNINGS=default::DeprecationWarning + DYNAMIC_VERSION_SOURCE={toxinidir} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/tests/requirements.txt @@ -31,16 +31,20 @@ usedevelop = False envdir = {toxworkdir}/linting commands = pep8: {envpython} -m flake8 --version - pep8: {envpython} -m flake8 --extend-exclude './.venv/','./venv/' + pep8: {envpython} -m flake8 src tests black: {envpython} -m black --version - black: {envpython} -m black --check --diff . + black: {envpython} -m black --check --diff src tests pylint: {envpython} -m pylint --version - pylint: {envpython} -m pylint {{cookiecutter.project_slug}} tests - format: {envpython} -m autopep8 -v -aa --in-place --recursive {{cookiecutter.project_slug}} + pylint: {envpython} -m pylint src tests + format: {envpython} -m autopep8 -v -aa --in-place --recursive src format: {envpython} -m autopep8 -v -aa --in-place --recursive tests - format: {envpython} -m black -v . + format: {envpython} -m black -v src tests - -[testenv:build] -usedevelop = False -commands = {envpython} -m build +[testenv:{build-local,build-ci-linux}] +deps = + -r{toxinidir}/tests/requirements.txt + -r{toxinidir}/build-requirements.txt +skip_install = True +commands = + build-local: {envpython} -m build + build-ci-linux: {envpython} -m cibuildwheel --platform linux --output-dir dist