From a319984282dec7c5eddafa8aa8eb0c2b9071916b Mon Sep 17 00:00:00 2001 From: Mark Elliot <123787712+mark-thm@users.noreply.github.com> Date: Thu, 30 May 2024 11:40:44 -0400 Subject: [PATCH] Add support for passing non-default arguments to uv pip compile --- readme.md | 7 +++ uv/private/pip.bzl | 90 +++++++++++++++++++++++++++------- uv/private/pip_compile.sh | 20 ++------ uv/private/pip_compile_test.sh | 25 +++------- 4 files changed, 93 insertions(+), 49 deletions(-) diff --git a/readme.md b/readme.md index dd7983a..9f3af81 100644 --- a/readme.md +++ b/readme.md @@ -34,6 +34,13 @@ Run the compilation step with `bazel run //:generate_requirements_txt`. This will automatically register a diff test with name `[name]_diff_test`. +Additionally, you can specify the following optional args: + +- `python_platform`: the `uv pip compile` compatible `--python-platform` value to pass to uv +- `args`: override the default arguments passed to uv (`--generate-hashes`, `--emit-index-url` and `--no-strip-extras`) +- `tags`: tags to apply to the test target +- `target_compatible_with`: restrict targets to running on the specified Bazel platform + ### create_venv Create a virtual environment creation target: diff --git a/uv/private/pip.bzl b/uv/private/pip.bzl index 6bcaaf0..593c8af 100644 --- a/uv/private/pip.bzl +++ b/uv/private/pip.bzl @@ -2,37 +2,51 @@ _PY_TOOLCHAIN = "@bazel_tools//tools/python:toolchain_type" -_common_attrs = { +_COMMON_ATTRS = { "requirements_in": attr.label(mandatory = True, allow_single_file = True), "requirements_txt": attr.label(mandatory = True, allow_single_file = True), - "python_platform": attr.string(default = ""), + "python_platform": attr.string(), "_uv": attr.label(default = "@multitool//tools/uv", executable = True, cfg = "exec"), } +_DEFAULT_ARGS = [ + "--generate-hashes", + "--emit-index-url", + "--no-strip-extras", +] + def _python_version(py_toolchain): return "{major}.{minor}".format( major = py_toolchain.py3_runtime.interpreter_version_info.major, minor = py_toolchain.py3_runtime.interpreter_version_info.minor, ) -def _python_platform(maybe_python_platform): - if maybe_python_platform == "": - return "" - return "--python-platform {python_platform}".format(python_platform = maybe_python_platform) - -def _uv_pip_compile(ctx, template, executable, generator_label): +def _uv_pip_compile( + ctx, + template, + executable, + generator_label, + args): py_toolchain = ctx.toolchains[_PY_TOOLCHAIN] + compile_command = "bazel run {label}".format(label = str(generator_label)) + + cmd_args = [] + cmd_args += args + cmd_args.append("--custom-compile-command='{compile_command}'".format(compile_command = compile_command)) + cmd_args.append("--python-version={version}".format(version = _python_version(py_toolchain))) + if ctx.attr.python_platform: + cmd_args.append("--python-platform={platform}".format(platform = ctx.attr.python_platform)) + ctx.actions.expand_template( template = template, output = executable, substitutions = { "{{uv}}": ctx.executable._uv.short_path, + "{{args}}": " \\\n ".join(cmd_args), "{{requirements_in}}": ctx.file.requirements_in.short_path, "{{requirements_txt}}": ctx.file.requirements_txt.short_path, + "{{compile_command}}": compile_command, "{{resolved_python}}": py_toolchain.py3_runtime.interpreter.short_path, - "{{python_version}}": _python_version(py_toolchain), - "{{python_platform}}": _python_platform(ctx.attr.python_platform), - "{{label}}": str(generator_label), }, ) @@ -47,14 +61,20 @@ def _runfiles(ctx): def _pip_compile_impl(ctx): executable = ctx.actions.declare_file(ctx.attr.name) - _uv_pip_compile(ctx, ctx.file._template, executable, ctx.label) + _uv_pip_compile( + ctx = ctx, + template = ctx.file._template, + executable = executable, + generator_label = ctx.label, + args = ctx.attr.args, + ) return DefaultInfo( executable = executable, runfiles = _runfiles(ctx), ) _pip_compile = rule( - attrs = _common_attrs | { + attrs = _COMMON_ATTRS | { "_template": attr.label(default = "//uv/private:pip_compile.sh", allow_single_file = True), }, toolchains = [_PY_TOOLCHAIN], @@ -64,14 +84,20 @@ _pip_compile = rule( def _pip_compile_test_impl(ctx): executable = ctx.actions.declare_file(ctx.attr.name) - _uv_pip_compile(ctx, ctx.file._template, executable, ctx.attr.generator_label.label) + _uv_pip_compile( + ctx = ctx, + template = ctx.file._template, + executable = executable, + generator_label = ctx.attr.generator_label.label, + args = ctx.attr.args, + ) return DefaultInfo( executable = executable, runfiles = _runfiles(ctx), ) _pip_compile_test = rule( - attrs = _common_attrs | { + attrs = _COMMON_ATTRS | { "generator_label": attr.label(mandatory = True), "_template": attr.label(default = "//uv/private:pip_compile_test.sh", allow_single_file = True), }, @@ -80,15 +106,44 @@ _pip_compile_test = rule( test = True, ) -def pip_compile(name, requirements_in = None, requirements_txt = None, target_compatible_with = None, python_platform = None, tags = None): +def pip_compile( + name, + requirements_in = None, + requirements_txt = None, + target_compatible_with = None, + python_platform = None, + args = None, + tags = None): + """ + Produce targets to compile a requirements.in or pyproject.toml file into a requirements.txt file. + + Args: + name: name of the primary compilation target. + requirements_in: (optional, default "//:requirements.in") a label for the requirements.in file. + requirements_txt: (optional, default "//:requirements.txt") a label for the requirements.txt file. + python_platform: (optional) a uv pip compile compatible value for --python-platform. + target_compatible_with: (optional) specify that a particular target is compatible only with certain + Bazel platforms. + args: (optional) override the default arguments passed to uv pip compile, default arguments are: + --generate-hashes (Include distribution hashes in the output file) + --emit-index-url (Include `--index-url` and `--extra-index-url` entries in the generated output file) + --no-strip-extras (Include extras in the output file) + tags: (optional) tags to apply to the generated test target + + Targets produced by this macro are: + [name]: a runnable target that will use requirements_in to generate and overwrite requirements_txt + [name]_diff_test: a testable target that will check that requirements_txt is up to date with requirements_in + """ tags = tags or [] + args = args or _DEFAULT_ARGS _pip_compile( name = name, requirements_in = requirements_in or "//:requirements.in", requirements_txt = requirements_txt or "//:requirements.txt", - python_platform = python_platform or "", + python_platform = python_platform, target_compatible_with = target_compatible_with, + args = args, ) _pip_compile_test( @@ -98,5 +153,6 @@ def pip_compile(name, requirements_in = None, requirements_txt = None, target_co requirements_txt = requirements_txt or "//:requirements.txt", python_platform = python_platform or "", target_compatible_with = target_compatible_with, + args = args, tags = ["requires-network"] + tags, ) diff --git a/uv/private/pip_compile.sh b/uv/private/pip_compile.sh index 8eeeb1b..381ec21 100644 --- a/uv/private/pip_compile.sh +++ b/uv/private/pip_compile.sh @@ -2,26 +2,16 @@ set -euo pipefail -UV="{{uv}}" -PYTHON_PLATFORM="{{python_platform}}" -RESOLVED_PYTHON="{{resolved_python}}" -PYTHON_VERSION="{{python_version}}" +# inputs from Bazel REQUIREMENTS_IN="{{requirements_in}}" REQUIREMENTS_TXT="{{requirements_txt}}" -LABEL="{{label}}" - -RESOLVED_PYTHON_BIN="$(dirname "$RESOLVED_PYTHON")" # set resolved python to front of the path +RESOLVED_PYTHON_BIN="$(dirname "{{resolved_python}}")" export PATH="$RESOLVED_PYTHON_BIN:$PATH" -$UV pip compile \ - --generate-hashes \ - --emit-index-url \ - --no-strip-extras \ - --custom-compile-command "bazel run $LABEL" \ - --python-version="$PYTHON_VERSION" \ - $(echo $PYTHON_PLATFORM) \ - -o "$REQUIREMENTS_TXT" \ +{{uv}} pip compile \ + {{args}} \ + --output-file="$REQUIREMENTS_TXT" \ "$REQUIREMENTS_IN" \ "$@" diff --git a/uv/private/pip_compile_test.sh b/uv/private/pip_compile_test.sh index 3c3e8c9..bd31ee6 100644 --- a/uv/private/pip_compile_test.sh +++ b/uv/private/pip_compile_test.sh @@ -2,39 +2,30 @@ set -euo pipefail -UV="{{uv}}" -PYTHON_PLATFORM="{{python_platform}}" -RESOLVED_PYTHON="{{resolved_python}}" -PYTHON_VERSION="{{python_version}}" +# inputs from Bazel REQUIREMENTS_IN="{{requirements_in}}" REQUIREMENTS_TXT="{{requirements_txt}}" -LABEL="{{label}}" - -RESOLVED_PYTHON_BIN="$(dirname "$RESOLVED_PYTHON")" +COMPILE_COMMAND="{{compile_command}}" # set resolved python to front of the path +RESOLVED_PYTHON_BIN="$(dirname "{{resolved_python}}")" export PATH="$RESOLVED_PYTHON_BIN:$PATH" # make a writable copy of incoming requirements cp "$REQUIREMENTS_TXT" __updated__ - -$UV pip compile \ + +{{uv}} pip compile \ --quiet \ --no-cache \ - --generate-hashes \ - --emit-index-url \ - --no-strip-extras \ - --custom-compile-command "bazel run ${LABEL}" \ - --python-version="$PYTHON_VERSION" \ - $(echo $PYTHON_PLATFORM) \ - -o __updated__ \ + {{args}} \ + --output-file="__updated__" \ "$REQUIREMENTS_IN" # check files match DIFF="$(diff "$REQUIREMENTS_TXT" "__updated__" || true)" if [ "$DIFF" != "" ] then - echo >&2 "FAIL: $REQUIREMENTS_TXT is out-of-date. Run 'bazel run $LABEL' to update." + echo >&2 "FAIL: $REQUIREMENTS_TXT is out-of-date. Run '$compile_command' to update." echo >&2 "$DIFF" exit 1 fi