From 7a37d5dc1f5cd977943bf434bf09f3d77fcc6217 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 14 Mar 2025 00:20:27 +0900 Subject: [PATCH 01/15] feat: uv lock rule instead of genrule This change implements the uv pip compile as a rule. In order to also make things easier to debug we provide a runnable rule that has the same arguments and updates the source tree output file automatically. The main design is to have a regular lock rule and then it returns a custom provider that has all of the recipe ingredients to construct an executable rule. The execution depends on having bash or powershell, however the powershell script is not yet complete and requires some help from the community. Work towards #1975. Address all of the comments --- docs/BUILD.bazel | 8 +- examples/BUILD.bazel | 3 + examples/bzlmod/requirements_lock_3_9.txt | 17 +- private/BUILD.bazel | 2 + python/private/BUILD.bazel | 1 + python/private/py_exec_tools_toolchain.bzl | 73 ++- python/uv/lock.bzl | 26 + python/uv/private/BUILD.bazel | 25 +- python/uv/private/lock.bat | 7 + python/uv/private/lock.bzl | 528 +++++++++++++++--- python/uv/private/lock.sh | 9 + python/uv/private/lock_copier.py | 69 +++ python/uv/private/uv_toolchain.bzl | 2 +- tests/support/remote-toolchains/BUILD.bazel | 23 + tests/uv/lock/BUILD.bazel | 5 + tests/uv/lock/lock_run_test.py | 165 ++++++ tests/uv/lock/lock_tests.bzl | 99 ++++ tests/uv/lock/testdata/build_constraints.txt | 1 + tests/uv/lock/testdata/build_constraints2.txt | 1 + tests/uv/lock/testdata/constraints.txt | 1 + tests/uv/lock/testdata/constraints2.txt | 1 + tests/uv/lock/testdata/requirements.in | 1 + tests/uv/lock/testdata/requirements.txt | 128 +++++ tools/private/publish_deps.bzl | 22 +- tools/publish/BUILD.bazel | 5 +- 25 files changed, 1116 insertions(+), 106 deletions(-) create mode 100755 python/uv/private/lock.bat create mode 100755 python/uv/private/lock.sh create mode 100644 python/uv/private/lock_copier.py create mode 100644 tests/support/remote-toolchains/BUILD.bazel create mode 100644 tests/uv/lock/BUILD.bazel create mode 100644 tests/uv/lock/lock_run_test.py create mode 100644 tests/uv/lock/lock_tests.bzl create mode 100644 tests/uv/lock/testdata/build_constraints.txt create mode 100644 tests/uv/lock/testdata/build_constraints2.txt create mode 100644 tests/uv/lock/testdata/constraints.txt create mode 100644 tests/uv/lock/testdata/constraints2.txt create mode 100644 tests/uv/lock/testdata/requirements.in create mode 100644 tests/uv/lock/testdata/requirements.txt diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel index ab996537c7..bebecd18b2 100644 --- a/docs/BUILD.bazel +++ b/docs/BUILD.bazel @@ -176,8 +176,12 @@ lock( name = "requirements", srcs = ["pyproject.toml"], out = "requirements.txt", - upgrade = True, - visibility = ["//private:__pkg__"], + args = [ + "--emit-index-url", + "--universal", + "--upgrade", + ], + visibility = ["//:__subpackages__"], ) # Temporary compatibility aliases for some other projects depending on the old diff --git a/examples/BUILD.bazel b/examples/BUILD.bazel index 92ca8e7199..0ef5211877 100644 --- a/examples/BUILD.bazel +++ b/examples/BUILD.bazel @@ -21,5 +21,8 @@ lock( name = "bzlmod_requirements_3_9", srcs = ["bzlmod/requirements.in"], out = "bzlmod/requirements_lock_3_9.txt", + args = [ + "--emit-index-url", + ], python_version = "3.9.19", ) diff --git a/examples/bzlmod/requirements_lock_3_9.txt b/examples/bzlmod/requirements_lock_3_9.txt index d74d1d39b6..ba1f85c438 100644 --- a/examples/bzlmod/requirements_lock_3_9.txt +++ b/examples/bzlmod/requirements_lock_3_9.txt @@ -26,10 +26,7 @@ chardet==4.0.0 \ colorama==0.4.6 \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 - # via - # -r examples/bzlmod/requirements.in - # pylint - # sphinx + # via -r examples/bzlmod/requirements.in dill==0.3.6 \ --hash=sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0 \ --hash=sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373 @@ -46,7 +43,7 @@ imagesize==1.4.1 \ --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \ --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a # via sphinx -importlib-metadata==8.4.0 ; python_version < '3.10' \ +importlib-metadata==8.4.0 \ --hash=sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1 \ --hash=sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5 # via sphinx @@ -265,9 +262,7 @@ s3cmd==2.1.0 \ setuptools==65.6.3 \ --hash=sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54 \ --hash=sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75 - # via - # babel - # yamllint + # via yamllint six==1.16.0 \ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 @@ -316,7 +311,7 @@ tabulate==0.9.0 \ --hash=sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c \ --hash=sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f # via -r examples/bzlmod/requirements.in -tomli==2.0.1 ; python_version < '3.11' \ +tomli==2.0.1 \ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f # via pylint @@ -324,7 +319,7 @@ tomlkit==0.11.6 \ --hash=sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b \ --hash=sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73 # via pylint -typing-extensions==4.12.2 ; python_version < '3.10' \ +typing-extensions==4.12.2 \ --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \ --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8 # via @@ -480,7 +475,7 @@ yamllint==1.28.0 \ --hash=sha256:89bb5b5ac33b1ade059743cf227de73daa34d5e5a474b06a5e17fc16583b0cf2 \ --hash=sha256:9e3d8ddd16d0583214c5fdffe806c9344086721f107435f68bad990e5a88826b # via -r examples/bzlmod/requirements.in -zipp==3.20.0 ; python_version < '3.10' \ +zipp==3.20.0 \ --hash=sha256:0145e43d89664cfe1a2e533adc75adafed82fe2da404b4bbb6b026c0157bdb31 \ --hash=sha256:58da6168be89f0be59beb194da1250516fdaa062ccebd30127ac65d30045e10d # via importlib-metadata diff --git a/private/BUILD.bazel b/private/BUILD.bazel index 68fefe910f..ef5652b826 100644 --- a/private/BUILD.bazel +++ b/private/BUILD.bazel @@ -15,6 +15,7 @@ multirun( ] + [ "//docs:requirements.update", ], + tags = ["manual"], ) # NOTE: The requirements for the pip dependencies may sometimes break the build @@ -24,4 +25,5 @@ multirun( alias( name = "whl_library_requirements.update", actual = "//tools/private/update_deps:update_pip_deps", + tags = ["manual"], ) diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel index 8b07fbd877..0f6668fa93 100644 --- a/python/private/BUILD.bazel +++ b/python/private/BUILD.bazel @@ -361,6 +361,7 @@ bzl_library( name = "py_exec_tools_toolchain_bzl", srcs = ["py_exec_tools_toolchain.bzl"], deps = [ + ":common_bzl", ":py_exec_tools_info_bzl", ":sentinel_bzl", ":toolchain_types_bzl", diff --git a/python/private/py_exec_tools_toolchain.bzl b/python/private/py_exec_tools_toolchain.bzl index edf9159759..2745ddfb5b 100644 --- a/python/private/py_exec_tools_toolchain.bzl +++ b/python/private/py_exec_tools_toolchain.bzl @@ -16,6 +16,7 @@ load("@bazel_skylib//lib:paths.bzl", "paths") load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") +load(":common.bzl", "runfiles_root_path") load(":py_exec_tools_info.bzl", "PyExecToolsInfo") load(":sentinel.bzl", "SentinelInfo") load(":toolchain_types.bzl", "TARGET_TOOLCHAIN_TYPE") @@ -82,24 +83,86 @@ See {obj}`PyExecToolsInfo.exec_interpreter` for further docs. }, ) +def relative_path(from_, to): + """Compute a relative path from one path to another. + + Args: + from_: {type}`str` the starting directory. Note that it should be + a directory because relative-symlinks are relative to the + directory the symlink resides in. + to: {type}`str` the path that `from_` wants to point to + + Returns: + {type}`str` a relative path + """ + from_parts = from_.split("/") + to_parts = to.split("/") + + # Strip common leading parts from both paths + n = min(len(from_parts), len(to_parts)) + for _ in range(n): + if from_parts[0] == to_parts[0]: + from_parts.pop(0) + to_parts.pop(0) + else: + break + + # Impossible to compute a relative path without knowing what ".." is + if from_parts and from_parts[0] == "..": + fail("cannot compute relative path from '%s' to '%s'", from_, to) + + parts = ([".."] * len(from_parts)) + to_parts + return paths.join(*parts) + def _current_interpreter_executable_impl(ctx): toolchain = ctx.toolchains[TARGET_TOOLCHAIN_TYPE] runtime = toolchain.py3_runtime + direct = [] # NOTE: We name the output filename after the underlying file name # because of things like pyenv: they use $0 to determine what to # re-exec. If it's not a recognized name, then they fail. if runtime.interpreter: - executable = ctx.actions.declare_file(runtime.interpreter.basename) - ctx.actions.symlink(output = executable, target_file = runtime.interpreter, is_executable = True) + # Even though ctx.actions.symlink() could be used, we bump into the issue + # with RBE where bazel is making a copy to the file instead of symlinking + # to the hermetic toolchain repository. This means that we need to employ + # a similar strategy to how the `py_executable` venv is created where the + # file in the `runfiles` is a dangling symlink into the hermetic toolchain + # repository. This smells like a bug in RBE, but I would not be surprised + # if it is not one. + + # Create a dangling symlink in `bin/python3` to the real interpreter + # in the hermetic toolchain. + interpreter_basename = runtime.interpreter.basename + executable = ctx.actions.declare_symlink("bin/" + interpreter_basename) + direct.append(executable) + interpreter_actual_path = runfiles_root_path(ctx, runtime.interpreter.short_path) + target_path = relative_path( + # dirname is necessary because a relative symlink is relative to + # the directory the symlink resides within. + from_ = paths.dirname(runfiles_root_path(ctx, executable.short_path)), + to = interpreter_actual_path, + ) + ctx.actions.symlink(output = executable, target_path = target_path) + + # Create a dangling symlink into the runfiles and use that as the + # entry point. + interpreter_actual_path = runfiles_root_path(ctx, executable.short_path) + executable = ctx.actions.declare_symlink(interpreter_basename) + target_path = interpreter_basename + ".runfiles/" + interpreter_actual_path + ctx.actions.symlink(output = executable, target_path = target_path) else: - executable = ctx.actions.declare_symlink(paths.basename(runtime.interpreter_path)) - ctx.actions.symlink(output = executable, target_path = runtime.interpreter_path) + interpreter_basename = paths.basename(runtime.interpreter.interpreter_path) + executable = ctx.actions.declare_symlink(interpreter_basename) + direct.append(executable) + target_path = runtime.interpreter_path + ctx.actions.symlink(output = executable, target_path = target_path) + return [ toolchain, DefaultInfo( executable = executable, - runfiles = ctx.runfiles([executable], transitive_files = runtime.files), + runfiles = ctx.runfiles(direct, transitive_files = runtime.files), ), ] diff --git a/python/uv/lock.bzl b/python/uv/lock.bzl index edffe4728c..68a63740da 100644 --- a/python/uv/lock.bzl +++ b/python/uv/lock.bzl @@ -14,6 +14,32 @@ """The `uv` locking rule. +Differences with the legacy {obj}`compile_pip_requirements` rule: +- This is implemented as a rule that performs locking in a build action. +- Additionally one can use the runnable target. +- Uses `uv`. +- This does not error out if the output file does not exist yet. +- Supports transitions out of the box. + +Note, this does not provide a `test` target, if you would like to add a test +target that always does the locking automatically to ensure that the +`requirements.txt` file is up-to-date, add something similar to: + +```starlark +load("@bazel_skylib//rules:native_binary.bzl", "native_test") +load("@rules_python//python/uv:lock.bzl", "lock") + +lock( + name = "requirements", + srcs = ["pyproject.toml"], +) + +native_test( + name = "requirements_test", + src = "requirements.update", +) +``` + EXPERIMENTAL: This is experimental and may be removed without notice """ diff --git a/python/uv/private/BUILD.bazel b/python/uv/private/BUILD.bazel index acf2a9c1f7..d17ca39490 100644 --- a/python/uv/private/BUILD.bazel +++ b/python/uv/private/BUILD.bazel @@ -13,6 +13,15 @@ # limitations under the License. load("@bazel_skylib//:bzl_library.bzl", "bzl_library") +load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility + +exports_files( + srcs = [ + "lock_copier.py", + ], + # only because this is used from a macro to template + visibility = ["//visibility:public"], +) filegroup( name = "distribution", @@ -31,9 +40,13 @@ bzl_library( srcs = ["lock.bzl"], visibility = ["//python/uv:__subpackages__"], deps = [ + ":toolchain_types_bzl", "//python:py_binary_bzl", "//python/private:bzlmod_enabled_bzl", - "@bazel_skylib//rules:write_file", + "//python/private:full_version_bzl", + "//python/private:toolchain_types_bzl", + "@bazel_skylib//lib:shell", + "@pythons_hub//:versions_bzl", ], ) @@ -81,3 +94,13 @@ bzl_library( "//python/private:text_util_bzl", ], ) + +filegroup( + name = "lock_template", + srcs = select({ + "@platforms//os:windows": ["lock.bat"], + "//conditions:default": ["lock.sh"], + }), + target_compatible_with = [] if BZLMOD_ENABLED else ["@platforms//:incompatible"], + visibility = ["//visibility:public"], +) diff --git a/python/uv/private/lock.bat b/python/uv/private/lock.bat new file mode 100755 index 0000000000..3954c10347 --- /dev/null +++ b/python/uv/private/lock.bat @@ -0,0 +1,7 @@ +if defined BUILD_WORKSPACE_DIRECTORY ( + set "out=%BUILD_WORKSPACE_DIRECTORY%\{{src_out}}" +) else ( + exit /b 1 +) + +"{{args}}" --output-file "%out%" %* diff --git a/python/uv/private/lock.bzl b/python/uv/private/lock.bzl index 9378f180db..c78c5a5399 100644 --- a/python/uv/private/lock.bzl +++ b/python/uv/private/lock.bzl @@ -12,114 +12,480 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""A simple macro to lock the requirements. +"""An implementation for a simple macro to lock the requirements. """ -load("@bazel_skylib//rules:write_file.bzl", "write_file") +load("@bazel_skylib//lib:shell.bzl", "shell") +load("@pythons_hub//:versions.bzl", "DEFAULT_PYTHON_VERSION", "MINOR_MAPPING") load("//python:py_binary.bzl", "py_binary") load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility +load("//python/private:full_version.bzl", "full_version") +load("//python/private:toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE") # buildifier: disable=bzl-visibility +load(":toolchain_types.bzl", "UV_TOOLCHAIN_TYPE") visibility(["//..."]) -_REQUIREMENTS_TARGET_COMPATIBLE_WITH = select({ - "@platforms//os:windows": ["@platforms//:incompatible"], - "//conditions:default": [], -}) if BZLMOD_ENABLED else ["@platforms//:incompatible"] +_PYTHON_VERSION_FLAG = "//python/config_settings:python_version" -def lock(*, name, srcs, out, upgrade = False, universal = True, args = [], **kwargs): - """Pin the requirements based on the src files. +_RunLockInfo = provider( + doc = "", + fields = { + "args": "The args passed to the `uv` by default when running the runnable target.", + "env": "The env passed to the execution.", + "srcs": "Source files required to run the runnable target.", + }, +) + +def _args(ctx): + """A small helper to ensure that the right args are pushed to the _RunLockInfo provider""" + run_info = [] + args = ctx.actions.args() + + def _add_args(arg, maybe_value = None): + run_info.append(arg) + if maybe_value: + args.add(arg, maybe_value) + run_info.append(maybe_value) + else: + args.add(arg) + + def _add_all(name, all_args = None, **kwargs): + if not all_args and type(name) == "list": + all_args = name + name = None + + before_each = kwargs.get("before_each") + if name: + args.add_all(name, all_args, **kwargs) + run_info.append(name) + else: + args.add_all(all_args, **kwargs) + + for arg in all_args: + if before_each: + run_info.append(before_each) + run_info.append(arg) + + return struct( + run_info = run_info, + run_shell = args, + add = _add_args, + add_all = _add_all, + ) + +def _lock_impl(ctx): + srcs = ctx.files.srcs + python_version = full_version( + version = ctx.attr.python_version or DEFAULT_PYTHON_VERSION, + minor_mapping = MINOR_MAPPING, + ) + output = ctx.actions.declare_file("{}.{}.out".format( + ctx.label.name, + python_version.replace(".", "_"), + )) + + toolchain_info = ctx.toolchains[UV_TOOLCHAIN_TYPE] + uv = toolchain_info.uv_toolchain_info.uv[DefaultInfo].files_to_run.executable + + interpreter = ctx.toolchains[EXEC_TOOLS_TOOLCHAIN_TYPE].exec_tools.exec_interpreter[DefaultInfo] + + args = _args(ctx) + args.add_all([ + uv, + "pip", + "compile", + "--no-python-downloads", + "--no-cache", + ]) + pkg = ctx.label.package + update_target = ctx.attr.update_target + args.add("--custom-compile-command", "bazel run //{}:{}".format(pkg, update_target)) + if ctx.attr.generate_hashes: + args.add("--generate-hashes") + if not ctx.attr.strip_extras: + args.add("--no-strip-extras") + args.add_all(ctx.files.build_constraints, before_each = "--build-constraints") + args.add_all(ctx.files.constraints, before_each = "--constraints") + args.add_all(ctx.attr.args) + + python = interpreter.files_to_run.executable or "python" + args.add("--python", python) + args.add_all(srcs) + + args.run_shell.add("--output-file", output) + + # These arguments does not change behaviour, but it reduces the output from + # the command, which is especially verbose in stderr. + args.run_shell.add("--no-progress") + args.run_shell.add("--quiet") + + if ctx.files.existing_output: + command = '{python} -c {python_cmd} && "$@"'.format( + python = getattr(python, "path", python), + python_cmd = shell.quote( + "from shutil import copy; copy(\"{src}\", \"{dst}\")".format( + src = ctx.files.existing_output[0].path, + dst = output.path, + ), + ), + ) + else: + command = '"$@"' + + srcs = srcs + ctx.files.build_constraints + ctx.files.constraints + + ctx.actions.run_shell( + command = command, + inputs = srcs + ctx.files.existing_output, + mnemonic = "PyRequirementsLockUv", + outputs = [output], + arguments = [args.run_shell], + tools = [ + uv, + interpreter.files_to_run, + ], + progress_message = "Creating a requirements.txt with uv: //{}:{}".format( + ctx.label.package, + ctx.label.name, + ), + env = ctx.attr.env, + ) + + return [ + DefaultInfo(files = depset([output])), + _RunLockInfo( + args = args.run_info, + env = ctx.attr.env, + srcs = depset( + srcs + [uv], + transitive = [interpreter.files], + ), + ), + ] + +def _transition_impl(input_settings, attr): + settings = { + _PYTHON_VERSION_FLAG: input_settings[_PYTHON_VERSION_FLAG], + } + if attr.python_version: + # FIXME @aignas 2025-03-20: using `full_version` is a workaround for a bug in + # how we order toolchains in bazel. If I set the `python_version` flag + # to `3.12`, I would expect the latest version to be selected, i.e. the + # one that is in MINOR_MAPPING, but it seems that 3.12.0 is selected, + # because of how the targets are ordered. + settings[_PYTHON_VERSION_FLAG] = full_version( + version = attr.python_version, + minor_mapping = MINOR_MAPPING, + ) + return settings + +_python_version_transition = transition( + implementation = _transition_impl, + inputs = [_PYTHON_VERSION_FLAG], + outputs = [_PYTHON_VERSION_FLAG], +) + +_lock = rule( + implementation = _lock_impl, + doc = """\ +""", + attrs = { + "args": attr.string_list( + doc = "Public, see the docs in the macro.", + ), + "build_constraints": attr.label_list( + allow_files = True, + doc = "Public, see the docs in the macro.", + ), + "constraints": attr.label_list( + allow_files = True, + doc = "Public, see the docs in the macro.", + ), + "env": attr.string_dict( + doc = "Public, see the docs in the macro.", + ), + "existing_output": attr.label( + mandatory = False, + allow_single_file = True, + doc = """\ +An already existing output file that is used as a basis for further +modifications and the locking is not done from scratch. +""", + ), + "generate_hashes": attr.bool( + doc = "Public, see the docs in the macro.", + default = True, + ), + "output": attr.string( + doc = "Public, see the docs in the macro.", + mandatory = True, + ), + "python_version": attr.string( + doc = "Public, see the docs in the macro.", + ), + "srcs": attr.label_list( + mandatory = True, + allow_files = True, + doc = "Public, see the docs in the macro.", + ), + "strip_extras": attr.bool( + doc = "Public, see the docs in the macro.", + default = False, + ), + "update_target": attr.string( + mandatory = True, + doc = """\ +The string to input for the 'uv pip compile'. +""", + ), + "_allowlist_function_transition": attr.label( + default = "@bazel_tools//tools/allowlists/function_transition_allowlist", + ), + }, + toolchains = [ + EXEC_TOOLS_TOOLCHAIN_TYPE, + UV_TOOLCHAIN_TYPE, + ], + cfg = _python_version_transition, +) + +def _lock_run_impl(ctx): + if ctx.attr.is_windows: + path_sep = "\\" + ext = ".exe" + else: + path_sep = "/" + ext = "" + + def _maybe_path(arg): + if hasattr(arg, "short_path"): + arg = arg.short_path + + return shell.quote(arg.replace("/", path_sep)) - Differences with the current {obj}`compile_pip_requirements` rule: - - This is implemented in shell and `uv`. - - This does not error out if the output file does not exist yet. - - Supports transitions out of the box. - - The execution of the lock file generation is happening inside of a build - action in a `genrule`. + info = ctx.attr.lock[_RunLockInfo] + executable = ctx.actions.declare_file(ctx.label.name + ext) + ctx.actions.expand_template( + template = ctx.files._template[0], + substitutions = { + '"{{args}}"': " ".join([_maybe_path(arg) for arg in info.args]), + "{{src_out}}": "{}/{}".format(ctx.label.package, ctx.attr.output).replace( + "/", + path_sep, + ), + }, + output = executable, + is_executable = True, + ) + + return [ + DefaultInfo( + executable = executable, + runfiles = ctx.runfiles(transitive_files = info.srcs), + ), + RunEnvironmentInfo( + environment = info.env, + ), + ] + +_lock_run = rule( + implementation = _lock_run_impl, + doc = """\ +""", + attrs = { + "is_windows": attr.bool(mandatory = True), + "lock": attr.label( + doc = "The lock target that is doing locking in a build action.", + providers = [_RunLockInfo], + cfg = "exec", + ), + "output": attr.string( + doc = """\ +The output that we would be updated, relative to the package the macro is used in. +""", + ), + "_template": attr.label( + default = "//python/uv/private:lock_template", + doc = """\ +The template to be used for 'uv pip compile'. This is either .ps1 or bash +script depending on what the target platform is executed on. +""", + ), + }, + executable = True, +) + +def _maybe_file(path): + """A small function to return a list of existing outputs. + + If the file referenced by the input argument exists, then it will return + it, otherwise it will return an empty list. This is useful to for programs + like pip-compile which behave differently if the output file exists and + update the output file in place. + + The API of the function ensures that path is not a glob itself. Args: - name: The name of the target to run for updating the requirements. - srcs: The srcs to use as inputs. - out: The output file. - upgrade: Tell `uv` to always upgrade the dependencies instead of - keeping them as they are. - universal: Tell `uv` to generate a universal lock file. - args: Extra args to pass to the rule. - **kwargs: Extra kwargs passed to the binary rule. + path: {type}`str` the file name. """ - pkg = native.package_name() - update_target = name + ".update" - - _args = [ - "--custom-compile-command='bazel run //{}:{}'".format(pkg, update_target), - "--generate-hashes", - "--emit-index-url", - "--no-strip-extras", - "--python=$(PYTHON3)", - ] + args + [ - "$(location {})".format(src) - for src in srcs - ] - if upgrade: - _args.append("--upgrade") - if universal: - _args.append("--universal") - _args.append("--output-file=$@") - cmd = "$(UV_BIN) pip compile " + " ".join(_args) + for p in native.glob([path], allow_empty = True): + if path == p: + return p + + return None - # Make a copy to ensure that we are not modifying the initial list - srcs = list(srcs) +def _expand_template_impl(ctx): + pkg = ctx.label.package + update_src = ctx.actions.declare_file(ctx.attr.update_target + ".py") + ctx.actions.expand_template( + template = ctx.files._template[0], + substitutions = { + "{{dst}}": "{}/{}".format(pkg, ctx.attr.output), + "{{src}}": "{}".format(ctx.files.src[0].short_path), + "{{update_target}}": "//{}:{}".format(pkg, ctx.attr.update_target), + }, + output = update_src, + ) + return DefaultInfo(files = depset([update_src])) + +_expand_template = rule( + implementation = _expand_template_impl, + attrs = { + "output": attr.string(mandatory = True), + "src": attr.label(mandatory = True), + "update_target": attr.string(mandatory = True), + "_template": attr.label( + default = "//python/uv/private:lock_copier.py", + allow_single_file = True, + ), + }, + doc = "Expand the template for the update script allowing us to use `select` statements in the {attr}`output` attribute.", +) + +def lock( + *, + name, + srcs, + out, + args = [], + build_constraints = [], + constraints = [], + env = None, + generate_hashes = True, + python_version = None, + strip_extras = False, + **kwargs): + """Pin the requirements based on the src files. + + This macro creates the following targets: + - `name`: the target that creates the requirements.txt file in a build + action. This target will have `no-cache` and `requires-network` added + to its tags. + - `name.run`: a runnable target that can be used to pass extra parameters + to the same command that would be run in the `name` action. This will + update the source copy of the requirements file. You can customize the + args via the command line, but it requires being able to run `uv` (and + possibly `python`) directly on your host. + - `name.update`: a target that can be run to update the source-tree version + of the requirements lock file. The output can be fed to the + {obj}`pip.parse` bzlmod extension tag class. Note, you can use + `native_test` to wrap this target to make a test. You can't customize the + args via command line, but you can use RBE to generate requirements + (offload execution and run for different platforms) + + :::{note} + All of the targets have `manual` tags as locking results cannot be cached. + ::: + + Args: + name: {type}`str` The prefix of all targets created by this macro. + srcs: {type}`list[Label]` The sources that will be used. Add all of the + files that would be passed as srcs to the `uv pip compile` command. + out: {type}`str` The output file relative to the package. + args: {type}`list[str]` The list of args to pass to uv. Note, these are + written into the runnable `name.run` target. + env: {type}`dict[str, str]` the environment variables to set. Note, this + is passed as is and the environment variables are not expanded. + build_constraints: {type}`list[Label]` The list of build constraints to use. + constraints: {type}`list[Label]` The list of constraints files to use. + generate_hashes: {type}`bool` Generate hashes for all of the + requirements. This is a must if you want to use + {attr}`pip.parse.experimental_index_url`. Defaults to `True`. + strip_extras: {type}`bool` whether to strip extras from the output. + Currently `rules_python` requires `--no-strip-extras` to properly + function, but sometimes one may want to not have the extras if you + are compiling the requirements file for using it as a constraints + file. Defaults to `False`. + python_version: {type}`str | None` the python_version to transition to + when locking the requirements. Defaults to the default python version + configured by the {obj}`python` module extension. + **kwargs: common kwargs passed to rules. + """ + update_target = "{}.update".format(name) + locker_target = "{}.run".format(name) # Check if the output file already exists, if yes, first copy it to the # output file location in order to make `uv` not change the requirements if # we are just running the command. - if native.glob([out]): - cmd = "cp -v $(location {}) $@; {}".format(out, cmd) - srcs.append(out) + maybe_out = _maybe_file(out) + + tags = ["manual"] + kwargs.pop("tags", []) + if not BZLMOD_ENABLED: + kwargs["target_compatible_with"] = ["@platforms//:incompatible"] - native.genrule( + # FIXME @aignas 2025-03-17: should we have one more target that transitions + # the python_version to ensure that if somebody calls `bazel build + # :requirements` that it is locked with the right `python_version`? + _lock( name = name, + args = args, + build_constraints = build_constraints, + constraints = constraints, + env = env, + existing_output = maybe_out, + generate_hashes = generate_hashes, + python_version = python_version, srcs = srcs, - outs = [out + ".new"], - cmd_bash = cmd, + strip_extras = strip_extras, + update_target = update_target, + output = out, tags = [ - "local", - "manual", "no-cache", - ], - target_compatible_with = _REQUIREMENTS_TARGET_COMPATIBLE_WITH, - toolchains = [ - Label("//python/uv:current_toolchain"), - Label("//python:current_py_toolchain"), - ], + "requires-network", + ] + tags, + **kwargs ) - # Write a script that can be used for updating the in-tree version of the - # requirements file - write_file( - name = name + ".update_gen", - out = update_target + ".py", - content = [ - "from os import environ", - "from pathlib import Path", - "from sys import stderr", - "", - 'src = Path(environ["REQUIREMENTS_FILE"])', - 'assert src.exists(), f"the {src} file does not exist"', - 'dst = Path(environ["BUILD_WORKSPACE_DIRECTORY"]) / "{}" / "{}"'.format(pkg, out), - 'print(f"Writing requirements contents\\n from {src.absolute()}\\n to {dst.absolute()}", file=stderr)', - "dst.write_text(src.read_text())", - 'print("Success!", file=stderr)', - ], + # A target for updating the in-tree version directly by skipping the in-action + # uv pip compile. + _lock_run( + name = locker_target, + lock = name, + output = out, + is_windows = select({ + "@platforms//os:windows": True, + "//conditions:default": False, + }), + tags = tags, + **kwargs + ) + + # FIXME @aignas 2025-03-20: is it possible to extend `py_binary` so that the + # srcs are generated before `py_binary` is run? I found that + # `ctx.files.srcs` usage in the base implementation is making it difficult. + template_target = "_{}_gen".format(name) + _expand_template( + name = template_target, + src = name, + output = out, + update_target = update_target, + tags = tags, ) py_binary( name = update_target, - srcs = [update_target + ".py"], - main = update_target + ".py", - data = [name], - env = { - "REQUIREMENTS_FILE": "$(rootpath {})".format(name), - }, - tags = ["manual"], + srcs = [template_target], + data = [name] + ([maybe_out] if maybe_out else []), + tags = tags, **kwargs ) diff --git a/python/uv/private/lock.sh b/python/uv/private/lock.sh new file mode 100755 index 0000000000..b6ba0c6c48 --- /dev/null +++ b/python/uv/private/lock.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -euo pipefail + +if [[ -n "${BUILD_WORKSPACE_DIRECTORY:-}" ]]; then + readonly out="${BUILD_WORKSPACE_DIRECTORY}/{{src_out}}" +else + exit 1 +fi +exec "{{args}}" --output-file "$out" "$@" diff --git a/python/uv/private/lock_copier.py b/python/uv/private/lock_copier.py new file mode 100644 index 0000000000..bcc64c1661 --- /dev/null +++ b/python/uv/private/lock_copier.py @@ -0,0 +1,69 @@ +import sys +from difflib import unified_diff +from os import environ +from pathlib import Path + +_LINE = "=" * 80 + + +def main(): + src = "{{src}}" + dst = "{{dst}}" + + src = Path(src) + if not src.exists(): + raise AssertionError(f"The {src} file does not exist") + + if "TEST_SRCDIR" in environ: + # Running as a bazel test + dst = Path(dst) + a = dst.read_text() if dst.exists() else "\n" + b = src.read_text() + + diff = unified_diff( + a.splitlines(), + b.splitlines(), + str(dst), + str(src), + lineterm="", + ) + diff = "\n".join(list(diff)) + if not diff: + print( + f"""\ +{_LINE} +The in source file copy is up-to-date. +{_LINE} +""" + ) + return 0 + + print(diff) + print( + f"""\ +{_LINE} +The in source file copy is out of date, please run: + + bazel run {{update_target}} +{_LINE} +""" + ) + return 1 + + if "BUILD_WORKSPACE_DIRECTORY" not in environ: + raise RuntimeError( + "This must be either run as `bazel test` via a `native_test` or similar or via `bazel run`" + ) + + print(f"cp /{src} /{dst}") + build_workspace = Path(environ["BUILD_WORKSPACE_DIRECTORY"]) + + dst_real_path = build_workspace / dst + dst_real_path.parent.mkdir(parents=True, exist_ok=True) + dst_real_path.write_text(src.read_text()) + print(f"OK: updated {dst_real_path}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/python/uv/private/uv_toolchain.bzl b/python/uv/private/uv_toolchain.bzl index b740fc304d..8c7f1b4b8c 100644 --- a/python/uv/private/uv_toolchain.bzl +++ b/python/uv/private/uv_toolchain.bzl @@ -53,7 +53,7 @@ uv_toolchain = rule( mandatory = True, allow_single_file = True, executable = True, - cfg = "target", + cfg = "exec", ), "version": attr.string(mandatory = True, doc = "Version of the uv binary."), }, diff --git a/tests/support/remote-toolchains/BUILD.bazel b/tests/support/remote-toolchains/BUILD.bazel new file mode 100644 index 0000000000..ce204314e4 --- /dev/null +++ b/tests/support/remote-toolchains/BUILD.bazel @@ -0,0 +1,23 @@ +constraint_setting( + name = "container-image", +) + +constraint_value( + name = "ubuntu-act-22-04", + constraint_setting = ":container-image", +) + +REMOTE_EXEC_CONSTRAINTS = [ + "@platforms//cpu:x86_64", + "@platforms//os:linux", + ":ubuntu-act-22-04", +] + +platform( + name = "ubuntu-act-22-04-platform", + constraint_values = REMOTE_EXEC_CONSTRAINTS, + exec_properties = { + "OSFamily": "linux", + "container-image": "docker://ghcr.io/catthehacker/ubuntu:act-22.04@sha256:5f9c35c25db1d51a8ddaae5c0ba8d3c163c5e9a4a6cc97acd409ac7eae239448", + }, +) diff --git a/tests/uv/lock/BUILD.bazel b/tests/uv/lock/BUILD.bazel new file mode 100644 index 0000000000..6b6902da44 --- /dev/null +++ b/tests/uv/lock/BUILD.bazel @@ -0,0 +1,5 @@ +load(":lock_tests.bzl", "lock_test_suite") + +lock_test_suite( + name = "lock_tests", +) diff --git a/tests/uv/lock/lock_run_test.py b/tests/uv/lock/lock_run_test.py new file mode 100644 index 0000000000..ef57f23d31 --- /dev/null +++ b/tests/uv/lock/lock_run_test.py @@ -0,0 +1,165 @@ +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +from python import runfiles + +rfiles = runfiles.Create() + + +def _relative_rpath(path: str) -> Path: + p = (Path("_main") / "tests" / "uv" / "lock" / path).as_posix() + rpath = rfiles.Rlocation(p) + if not rpath: + raise ValueError(f"Could not find file: {p}") + + return Path(rpath) + + +class LockTests(unittest.TestCase): + def test_requirements_updating_for_the_first_time(self): + # Given + copier_path = _relative_rpath("requirements_new_file.update") + + # When + with tempfile.TemporaryDirectory() as dir: + workspace_dir = Path(dir) + want_path = workspace_dir / "tests" / "uv" / "lock" / "does_not_exist.txt" + + self.assertFalse( + want_path.exists(), "The path should not exist after the test" + ) + output = subprocess.run( + copier_path, + capture_output=True, + env={ + "BUILD_WORKSPACE_DIRECTORY": f"{workspace_dir}", + }, + ) + + # Then + self.assertEqual(0, output.returncode, output.stderr) + self.assertIn( + "cp /tests/uv/lock/requirements_new_file", + output.stdout.decode("utf-8"), + ) + self.assertTrue(want_path.exists(), "The path should exist after the test") + self.assertNotEqual(want_path.read_text(), "") + + def test_requirements_updating(self): + # Given + copier_path = _relative_rpath("requirements.update") + existing_file = _relative_rpath("testdata/requirements.txt") + want_text = existing_file.read_text() + + # When + with tempfile.TemporaryDirectory() as dir: + workspace_dir = Path(dir) + want_path = ( + workspace_dir + / "tests" + / "uv" + / "lock" + / "testdata" + / "requirements.txt" + ) + want_path.parent.mkdir(parents=True) + want_path.write_text( + want_text + "\n\n" + ) # Write something else to see that it is restored + + output = subprocess.run( + copier_path, + capture_output=True, + env={ + "BUILD_WORKSPACE_DIRECTORY": f"{workspace_dir}", + }, + ) + + # Then + self.assertEqual(0, output.returncode) + self.assertIn( + "cp /tests/uv/lock/requirements", + output.stdout.decode("utf-8"), + ) + self.assertEqual(want_path.read_text(), want_text) + + def test_requirements_run_on_the_first_time(self): + # Given + copier_path = _relative_rpath("requirements_new_file.run") + + # When + with tempfile.TemporaryDirectory() as dir: + workspace_dir = Path(dir) + want_path = workspace_dir / "tests" / "uv" / "lock" / "does_not_exist.txt" + # NOTE @aignas 2025-03-18: right now we require users to have the folder + # there already + want_path.parent.mkdir(parents=True) + + self.assertFalse( + want_path.exists(), "The path should not exist after the test" + ) + output = subprocess.run( + copier_path, + capture_output=True, + env={ + "BUILD_WORKSPACE_DIRECTORY": f"{workspace_dir}", + }, + ) + + # Then + self.assertEqual(0, output.returncode, output.stderr) + self.assertTrue(want_path.exists(), "The path should exist after the test") + got_contents = want_path.read_text() + self.assertNotEqual(got_contents, "") + self.assertIn( + got_contents, + output.stdout.decode("utf-8"), + ) + + def test_requirements_run(self): + # Given + copier_path = _relative_rpath("requirements.run") + existing_file = _relative_rpath("testdata/requirements.txt") + want_text = existing_file.read_text() + + # When + with tempfile.TemporaryDirectory() as dir: + workspace_dir = Path(dir) + want_path = ( + workspace_dir + / "tests" + / "uv" + / "lock" + / "testdata" + / "requirements.txt" + ) + + want_path.parent.mkdir(parents=True) + want_path.write_text( + want_text + "\n\n" + ) # Write something else to see that it is restored + + output = subprocess.run( + copier_path, + capture_output=True, + env={ + "BUILD_WORKSPACE_DIRECTORY": f"{workspace_dir}", + }, + ) + + # Then + self.assertEqual(0, output.returncode, output.stderr) + self.assertTrue(want_path.exists(), "The path should exist after the test") + got_contents = want_path.read_text() + self.assertNotEqual(got_contents, "") + self.assertIn( + got_contents, + output.stdout.decode("utf-8"), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/uv/lock/lock_tests.bzl b/tests/uv/lock/lock_tests.bzl new file mode 100644 index 0000000000..fd5867a45c --- /dev/null +++ b/tests/uv/lock/lock_tests.bzl @@ -0,0 +1,99 @@ +# Copyright 2025 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"" + +load("@bazel_skylib//rules:native_binary.bzl", "native_test") +load("//python/uv:lock.bzl", "lock") +load("//tests/support:sh_py_run_test.bzl", "py_reconfig_test") + +def lock_test_suite(name): + """The test suite with various lock-related integration tests + + Args: + name: {type}`str` the name of the test suite + """ + lock( + name = "requirements", + srcs = ["testdata/requirements.in"], + constraints = [ + "testdata/constraints.txt", + "testdata/constraints2.txt", + ], + build_constraints = [ + "testdata/build_constraints.txt", + "testdata/build_constraints2.txt", + ], + out = "testdata/requirements.txt", + ) + + lock( + name = "requirements_new_file", + srcs = ["testdata/requirements.in"], + out = "does_not_exist.txt", + ) + + py_reconfig_test( + name = "requirements_run_tests", + env = { + "BUILD_WORKSPACE_DIRECTORY": "foo", + }, + srcs = ["lock_run_test.py"], + deps = [ + "//python/runfiles", + ], + data = [ + "requirements_new_file.update", + "requirements_new_file.run", + "requirements.update", + "requirements.run", + "testdata/requirements.txt", + ], + main = "lock_run_test.py", + tags = [ + "requires-network", + # FIXME @aignas 2025-03-19: it seems that the RBE tests are failing + # to execute the `requirements.run` targets that require network. + # + # We could potentially dump the required `.html` files and somehow + # provide it to the `uv`, but may rely on internal uv handling of + # `--index-url`. + "no-remote-exec", + ], + # FIXME @aignas 2025-03-19: It seems that currently: + # 1. The Windows runners are not compatible with the `uv` Windows binaries. + # 2. The Python launcher is having trouble launching scripts from within the Python test. + target_compatible_with = select({ + "@platforms//os:windows": ["@platforms//:incompatible"], + "//conditions:default": [], + }), + ) + + # document and check that this actually works + native_test( + name = "requirements_test", + src = ":requirements.update", + target_compatible_with = select({ + "@platforms//os:windows": ["@platforms//:incompatible"], + "//conditions:default": [], + }), + ) + + native.test_suite( + name = name, + tests = [ + ":requirements_test", + ":requirements_run_tests", + ], + ) diff --git a/tests/uv/lock/testdata/build_constraints.txt b/tests/uv/lock/testdata/build_constraints.txt new file mode 100644 index 0000000000..34c3ebe3de --- /dev/null +++ b/tests/uv/lock/testdata/build_constraints.txt @@ -0,0 +1 @@ +certifi==2025.1.31 diff --git a/tests/uv/lock/testdata/build_constraints2.txt b/tests/uv/lock/testdata/build_constraints2.txt new file mode 100644 index 0000000000..34c3ebe3de --- /dev/null +++ b/tests/uv/lock/testdata/build_constraints2.txt @@ -0,0 +1 @@ +certifi==2025.1.31 diff --git a/tests/uv/lock/testdata/constraints.txt b/tests/uv/lock/testdata/constraints.txt new file mode 100644 index 0000000000..18ade2c5b9 --- /dev/null +++ b/tests/uv/lock/testdata/constraints.txt @@ -0,0 +1 @@ +charset-normalizer==3.4.0 diff --git a/tests/uv/lock/testdata/constraints2.txt b/tests/uv/lock/testdata/constraints2.txt new file mode 100644 index 0000000000..18ade2c5b9 --- /dev/null +++ b/tests/uv/lock/testdata/constraints2.txt @@ -0,0 +1 @@ +charset-normalizer==3.4.0 diff --git a/tests/uv/lock/testdata/requirements.in b/tests/uv/lock/testdata/requirements.in new file mode 100644 index 0000000000..f2293605cf --- /dev/null +++ b/tests/uv/lock/testdata/requirements.in @@ -0,0 +1 @@ +requests diff --git a/tests/uv/lock/testdata/requirements.txt b/tests/uv/lock/testdata/requirements.txt new file mode 100644 index 0000000000..d02844636d --- /dev/null +++ b/tests/uv/lock/testdata/requirements.txt @@ -0,0 +1,128 @@ +# This file was autogenerated by uv via the following command: +# bazel run //tests/uv/lock:requirements.update +certifi==2025.1.31 \ + --hash=sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651 \ + --hash=sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe + # via requests +charset-normalizer==3.4.0 \ + --hash=sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621 \ + --hash=sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6 \ + --hash=sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8 \ + --hash=sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912 \ + --hash=sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c \ + --hash=sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b \ + --hash=sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d \ + --hash=sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d \ + --hash=sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95 \ + --hash=sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e \ + --hash=sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565 \ + --hash=sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64 \ + --hash=sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab \ + --hash=sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be \ + --hash=sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e \ + --hash=sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907 \ + --hash=sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0 \ + --hash=sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2 \ + --hash=sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62 \ + --hash=sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62 \ + --hash=sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23 \ + --hash=sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc \ + --hash=sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284 \ + --hash=sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca \ + --hash=sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455 \ + --hash=sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858 \ + --hash=sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b \ + --hash=sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594 \ + --hash=sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc \ + --hash=sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db \ + --hash=sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b \ + --hash=sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea \ + --hash=sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6 \ + --hash=sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920 \ + --hash=sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749 \ + --hash=sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7 \ + --hash=sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd \ + --hash=sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99 \ + --hash=sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242 \ + --hash=sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee \ + --hash=sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129 \ + --hash=sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2 \ + --hash=sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51 \ + --hash=sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee \ + --hash=sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8 \ + --hash=sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b \ + --hash=sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613 \ + --hash=sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742 \ + --hash=sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe \ + --hash=sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3 \ + --hash=sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5 \ + --hash=sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631 \ + --hash=sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7 \ + --hash=sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15 \ + --hash=sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c \ + --hash=sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea \ + --hash=sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417 \ + --hash=sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250 \ + --hash=sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88 \ + --hash=sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca \ + --hash=sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa \ + --hash=sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99 \ + --hash=sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149 \ + --hash=sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41 \ + --hash=sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574 \ + --hash=sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0 \ + --hash=sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f \ + --hash=sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d \ + --hash=sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654 \ + --hash=sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3 \ + --hash=sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19 \ + --hash=sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90 \ + --hash=sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578 \ + --hash=sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9 \ + --hash=sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1 \ + --hash=sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51 \ + --hash=sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719 \ + --hash=sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236 \ + --hash=sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a \ + --hash=sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c \ + --hash=sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade \ + --hash=sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944 \ + --hash=sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc \ + --hash=sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6 \ + --hash=sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6 \ + --hash=sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27 \ + --hash=sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6 \ + --hash=sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2 \ + --hash=sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12 \ + --hash=sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf \ + --hash=sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114 \ + --hash=sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7 \ + --hash=sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf \ + --hash=sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d \ + --hash=sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b \ + --hash=sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed \ + --hash=sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03 \ + --hash=sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4 \ + --hash=sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67 \ + --hash=sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365 \ + --hash=sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a \ + --hash=sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748 \ + --hash=sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b \ + --hash=sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079 \ + --hash=sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482 + # via + # -c tests/uv/lock/testdata/constraints.txt + # -c tests/uv/lock/testdata/constraints2.txt + # requests +idna==3.10 \ + --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ + --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 + # via requests +requests==2.32.3 \ + --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ + --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 + # via -r tests/uv/lock/testdata/requirements.in +urllib3==2.3.0 \ + --hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \ + --hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d + # via requests diff --git a/tools/private/publish_deps.bzl b/tools/private/publish_deps.bzl index 538cc1d583..a9b0dbc562 100644 --- a/tools/private/publish_deps.bzl +++ b/tools/private/publish_deps.bzl @@ -17,13 +17,27 @@ load("//python/uv/private:lock.bzl", "lock") # buildifier: disable=bzl-visibility -def publish_deps(*, name, outs, **kwargs): - """Generate all of the requirements files for all platforms.""" +def publish_deps(*, name, args, outs, **kwargs): + """Generate all of the requirements files for all platforms. + + Args: + name: {type}`str`: the currently unused. + args: {type}`list[str]`: the common args to apply. + outs: {type}`dict[Label, str]`: the output files mapping to the platform + for each requirement file to be generated. + **kwargs: Extra args passed to the {rule}`lock` rule. + """ + all_args = args for out, platform in outs.items(): + args = [] + all_args + if platform: + args.append("--python-platform=" + platform) + else: + args.append("--universal") + lock( name = out.replace(".txt", ""), out = out, - universal = platform == "", - args = [] if not platform else ["--python-platform=" + platform], + args = args, **kwargs ) diff --git a/tools/publish/BUILD.bazel b/tools/publish/BUILD.bazel index 4cf99e4d97..2f02809ccd 100644 --- a/tools/publish/BUILD.bazel +++ b/tools/publish/BUILD.bazel @@ -33,6 +33,9 @@ publish_deps( "requirements_universal.txt": "", # universal "requirements_windows.txt": "windows", }, - upgrade = True, + args = [ + "--emit-index-url", + "--upgrade", # always upgrade + ], visibility = ["//private:__pkg__"], ) From 363780a6f674645b8cbf662e6755eb7eb2a2bef3 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Sat, 22 Mar 2025 21:39:57 +0900 Subject: [PATCH 02/15] fixup --- python/private/py_exec_tools_toolchain.bzl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/private/py_exec_tools_toolchain.bzl b/python/private/py_exec_tools_toolchain.bzl index 2745ddfb5b..8b7c23ad0f 100644 --- a/python/private/py_exec_tools_toolchain.bzl +++ b/python/private/py_exec_tools_toolchain.bzl @@ -152,7 +152,7 @@ def _current_interpreter_executable_impl(ctx): target_path = interpreter_basename + ".runfiles/" + interpreter_actual_path ctx.actions.symlink(output = executable, target_path = target_path) else: - interpreter_basename = paths.basename(runtime.interpreter.interpreter_path) + interpreter_basename = paths.basename(runtime.interpreter_path) executable = ctx.actions.declare_symlink(interpreter_basename) direct.append(executable) target_path = runtime.interpreter_path From 31370ea322f7a4bd0625b8f60f093a8e144c50a7 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Sat, 22 Mar 2025 21:42:24 +0900 Subject: [PATCH 03/15] decrease the diff for the py_exec_tools_toolchain --- python/private/py_exec_tools_toolchain.bzl | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/python/private/py_exec_tools_toolchain.bzl b/python/private/py_exec_tools_toolchain.bzl index 8b7c23ad0f..c5cbd9813f 100644 --- a/python/private/py_exec_tools_toolchain.bzl +++ b/python/private/py_exec_tools_toolchain.bzl @@ -117,7 +117,7 @@ def relative_path(from_, to): def _current_interpreter_executable_impl(ctx): toolchain = ctx.toolchains[TARGET_TOOLCHAIN_TYPE] runtime = toolchain.py3_runtime - direct = [] + runfiles = [] # NOTE: We name the output filename after the underlying file name # because of things like pyenv: they use $0 to determine what to @@ -135,7 +135,7 @@ def _current_interpreter_executable_impl(ctx): # in the hermetic toolchain. interpreter_basename = runtime.interpreter.basename executable = ctx.actions.declare_symlink("bin/" + interpreter_basename) - direct.append(executable) + runfiles.append(executable) interpreter_actual_path = runfiles_root_path(ctx, runtime.interpreter.short_path) target_path = relative_path( # dirname is necessary because a relative symlink is relative to @@ -152,11 +152,9 @@ def _current_interpreter_executable_impl(ctx): target_path = interpreter_basename + ".runfiles/" + interpreter_actual_path ctx.actions.symlink(output = executable, target_path = target_path) else: - interpreter_basename = paths.basename(runtime.interpreter_path) - executable = ctx.actions.declare_symlink(interpreter_basename) - direct.append(executable) - target_path = runtime.interpreter_path - ctx.actions.symlink(output = executable, target_path = target_path) + executable = ctx.actions.declare_symlink(paths.basename(runtime.interpreter_path)) + runfiles.append(executable) + ctx.actions.symlink(output = executable, target_path = runtime.interpreter_path) return [ toolchain, From 69cb0b456f8f40506dd16b913a81769cc3dad3ed Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Sat, 22 Mar 2025 21:55:35 +0900 Subject: [PATCH 04/15] revert the py_exec_tools_toolchain file --- python/private/py_exec_tools_toolchain.bzl | 67 +--------------------- 1 file changed, 3 insertions(+), 64 deletions(-) diff --git a/python/private/py_exec_tools_toolchain.bzl b/python/private/py_exec_tools_toolchain.bzl index c5cbd9813f..edf9159759 100644 --- a/python/private/py_exec_tools_toolchain.bzl +++ b/python/private/py_exec_tools_toolchain.bzl @@ -16,7 +16,6 @@ load("@bazel_skylib//lib:paths.bzl", "paths") load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") -load(":common.bzl", "runfiles_root_path") load(":py_exec_tools_info.bzl", "PyExecToolsInfo") load(":sentinel.bzl", "SentinelInfo") load(":toolchain_types.bzl", "TARGET_TOOLCHAIN_TYPE") @@ -83,84 +82,24 @@ See {obj}`PyExecToolsInfo.exec_interpreter` for further docs. }, ) -def relative_path(from_, to): - """Compute a relative path from one path to another. - - Args: - from_: {type}`str` the starting directory. Note that it should be - a directory because relative-symlinks are relative to the - directory the symlink resides in. - to: {type}`str` the path that `from_` wants to point to - - Returns: - {type}`str` a relative path - """ - from_parts = from_.split("/") - to_parts = to.split("/") - - # Strip common leading parts from both paths - n = min(len(from_parts), len(to_parts)) - for _ in range(n): - if from_parts[0] == to_parts[0]: - from_parts.pop(0) - to_parts.pop(0) - else: - break - - # Impossible to compute a relative path without knowing what ".." is - if from_parts and from_parts[0] == "..": - fail("cannot compute relative path from '%s' to '%s'", from_, to) - - parts = ([".."] * len(from_parts)) + to_parts - return paths.join(*parts) - def _current_interpreter_executable_impl(ctx): toolchain = ctx.toolchains[TARGET_TOOLCHAIN_TYPE] runtime = toolchain.py3_runtime - runfiles = [] # NOTE: We name the output filename after the underlying file name # because of things like pyenv: they use $0 to determine what to # re-exec. If it's not a recognized name, then they fail. if runtime.interpreter: - # Even though ctx.actions.symlink() could be used, we bump into the issue - # with RBE where bazel is making a copy to the file instead of symlinking - # to the hermetic toolchain repository. This means that we need to employ - # a similar strategy to how the `py_executable` venv is created where the - # file in the `runfiles` is a dangling symlink into the hermetic toolchain - # repository. This smells like a bug in RBE, but I would not be surprised - # if it is not one. - - # Create a dangling symlink in `bin/python3` to the real interpreter - # in the hermetic toolchain. - interpreter_basename = runtime.interpreter.basename - executable = ctx.actions.declare_symlink("bin/" + interpreter_basename) - runfiles.append(executable) - interpreter_actual_path = runfiles_root_path(ctx, runtime.interpreter.short_path) - target_path = relative_path( - # dirname is necessary because a relative symlink is relative to - # the directory the symlink resides within. - from_ = paths.dirname(runfiles_root_path(ctx, executable.short_path)), - to = interpreter_actual_path, - ) - ctx.actions.symlink(output = executable, target_path = target_path) - - # Create a dangling symlink into the runfiles and use that as the - # entry point. - interpreter_actual_path = runfiles_root_path(ctx, executable.short_path) - executable = ctx.actions.declare_symlink(interpreter_basename) - target_path = interpreter_basename + ".runfiles/" + interpreter_actual_path - ctx.actions.symlink(output = executable, target_path = target_path) + executable = ctx.actions.declare_file(runtime.interpreter.basename) + ctx.actions.symlink(output = executable, target_file = runtime.interpreter, is_executable = True) else: executable = ctx.actions.declare_symlink(paths.basename(runtime.interpreter_path)) - runfiles.append(executable) ctx.actions.symlink(output = executable, target_path = runtime.interpreter_path) - return [ toolchain, DefaultInfo( executable = executable, - runfiles = ctx.runfiles(direct, transitive_files = runtime.files), + runfiles = ctx.runfiles([executable], transitive_files = runtime.files), ), ] From a613d271f6faa28b1810269b0709cf8142802db4 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Sun, 23 Mar 2025 00:13:02 +0900 Subject: [PATCH 05/15] fix the RBE config by forwarding the runtime to the exec_tools_info --- python/private/py_exec_tools_info.bzl | 24 ++++++++++++++++++---- python/private/py_exec_tools_toolchain.bzl | 23 +++++++++++++++------ python/uv/private/lock.bzl | 11 +++++----- 3 files changed, 43 insertions(+), 15 deletions(-) diff --git a/python/private/py_exec_tools_info.bzl b/python/private/py_exec_tools_info.bzl index b74f480fab..dfd10a284f 100644 --- a/python/private/py_exec_tools_info.bzl +++ b/python/private/py_exec_tools_info.bzl @@ -24,15 +24,31 @@ When running it in an action, use `DefaultInfo.files_to_run` to ensure all its files are appropriately available. An exec interpreter may not be available, e.g. if all the exec tools are prebuilt binaries. -NOTE: this interpreter is really only for use when a build tool cannot use +:::{note} +this interpreter is really only for use when a build tool cannot use the Python toolchain itself. When possible, prefeer to define a `py_binary` instead and use it via a `cfg=exec` attribute; this makes it much easier to setup the runtime environment for the binary. See also: `py_interpreter_program` rule. +::: -NOTE: What interpreter is used depends on the toolchain constraints. Ensure -the proper target constraints are being applied when obtaining this from -the toolchain. +:::{note} +What interpreter is used depends on the toolchain constraints. Ensure the +proper target constraints are being applied when obtaining this from the +toolchain. +::: + +:::{warning} +This does not work correctly in case of RBE, please use exec_runtime instead. + +Once https://github.com/bazelbuild/bazel/issues/23620 is resolved this warning +may be removed. +::: +""", + "exec_runtime": """ +:type: PyRuntimeInfo | None + +The forwarded {obj}`PyRuntimeInfo` field. """, "precompiler": """ :type: Target | None diff --git a/python/private/py_exec_tools_toolchain.bzl b/python/private/py_exec_tools_toolchain.bzl index edf9159759..917677eeaf 100644 --- a/python/private/py_exec_tools_toolchain.bzl +++ b/python/private/py_exec_tools_toolchain.bzl @@ -29,13 +29,19 @@ def _py_exec_tools_toolchain_impl(ctx): if SentinelInfo in ctx.attr.exec_interpreter: exec_interpreter = None - return [platform_common.ToolchainInfo( - exec_tools = PyExecToolsInfo( - exec_interpreter = exec_interpreter, - precompiler = ctx.attr.precompiler, + # Forward the provider fields from the toolchain itself. + exec_runtime = ctx.attr.exec_interpreter[platform_common.ToolchainInfo] + + return [ + platform_common.ToolchainInfo( + exec_tools = PyExecToolsInfo( + exec_interpreter = exec_interpreter, + exec_runtime = exec_runtime, + precompiler = ctx.attr.precompiler, + ), + **extra_kwargs ), - **extra_kwargs - )] + ] py_exec_tools_toolchain = rule( implementation = _py_exec_tools_toolchain_impl, @@ -51,6 +57,11 @@ This provides `ToolchainInfo` with the following attributes: attrs = { "exec_interpreter": attr.label( default = "//python/private:current_interpreter_executable", + providers = [ + DefaultInfo, + # Add the toolchain provider so that we can forward provider fields. + platform_common.ToolchainInfo, + ], cfg = "exec", doc = """ An interpreter that is directly usable in the exec configuration diff --git a/python/uv/private/lock.bzl b/python/uv/private/lock.bzl index c78c5a5399..ec9de0a1ef 100644 --- a/python/uv/private/lock.bzl +++ b/python/uv/private/lock.bzl @@ -87,8 +87,6 @@ def _lock_impl(ctx): toolchain_info = ctx.toolchains[UV_TOOLCHAIN_TYPE] uv = toolchain_info.uv_toolchain_info.uv[DefaultInfo].files_to_run.executable - interpreter = ctx.toolchains[EXEC_TOOLS_TOOLCHAIN_TYPE].exec_tools.exec_interpreter[DefaultInfo] - args = _args(ctx) args.add_all([ uv, @@ -108,7 +106,10 @@ def _lock_impl(ctx): args.add_all(ctx.files.constraints, before_each = "--constraints") args.add_all(ctx.attr.args) - python = interpreter.files_to_run.executable or "python" + exec_tools = ctx.toolchains[EXEC_TOOLS_TOOLCHAIN_TYPE].exec_tools + runtime = exec_tools.exec_runtime.py3_runtime + python = runtime.interpreter or runtime.interpreter_path + python_files = runtime.files args.add("--python", python) args.add_all(srcs) @@ -142,7 +143,7 @@ def _lock_impl(ctx): arguments = [args.run_shell], tools = [ uv, - interpreter.files_to_run, + python_files, ], progress_message = "Creating a requirements.txt with uv: //{}:{}".format( ctx.label.package, @@ -158,7 +159,7 @@ def _lock_impl(ctx): env = ctx.attr.env, srcs = depset( srcs + [uv], - transitive = [interpreter.files], + transitive = [python_files], ), ), ] From dd8d449e14b82191a8f10a4e501b6fde82e2ae8b Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Sun, 23 Mar 2025 00:15:22 +0900 Subject: [PATCH 06/15] add a note about RBE --- python/uv/private/lock.bzl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/python/uv/private/lock.bzl b/python/uv/private/lock.bzl index ec9de0a1ef..8fc1a46916 100644 --- a/python/uv/private/lock.bzl +++ b/python/uv/private/lock.bzl @@ -392,7 +392,10 @@ def lock( {obj}`pip.parse` bzlmod extension tag class. Note, you can use `native_test` to wrap this target to make a test. You can't customize the args via command line, but you can use RBE to generate requirements - (offload execution and run for different platforms) + (offload execution and run for different platforms). Note, that for RBE + to be usable, one needs to ensure that the nodes running the action have + internet connectivity or the indexes are provided in a different way for + a fully offline operation. :::{note} All of the targets have `manual` tags as locking results cannot be cached. From d8bdd3c8f42e3b164a47020510e17e3f3d8ea548 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Sun, 23 Mar 2025 00:21:29 +0900 Subject: [PATCH 07/15] fix the sentinel --- python/private/sentinel.bzl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/python/private/sentinel.bzl b/python/private/sentinel.bzl index 6d753e1983..777e7b0306 100644 --- a/python/private/sentinel.bzl +++ b/python/private/sentinel.bzl @@ -25,6 +25,10 @@ SentinelInfo = provider( def _sentinel_impl(ctx): _ = ctx # @unused - return [SentinelInfo()] + return [ + SentinelInfo(), + # Also output ToolchainInfo + platform_common.ToolchainInfo(), + ] sentinel = rule(implementation = _sentinel_impl) From 5dc2ba34233aaf1544a1c73920e88c73d1ffc6b8 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Sun, 23 Mar 2025 00:28:39 +0900 Subject: [PATCH 08/15] add tags for the test locks --- tests/uv/lock/lock_tests.bzl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/uv/lock/lock_tests.bzl b/tests/uv/lock/lock_tests.bzl index fd5867a45c..877854a4a0 100644 --- a/tests/uv/lock/lock_tests.bzl +++ b/tests/uv/lock/lock_tests.bzl @@ -35,6 +35,9 @@ def lock_test_suite(name): "testdata/build_constraints.txt", "testdata/build_constraints2.txt", ], + # It seems that the CI remote executors for the RBE do not have network + # connectivity. Is it only our setup or is it a property of RBE? + tags = ["no-remote-exec"], out = "testdata/requirements.txt", ) @@ -42,6 +45,9 @@ def lock_test_suite(name): name = "requirements_new_file", srcs = ["testdata/requirements.in"], out = "does_not_exist.txt", + # It seems that the CI remote executors for the RBE do not have network + # connectivity. Is it only our setup or is it a property of RBE? + tags = ["no-remote-exec"], ) py_reconfig_test( From 2e7211e26c78f3a19bdb850e310c857a037f1689 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Sun, 23 Mar 2025 00:33:06 +0900 Subject: [PATCH 09/15] add docs --- CHANGELOG.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc40a25961..55029edae1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,7 +61,14 @@ Unreleased changes template. {#v0-0-0-added} ### Added -* Nothing added. +* (uv) A {obj}`lock` rule that is the replacement for the + {obj}`compile_pip_requirements`. This may still have rough corners + so please report issues with it in the + [#1975](https://github.com/bazel-contrib/rules_python/issues/1975). + Main highlights - the locking can be done within a build action or outside + it, there is no more automatic `test` target (but it can be added on the user + side by using `native_test`). For customizing the `uv` version that is used, + please check the {obj}`uv.configure` tag class. {#v0-0-0-removed} ### Removed From b3bd1c9b0d40b673cbe3891b87ab66674d1af799 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Mon, 24 Mar 2025 10:25:06 +0900 Subject: [PATCH 10/15] add a note about the exec toolchain changes. --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55029edae1..6eb026a14e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,7 +53,10 @@ Unreleased changes template. {#v0-0-0-changed} ### Changed -* Nothing changed. +* (toolchain) The `exec` configuration toolchain now has the forwarded + `py3_runtime` field from the `target` configuration toolchain. This is for + increased compatibility with the `RBE` setups where access to the `exec` + configuration interpreter is needed. {#v0-0-0-fixed} ### Fixed From afe308c4f5a722b0acd448dd0ba969d5da8b670b Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Mon, 24 Mar 2025 10:27:14 +0900 Subject: [PATCH 11/15] Update py_exec_tools_toolchain.bzl docs --- python/private/py_exec_tools_toolchain.bzl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/python/private/py_exec_tools_toolchain.bzl b/python/private/py_exec_tools_toolchain.bzl index 917677eeaf..15cfe5853d 100644 --- a/python/private/py_exec_tools_toolchain.bzl +++ b/python/private/py_exec_tools_toolchain.bzl @@ -80,6 +80,11 @@ handle all the necessary transitions and runtime setup to invoke a program. ::: See {obj}`PyExecToolsInfo.exec_interpreter` for further docs. + +:::{versionchanged} VERSION_NEXT_FEATURE +From now on the provided target also needs to provide `platform_common.ToolchainInfo` +so that the toolchain `py_runtime` field can be correctly forwarded. +::: """, ), "precompiler": attr.label( From 0573d6be9610bf115513792bbf4ca4953baadbd2 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Mon, 24 Mar 2025 11:19:24 +0900 Subject: [PATCH 12/15] Update python/uv/lock.bzl --- python/uv/lock.bzl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/uv/lock.bzl b/python/uv/lock.bzl index 68a63740da..82b00bc2d2 100644 --- a/python/uv/lock.bzl +++ b/python/uv/lock.bzl @@ -40,7 +40,7 @@ native_test( ) ``` -EXPERIMENTAL: This is experimental and may be removed without notice +EXPERIMENTAL: This is experimental and may be changed without notice. """ load("//python/uv/private:lock.bzl", _lock = "lock") From 64c6c8ca44bb92a9fff204f7a60e679444209ef6 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Mon, 24 Mar 2025 11:23:16 +0900 Subject: [PATCH 13/15] Update python/private/py_exec_tools_info.bzl --- python/private/py_exec_tools_info.bzl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/python/private/py_exec_tools_info.bzl b/python/private/py_exec_tools_info.bzl index dfd10a284f..1294fc6207 100644 --- a/python/private/py_exec_tools_info.bzl +++ b/python/private/py_exec_tools_info.bzl @@ -49,6 +49,9 @@ may be removed. :type: PyRuntimeInfo | None The forwarded {obj}`PyRuntimeInfo` field. + +:::{versionadded} VERSION_NEXT_FEATURE +::: """, "precompiler": """ :type: Target | None From 318a462b8d5e332ca8b454000884130f39275650 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Mon, 24 Mar 2025 11:24:19 +0900 Subject: [PATCH 14/15] Delete tests/support/remote-toolchains/BUILD.bazel --- tests/support/remote-toolchains/BUILD.bazel | 23 --------------------- 1 file changed, 23 deletions(-) delete mode 100644 tests/support/remote-toolchains/BUILD.bazel diff --git a/tests/support/remote-toolchains/BUILD.bazel b/tests/support/remote-toolchains/BUILD.bazel deleted file mode 100644 index ce204314e4..0000000000 --- a/tests/support/remote-toolchains/BUILD.bazel +++ /dev/null @@ -1,23 +0,0 @@ -constraint_setting( - name = "container-image", -) - -constraint_value( - name = "ubuntu-act-22-04", - constraint_setting = ":container-image", -) - -REMOTE_EXEC_CONSTRAINTS = [ - "@platforms//cpu:x86_64", - "@platforms//os:linux", - ":ubuntu-act-22-04", -] - -platform( - name = "ubuntu-act-22-04-platform", - constraint_values = REMOTE_EXEC_CONSTRAINTS, - exec_properties = { - "OSFamily": "linux", - "container-image": "docker://ghcr.io/catthehacker/ubuntu:act-22.04@sha256:5f9c35c25db1d51a8ddaae5c0ba8d3c163c5e9a4a6cc97acd409ac7eae239448", - }, -) From db046aa487a4ee166b5af6fa220e6195f654b441 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Mon, 24 Mar 2025 18:41:57 +0900 Subject: [PATCH 15/15] retain the universal requirements for the bzlmod example --- examples/BUILD.bazel | 2 ++ examples/bzlmod/requirements_lock_3_9.txt | 17 +++++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/examples/BUILD.bazel b/examples/BUILD.bazel index 0ef5211877..d2fddc44c5 100644 --- a/examples/BUILD.bazel +++ b/examples/BUILD.bazel @@ -23,6 +23,8 @@ lock( out = "bzlmod/requirements_lock_3_9.txt", args = [ "--emit-index-url", + "--universal", + "--python-version=3.9", ], python_version = "3.9.19", ) diff --git a/examples/bzlmod/requirements_lock_3_9.txt b/examples/bzlmod/requirements_lock_3_9.txt index ba1f85c438..c48f406451 100644 --- a/examples/bzlmod/requirements_lock_3_9.txt +++ b/examples/bzlmod/requirements_lock_3_9.txt @@ -26,7 +26,10 @@ chardet==4.0.0 \ colorama==0.4.6 \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 - # via -r examples/bzlmod/requirements.in + # via + # -r examples/bzlmod/requirements.in + # pylint + # sphinx dill==0.3.6 \ --hash=sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0 \ --hash=sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373 @@ -43,7 +46,7 @@ imagesize==1.4.1 \ --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \ --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a # via sphinx -importlib-metadata==8.4.0 \ +importlib-metadata==8.4.0 ; python_full_version < '3.10' \ --hash=sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1 \ --hash=sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5 # via sphinx @@ -262,7 +265,9 @@ s3cmd==2.1.0 \ setuptools==65.6.3 \ --hash=sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54 \ --hash=sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75 - # via yamllint + # via + # babel + # yamllint six==1.16.0 \ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 @@ -311,7 +316,7 @@ tabulate==0.9.0 \ --hash=sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c \ --hash=sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f # via -r examples/bzlmod/requirements.in -tomli==2.0.1 \ +tomli==2.0.1 ; python_full_version < '3.11' \ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f # via pylint @@ -319,7 +324,7 @@ tomlkit==0.11.6 \ --hash=sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b \ --hash=sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73 # via pylint -typing-extensions==4.12.2 \ +typing-extensions==4.12.2 ; python_full_version < '3.10' \ --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \ --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8 # via @@ -475,7 +480,7 @@ yamllint==1.28.0 \ --hash=sha256:89bb5b5ac33b1ade059743cf227de73daa34d5e5a474b06a5e17fc16583b0cf2 \ --hash=sha256:9e3d8ddd16d0583214c5fdffe806c9344086721f107435f68bad990e5a88826b # via -r examples/bzlmod/requirements.in -zipp==3.20.0 \ +zipp==3.20.0 ; python_full_version < '3.10' \ --hash=sha256:0145e43d89664cfe1a2e533adc75adafed82fe2da404b4bbb6b026c0157bdb31 \ --hash=sha256:58da6168be89f0be59beb194da1250516fdaa062ccebd30127ac65d30045e10d # via importlib-metadata