From d22ffbacb4228cb4ffdc6396bca9e43ad194bfff Mon Sep 17 00:00:00 2001 From: euri10 <benoit.barthelet@gmail.com> Date: Tue, 15 Oct 2024 12:25:11 +0000 Subject: [PATCH] feat(uv): add uv support as a new build system --- README.md | 16 ++--- kicker.json | 2 +- templates/gitlab-ci-python.yml | 109 ++++++++++++++++++++++++++++++--- 3 files changed, 110 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 1e99903..4fa3248 100644 --- a/README.md +++ b/README.md @@ -64,12 +64,13 @@ By default it tries to auto-detect the build system used by the project (based o and/or `setup.py` and/or `requirements.txt`), but the build system might also be set explicitly using the `$PYTHON_BUILD_SYSTEM` variable: -| Value | Build System (scope) | -| ---------------- | ---------------------------------------------------------- | -| _none_ (default) or `auto` | The template tries to **auto-detect** the actual build system | -| `setuptools` | [Setuptools](https://setuptools.pypa.io/) (dependencies, build & packaging) | -| `poetry` | [Poetry](https://python-poetry.org/) (dependencies, build, test & packaging) | -| `pipenv` | [Pipenv](https://pipenv.pypa.io/) (dependencies only) | +| Value | Build System (scope) | +| ---------------- |--------------------------------------------------------------------------------------------------------| +| _none_ (default) or `auto` | The template tries to **auto-detect** the actual build system | +| `setuptools` | [Setuptools](https://setuptools.pypa.io/) (dependencies, build & packaging) | +| `poetry` | [Poetry](https://python-poetry.org/) (dependencies, build, test & packaging) | +| `uv` | [uv](https://docs.astral.sh/uv/) (dependencies, build, test & packaging)| +| `pipenv` | [Pipenv](https://pipenv.pypa.io/) (dependencies only) | | `reqfile` | [Requirements Files](https://pip.pypa.io/en/stable/user_guide/#requirements-files) (dependencies only) | :warning: You can explicitly set the build tool version by setting `$PYTHON_BUILD_SYSTEM` variable including a [version identification](https://peps.python.org/pep-0440/). For example `PYTHON_BUILD_SYSTEM="poetry==1.1.15"` @@ -372,9 +373,10 @@ This job is **disabled by default** and allows to perform a complete release of 3. build the [Python packages](https://packaging.python.org/), 4. publish the built packages to a PyPI compatible repository ([GitLab packages](https://docs.gitlab.com/ee/user/packages/pypi_repository/) by default). -The Python template supports two packaging systems: +The Python template supports three packaging systems: * [Poetry](https://python-poetry.org/): uses Poetry-specific [version](https://python-poetry.org/docs/cli/#version), [build](https://python-poetry.org/docs/cli/#build) and [publish](https://python-poetry.org/docs/cli/#publish) commands. +* [uv](https://docs.astral.sh/uv/): uses [uv](https://docs.astral.sh/uv/) as version management, [build](https://docs.astral.sh/uv/guides/publish/#building-your-package) as package builder and [publish](https://docs.astral.sh/uv/guides/publish/) to publish. * [Setuptools](https://setuptools.pypa.io/): uses [bump-my-version](https://github.com/callowayproject/bump-my-version) as version management, [build](https://pypa-build.readthedocs.io/) as package builder and [Twine](https://twine.readthedocs.io/) to publish. The release job is bound to the `publish` stage, appears only on production and integration branches and uses the following variables: diff --git a/kicker.json b/kicker.json index bd36a3a..687a3c8 100644 --- a/kicker.json +++ b/kicker.json @@ -21,7 +21,7 @@ "name": "PYTHON_BUILD_SYSTEM", "description": "Python build-system to use to install dependencies, build and package the project", "type": "enum", - "values": ["auto", "setuptools", "poetry", "pipenv", "reqfile"], + "values": ["auto", "setuptools", "poetry", "pipenv", "reqfile", "uv"], "default": "auto", "advanced": true }, diff --git a/templates/gitlab-ci-python.yml b/templates/gitlab-ci-python.yml index 86604cb..6f00849 100644 --- a/templates/gitlab-ci-python.yml +++ b/templates/gitlab-ci-python.yml @@ -29,6 +29,7 @@ spec: - poetry - pipenv - reqfile + - uv default: auto reqs-file: description: |- @@ -569,7 +570,7 @@ variables: case "${PYTHON_BUILD_SYSTEM:-auto}" in auto) ;; - poetry*|setuptools*|pipenv*) + poetry*|setuptools*|pipenv*|uv*) log_info "--- Build system explicitly declared: ${PYTHON_BUILD_SYSTEM}" return ;; @@ -588,6 +589,17 @@ variables: export PYTHON_BUILD_SYSTEM="reqfile" return fi + + if [[ -f "uv.lock" ]] + then + if [[ -f "pyproject.toml" ]] + then + log_info "--- Build system auto-detected: uv (uv.lock and pyproject.toml)" + export PYTHON_BUILD_SYSTEM="uv" + return + fi + log_error "--- Build system auto-detected: uv (uv.lock) but no pyproject.toml found: please read template doc" + fi if [[ -f "pyproject.toml" ]] then @@ -636,6 +648,13 @@ variables: pip install ${PIP_OPTS} "$PYTHON_BUILD_SYSTEM" fi } + function maybe_install_uv() { + if [[ "$PYTHON_BUILD_SYSTEM" =~ ^uv ]] && ! command -v uv > /dev/null + then + # shellcheck disable=SC2086 + pip install ${PIP_OPTS} "$PYTHON_BUILD_SYSTEM" + fi + } # install requirements function install_requirements() { @@ -680,6 +699,13 @@ variables: log_warn "--- requirements build system defined, but no ${PYTHON_REQS_FILE} file found" fi ;; + uv*) + if [[ ! -f "uv.lock" ]]; then + log_warn "Using uv but \\e[33;1muv.lock\\e[0m file not found: you shall commit it with your project files" + fi + maybe_install_uv + uv sync --frozen ${PYTHON_EXTRA_DEPS:+--extras "$PYTHON_EXTRA_DEPS"} + ;; esac } @@ -688,6 +714,10 @@ variables: then maybe_install_poetry poetry run "$@" + elif [[ "$PYTHON_BUILD_SYSTEM" =~ ^uv ]] + then + maybe_install_uv + uv run "$@" else "$@" fi @@ -698,8 +728,15 @@ variables: } function _pip() { - # shellcheck disable=SC2086 - _run pip ${PIP_OPTS} "$@" + if [[ "$PYTHON_BUILD_SYSTEM" =~ ^uv ]] + then + maybe_install_uv + # shellcheck disable=SC2086 + uv pip ${PIP_OPTS} "$@" + else + # shellcheck disable=SC2086 + _run pip ${PIP_OPTS} "$@" + fi } function py_package() { @@ -707,6 +744,10 @@ variables: then maybe_install_poetry poetry build + elif [[ "$PYTHON_BUILD_SYSTEM" =~ ^uv ]] + then + maybe_install_uv + uv build else # shellcheck disable=SC2086 pip install ${PIP_OPTS} build @@ -772,6 +813,33 @@ variables: py_commit_message=$(python -c "print('$PYTHON_RELEASE_COMMIT_MESSAGE'.format(current_version='$py_cur_version', new_version='$py_next_version'))") git commit -m "$py_commit_message" git tag "$py_next_version" + elif [[ "$PYTHON_BUILD_SYSTEM" =~ ^uv ]] + then + maybe_install_uv + if [[ -z "$py_next_version" ]] + then + # quick version waiting for uv to manage bump + # related uv MR https://github.com/astral-sh/uv/pull/7248#issuecomment-2395465334 + mkdir -p -m 777 tbc_tmp + uvx --from toml-cli toml get --toml-path pyproject.toml project.version > tbc_tmp/version.txt + py_cur_version=$(cat tbc_tmp/version.txt) + + py_release_part="$PYTHON_RELEASE_NEXT" + log_info "[bump-my-version] increase \\e[1;94m${py_release_part}\\e[0m (from current \\e[1;94m${py_cur_version}\\e[0m)" + uvx bump-my-version bump ${TRACE+--verbose} --current-version "$py_cur_version" "$py_release_part" tbc_tmp/version.txt + py_next_version=$(cat tbc_tmp/version.txt) + rm -fr tbc_tmp/version.txt + fi + + log_info "[uv] change version \\e[1;94m${py_cur_version}\\e[0m → \\e[1;94m${py_next_version}\\e[0m" + uvx --from toml-cli toml set --toml-path pyproject.toml project.version "$py_next_version" + + # Git commit and tag + git add pyproject.toml + # emulate bump-my-version to generate commit message + py_commit_message=$(python -c "print('$PYTHON_RELEASE_COMMIT_MESSAGE'.format(current_version='$py_cur_version', new_version='$py_next_version'))") + git commit -m "$py_commit_message" + git tag --force "$py_next_version" else # Setuptools / bump-my-version # shellcheck disable=SC2086 @@ -825,6 +893,18 @@ variables: log_info "--- publish packages (poetry) to $PYTHON_REPOSITORY_URL with user $PYTHON_REPOSITORY_USERNAME..." poetry config repositories.user_defined "$PYTHON_REPOSITORY_URL" poetry publish ${TRACE+--verbose} --username "$PYTHON_REPOSITORY_USERNAME" --password "$PYTHON_REPOSITORY_PASSWORD" --repository user_defined + elif [[ "$PYTHON_BUILD_SYSTEM" =~ ^uv ]] + then + maybe_install_uv + + if [[ "$PYTHON_PACKAGE_ENABLED" != "true" ]] + then + log_info "--- build packages (uv)..." + uv build ${TRACE+--verbose} + fi + + log_info "--- publish packages (uv) to $PYTHON_REPOSITORY_URL with user $PYTHON_REPOSITORY_USERNAME..." + uv publish ${TRACE+--verbose} --username "$PYTHON_REPOSITORY_USERNAME" --password "$PYTHON_REPOSITORY_PASSWORD" --publish-url "$PYTHON_REPOSITORY_URL" else # shellcheck disable=SC2086 pip install ${PIP_OPTS} build twine @@ -887,6 +967,7 @@ stages: PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" POETRY_CACHE_DIR: "$CI_PROJECT_DIR/.cache/poetry" PIPENV_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pipenv" + UV_CACHE_DIR: "$CI_PROJECT_DIR/.cache/uv" POETRY_VIRTUALENVS_IN_PROJECT: "false" cache: key: "$CI_COMMIT_REF_SLUG-python" @@ -956,7 +1037,7 @@ py-black: script: - install_requirements - _pip install black - - _run black . --check + - _run black . --check --extend-exclude '(\/\.cache\/|\/\.venv\/)' rules: # exclude if $PYTHON_BLACK_ENABLED not set - if: '$PYTHON_BLACK_ENABLED != "true"' @@ -969,7 +1050,7 @@ py-isort: script: - install_requirements - _pip install isort - - _run isort . --check-only --extend-skip .cache + - _run isort . --check-only --extend-skip .cache --extend-skip .venv rules: # exclude if $PYTHON_ISORT_ENABLED not set - if: '$PYTHON_ISORT_ENABLED != "true"' @@ -1018,7 +1099,7 @@ py-mypy: - mkdir -p -m 777 reports - install_requirements - _pip install mypy mypy-to-codeclimate - - _run mypy ${MYPY_ARGS} ${MYPY_FILES:-$(find -type f -name "*.py" -not -path "./.cache/*")} | tee reports/py-mypy.console.txt || true + - _run mypy ${MYPY_ARGS} ${MYPY_FILES:-$(find -type f -name "*.py" -not -path "./.cache/*" -not -path "./.venv/*")} | tee reports/py-mypy.console.txt || true # mypy-to-codeclimate will fail if any error was found - _run mypy-to-codeclimate reports/py-mypy.console.txt reports/py-mypy.codeclimate.json artifacts: @@ -1140,15 +1221,15 @@ py-bandit: - | if [[ "$SONAR_HOST_URL" ]] then - _run bandit ${TRACE+--verbose} --exit-zero --exclude ./.cache --format csv --output reports/py-bandit.bandit.csv ${BANDIT_ARGS} + _run bandit ${TRACE+--verbose} --exit-zero --exclude ./.cache --exclude ./.venv --format csv --output reports/py-bandit.bandit.csv ${BANDIT_ARGS} fi # JSON (for DefectDojo) - | if [[ "$DEFECTDOJO_BANDIT_REPORTS" ]] then - _run bandit ${TRACE+--verbose} --exit-zero --exclude ./.cache --format json --output reports/py-bandit.bandit.json ${BANDIT_ARGS} + _run bandit ${TRACE+--verbose} --exit-zero --exclude ./.cache --exclude ./.venv --format json --output reports/py-bandit.bandit.json ${BANDIT_ARGS} fi - - _run bandit ${TRACE+--verbose} --exclude ./.cache ${BANDIT_ARGS} + - _run bandit ${TRACE+--verbose} --exclude ./.cache --exclude ./.venv ${BANDIT_ARGS} artifacts: when: always name: "$CI_JOB_NAME artifacts from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG" @@ -1194,6 +1275,11 @@ py-trivy: log_info "$PYTHON_BUILD_SYSTEM build system (\\e[32muse lock file\\e[0m)" cp poetry.lock Pipfile.lock ./reports 2>/dev/null || true ;; + uv*) + log_info "$PYTHON_BUILD_SYSTEM build system used (\\e[32mmust generate pinned requirements.txt from uv.lock\\e[0m)" + maybe_install_uv + uv export > ./reports/requirements.txt + ;; *) log_info "$PYTHON_BUILD_SYSTEM build system used (\\e[32mmust generate pinned requirements.txt\\e[0m)" install_requirements @@ -1244,6 +1330,11 @@ py-sbom: poetry*|pipenv*) log_info "$PYTHON_BUILD_SYSTEM build system (\\e[32muse lock file\\e[0m)" ;; + uv*) + log_info "$PYTHON_BUILD_SYSTEM build system used (\\e[32mmust generate pinned requirements.txt from uv.lock\\e[0m)" + maybe_install_uv + uv export > ./reports/requirements.txt + ;; *) log_info "$PYTHON_BUILD_SYSTEM build system used (\\e[32mmust generate pinned requirements.txt\\e[0m)" install_requirements -- GitLab