diff --git a/CHANGELOG.md b/CHANGELOG.md index c64241ccbf..80466fc3f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,10 @@ Unreleased changes template. {#v0-0-0-changed} ### Changed +* (toolchain) The `exec` configuration toolchain now has the forwarded + `exec_interpreter` now also forwards the `ToolchainInfo` provider. This is + for increased compatibility with the `RBE` setups where access to the `exec` + configuration interpreter is needed. * (toolchains) Use the latest astrahl-sh toolchain release [20250317] for Python versions: * 3.9.21 * 3.10.16 @@ -75,6 +79,14 @@ Unreleased changes template. {#v0-0-0-added} ### 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. * Add support for riscv64 linux platform. * (toolchains) Add python 3.13.2 and 3.12.9 toolchains 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..d2fddc44c5 100644 --- a/examples/BUILD.bazel +++ b/examples/BUILD.bazel @@ -21,5 +21,10 @@ lock( name = "bzlmod_requirements_3_9", srcs = ["bzlmod/requirements.in"], 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 d74d1d39b6..c48f406451 100644 --- a/examples/bzlmod/requirements_lock_3_9.txt +++ b/examples/bzlmod/requirements_lock_3_9.txt @@ -46,7 +46,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 ; python_full_version < '3.10' \ --hash=sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1 \ --hash=sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5 # via sphinx @@ -316,7 +316,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 ; python_full_version < '3.11' \ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f # via pylint @@ -324,7 +324,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 ; python_full_version < '3.10' \ --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \ --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8 # via @@ -480,7 +480,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 ; python_full_version < '3.10' \ --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_info.bzl b/python/private/py_exec_tools_info.bzl index b74f480fab..ad9a7b0c5e 100644 --- a/python/private/py_exec_tools_info.bzl +++ b/python/private/py_exec_tools_info.bzl @@ -24,15 +24,26 @@ 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. +::: """, "precompiler": """ :type: Target | None diff --git a/python/private/py_exec_tools_toolchain.bzl b/python/private/py_exec_tools_toolchain.bzl index edf9159759..ff30431ff4 100644 --- a/python/private/py_exec_tools_toolchain.bzl +++ b/python/private/py_exec_tools_toolchain.bzl @@ -29,13 +29,15 @@ 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, + return [ + platform_common.ToolchainInfo( + exec_tools = PyExecToolsInfo( + exec_interpreter = exec_interpreter, + precompiler = ctx.attr.precompiler, + ), + **extra_kwargs ), - **extra_kwargs - )] + ] py_exec_tools_toolchain = rule( implementation = _py_exec_tools_toolchain_impl, @@ -51,6 +53,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 @@ -69,6 +76,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( diff --git a/python/private/sentinel.bzl b/python/private/sentinel.bzl index 6d753e1983..8b69682b49 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 to allow it to be used for noop toolchains + platform_common.ToolchainInfo(), + ] sentinel = rule(implementation = _sentinel_impl) diff --git a/python/uv/lock.bzl b/python/uv/lock.bzl index edffe4728c..82b00bc2d2 100644 --- a/python/uv/lock.bzl +++ b/python/uv/lock.bzl @@ -14,7 +14,33 @@ """The `uv` locking rule. -EXPERIMENTAL: This is experimental and may be removed without notice +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 changed without notice. """ load("//python/uv/private:lock.bzl", _lock = "lock") 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..69d277d653 100644 --- a/python/uv/private/lock.bzl +++ b/python/uv/private/lock.bzl @@ -12,114 +12,483 @@ # 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 + + 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) + + exec_tools = ctx.toolchains[EXEC_TOOLS_TOOLCHAIN_TYPE].exec_tools + runtime = exec_tools.exec_interpreter[platform_common.ToolchainInfo].py3_runtime + python = runtime.interpreter or runtime.interpreter_path + python_files = runtime.files + 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") - 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`. + 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, + python_files, + ], + progress_message = "Creating a requirements.txt with uv: %{label}", + env = ctx.attr.env, + ) + + return [ + DefaultInfo(files = depset([output])), + _RunLockInfo( + args = args.run_info, + env = ctx.attr.env, + srcs = depset( + srcs + [uv], + transitive = [python_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 = """\ +The lock rule that does the locking in a build action (that makes it possible +to use RBE) and also prepares information for a `bazel run` executable rule. +""", + 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)) + + 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 + +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, 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. - # Make a copy to ensure that we are not modifying the initial list - srcs = list(srcs) + :::{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 <bazel-sandbox>/{src} <workspace>/{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/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 <bazel-sandbox>/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 <bazel-sandbox>/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..35c7c19328 --- /dev/null +++ b/tests/uv/lock/lock_tests.bzl @@ -0,0 +1,105 @@ +# 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", + ], + # It seems that the CI remote executors for the RBE do not have network + # connectivity due to current CI setup. + tags = ["no-remote-exec"], + out = "testdata/requirements.txt", + ) + + lock( + 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 due to current CI setup. + tags = ["no-remote-exec"], + ) + + 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__"], )