Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -94,24 +94,32 @@ def fetch_buildkite_data(build_url):


def download_log(job_url, output_path):
# Construct raw log URL: job_url + "/raw" (Buildkite convention)
# job_url e.g. https://buildkite.com/org/pipeline/builds/14394#job-id
# Wait, the job['path'] gives /org/pipeline/builds/14394#job-id
# We want /org/pipeline/builds/14394/jobs/job-id/raw? No
# The clean URL for a job is https://buildkite.com/org/pipeline/builds/14394/jobs/job-id
# And raw log is https://buildkite.com/org/pipeline/builds/14394/jobs/job-id/raw

# We have full_url e.g. https://buildkite.com/bazel/rules-python-python/builds/14394#019c5cf9-e3cf-468f-a7b1-8f9f5ad4b08c
# We need to transform it.
# job_url looks like:
# https://buildkite.com/bazel/rules-python-python/builds/15594#019e879b-...
# We need to transform it to:
# https://buildkite.com/organizations/bazel/pipelines/rules-python-python/builds/15594/jobs/{job_id}/download.txt

if "#" in job_url:
base, job_id = job_url.split("#")
# Ensure base doesn't end with /
if base.endswith("/"):
base = base[:-1]

# Build raw URL
raw_url = f"{base}/jobs/{job_id}/raw"
base = base.rstrip("/")

# Parse the path segments: https://buildkite.com/org/pipeline/builds/N
# Rebuild with the /organizations/org/pipelines/pipeline/ format which
# supports the /jobs/{id}/download.txt log URL without auth.
parts = base.split("/")
# parts = ["https:", "", "buildkite.com", "org", "pipeline", "builds", "N"]
if len(parts) >= 7 and parts[2] == "buildkite.com":
org = parts[3]
pipeline = parts[4]
build_num = parts[6] if len(parts) >= 7 else ""
raw_url = (
f"https://buildkite.com/organizations/{org}"
f"/pipelines/{pipeline}"
f"/builds/{build_num}"
f"/jobs/{job_id}/download.txt"
)
else:
raw_url = f"{base}/jobs/{job_id}/download.txt"
else:
print(f"Could not parse job URL for download: {job_url}", file=sys.stderr)
return False
Expand Down
1 change: 1 addition & 0 deletions .bazelignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ tests/integration/compile_pip_requirements/bazel-compile_pip_requirements
tests/integration/local_toolchains/bazel-local_toolchains
tests/integration/py_cc_toolchain_registered/bazel-py_cc_toolchain_registered
tests/integration/toolchain_target_settings/bazel-module_under_test
tests/integration/uv_lock/bazel-uv_lock
1 change: 1 addition & 0 deletions .bazelrc.deleted_packages
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ common --deleted_packages=tests/integration/pip_parse/empty
common --deleted_packages=tests/integration/pip_parse_isolated
common --deleted_packages=tests/integration/py_cc_toolchain_registered
common --deleted_packages=tests/integration/toolchain_target_settings
common --deleted_packages=tests/integration/uv_lock
common --deleted_packages=tests/modules/another_module
common --deleted_packages=tests/modules/other
common --deleted_packages=tests/modules/other/nspkg_delta
Expand Down
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
python/features.bzl export-subst
tools/publish/*.txt linguist-generated=true
tests/uv/lock/testdata/requirements.txt text eol=lf
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,7 @@ user.bazelrc
# MODULE.bazel.lock is ignored for now as per recommendation from upstream.
# See https://github.com/bazelbuild/bazel/issues/20369
MODULE.bazel.lock

# Buildkite logs
*Windows*.log

6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ END_UNRELEASED_TEMPLATE
PyPI download and supports PyPI mirror implementations that do not support the root
index functionality. Fixes
([#3769](https://github.com/bazel-contrib/rules_python/pull/3769)).
* (uv) allow user overwrite the build environment using `--action_env` to allow
setting authentication for the index URL.
([#3405](https://github.com/bazel-contrib/rules_python/issues/3405))
* (uv) fix the execution of the `uv pip compile` in the sandbox. Work
towards better supporting `uv` out of the box on our platforms.
([#1975](https://github.com/bazel-contrib/rules_python/issues/1975))

{#v0-0-0-added}
### Added
Expand Down
14 changes: 7 additions & 7 deletions python/uv/private/lock.bat
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
if defined BUILD_WORKSPACE_DIRECTORY (
set "out=%BUILD_WORKSPACE_DIRECTORY%\{{src_out}}"
) else (
exit /b 1
)

"{{args}}" --output-file "%out%" %*
if defined BUILD_WORKSPACE_DIRECTORY (
set "out=%BUILD_WORKSPACE_DIRECTORY%\{{src_out}}"
) else (
exit /b 1
)
"{{args}}" --output-file "%out%" %*
95 changes: 85 additions & 10 deletions python/uv/private/lock.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -117,31 +117,96 @@ def _lock_impl(ctx):
args.run_shell.add("--no-progress")
args.run_shell.add("--quiet")

# Generate a wrapper script that copies the existing output (if any) and
# then runs uv. On POSIX, args are forwarded via exec "$@". On Windows,
# the full command line is embedded in the .bat file with backslash paths
# (CMD doesn't recognize forward slashes in executable paths).
if ctx.attr.is_windows:
ext = ".bat"
lines = ["@echo off"]
else:
ext = ".sh"
lines = ["#!/usr/bin/env bash", "set -euo pipefail"]

python_path = getattr(python, "path", python)

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(
python_cmd = "from shutil import copy; copy(\"{src}\", \"{dst}\")".format(
src = ctx.files.existing_output[0].path,
dst = output.path,
)
if ctx.attr.is_windows:
# In batch files, use "" to escape internal double quotes.
lines.append(
"\"{py}\" -c \"from shutil import copy; copy(\"\"{src}\"\", \"\"{dst}\"\")\"".format(
py = python_path,
src = ctx.files.existing_output[0].path,
dst = output.path,
),
)
else:
lines.append("{py} -c '{cmd}'".format(
py = python_path,
cmd = python_cmd,
))

if ctx.attr.is_windows:
# Build the command line with backslash paths for CMD.
# args.run_info has most args; add the output/progress/quiet
# args that were only added directly to args.run_shell.
def _quote(arg):
if hasattr(arg, "path"):
arg = arg.path.replace("/", "\\")
else:
arg = str(arg)
return '"' + arg.replace('"', '""') + '"'

bat_args = args.run_info + [
"--output-file",
output,
"--no-progress",
"--quiet",
]
lines.append(" ".join([_quote(a) for a in bat_args]))

# Normalize CRLF line endings in the output on Windows.
lines.append(
"\"{py}\" -c \"import pathlib;p=pathlib.Path(r\"\"{dst}\"\");p.write_bytes(p.read_bytes().replace(b'\\r\\n', b'\\n'))\"".format(
py = python_path,
dst = output.path,
),
)
else:
command = '"$@"'
lines.append('exec "$@"')

script = ctx.actions.declare_file(ctx.label.name + "_lock" + ext)
if ctx.attr.is_windows:
content = "\r\n".join(lines) + "\r\n"
else:
content = "\n".join(lines) + "\n"
ctx.actions.write(output = script, content = content, is_executable = True)

srcs = srcs + ctx.files.build_constraints + ctx.files.constraints

ctx.actions.run_shell(
command = command,
ctx.actions.run(
executable = script,
inputs = srcs + ctx.files.existing_output,
mnemonic = "PyRequirementsLockUv",
outputs = [output],
arguments = [args.run_shell],
# On Windows, the command line is embedded directly in the .bat
# script (with backslash paths). On POSIX, args are forwarded via
# exec "$@" in the .sh script.
arguments = [args.run_shell] if not ctx.attr.is_windows else [],
tools = [
uv,
python_files,
script,
],
# User reported being unable to add `--action_env` and get it to work.
# Without this flag.
#
# Ref: https://app.slack.com/client/TA4K1KQ87/CA306CEV6
use_default_shell_env = True,
progress_message = "Creating a requirements.txt with uv: %{label}",
env = ctx.attr.env,
)
Expand Down Expand Up @@ -205,6 +270,7 @@ modifications and the locking is not done from scratch.
doc = "Public, see the docs in the macro.",
default = True,
),
"is_windows": attr.bool(mandatory = True),
"output": attr.string(
doc = "Public, see the docs in the macro.",
mandatory = True,
Expand Down Expand Up @@ -241,7 +307,7 @@ The string to input for the 'uv pip compile'.
def _lock_run_impl(ctx):
if ctx.attr.is_windows:
path_sep = "\\"
ext = ".exe"
ext = ".bat"
else:
path_sep = "/"
ext = ""
Expand All @@ -250,7 +316,12 @@ def _lock_run_impl(ctx):
if hasattr(arg, "short_path"):
arg = arg.short_path

return shell.quote(arg.replace("/", path_sep))
arg = arg.replace("/", path_sep)
if ctx.attr.is_windows:
# On Windows, CMD uses double quotes for quoting, and internal
# double quotes are escaped by doubling them.
return '"' + arg.replace('"', '""') + '"'
return shell.quote(arg)

info = ctx.attr.lock[_RunLockInfo]
executable = ctx.actions.declare_file(ctx.label.name + ext)
Expand Down Expand Up @@ -438,6 +509,10 @@ def lock(
env = env,
existing_output = maybe_out,
generate_hashes = generate_hashes,
is_windows = select({
"@platforms//os:windows": True,
"//conditions:default": False,
}),
python_version = python_version,
srcs = srcs,
strip_extras = strip_extras,
Expand Down
2 changes: 1 addition & 1 deletion python/uv/private/lock_copier.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def main():
"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}")
print(f"cp <bazel-sandbox>/{src.as_posix()} <workspace>/{dst}")
build_workspace = Path(environ["BUILD_WORKSPACE_DIRECTORY"])

dst_real_path = build_workspace / dst
Expand Down
36 changes: 36 additions & 0 deletions tests/integration/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
# limitations under the License.

load("@rules_bazel_integration_test//bazel_integration_test:defs.bzl", "default_test_runner")
load("//python:py_binary.bzl", "py_binary")
load("//python:py_library.bzl", "py_library")
load("//tests/support:support.bzl", "NOT_WINDOWS")
load(":integration_test.bzl", "rules_python_integration_test")

licenses(["notice"])
Expand Down Expand Up @@ -48,6 +50,7 @@ test_suite(
tests = [
"bzlmod_lockfile_test_bazel_9.1.0",
"local_toolchains_test_bazel_self",
"uv_lock_test_bazel_self",
],
)

Expand Down Expand Up @@ -111,8 +114,41 @@ rules_python_integration_test(
py_main = "toolchain_target_settings_test.py",
)

rules_python_integration_test(
name = "uv_lock_test",
py_deps = [
"@pypiserver//pypiserver",
":uv_lock_pypi_server_lib",
],
py_main = "uv_lock_test.py",
)

py_library(
name = "runner_lib",
srcs = ["runner.py"],
imports = ["../../"],
)

py_library(
name = "uv_lock_pypi_server_lib",
srcs = ["uv_lock_pypi_server.py"],
imports = ["../../"],
# currently windows is not working due to
# https://github.com/pypiserver/pypiserver/blob/main/pypiserver/config.py#L123
#
# class DEFAULTS:
# ....
# PACKAGE_DIRECTORIES = [pathlib.Path("~/packages").expanduser().resolve()]
# ....
#
# which is loaded through `__init__.py` even though it is not used and breaks because
# in a Windows sandbox one cannot resolve the home directory.
target_compatible_with = NOT_WINDOWS,
deps = ["@pypiserver//pypiserver"],
)

py_binary(
name = "uv_lock_pypi_server",
srcs = ["uv_lock_pypi_server.py"],
deps = [":uv_lock_pypi_server_lib"],
)
7 changes: 5 additions & 2 deletions tests/integration/integration_test.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ load(
)
load("//python:py_test.bzl", "py_test")

def _test_runner(*, name, bazel_version, py_main, bzlmod):
def _test_runner(*, name, bazel_version, py_main, bzlmod, py_deps):
if py_main:
test_runner = "{}_bazel_{}_py_runner".format(name, bazel_version)
py_test(
name = test_runner,
srcs = [py_main],
main = py_main,
deps = [":runner_lib"],
deps = [":runner_lib"] + py_deps,
# Hide from ... patterns; should only be run as part
# of the bazel integration test
tags = ["manual"],
Expand All @@ -46,6 +46,7 @@ def rules_python_integration_test(
bzlmod = True,
tags = None,
py_main = None,
py_deps = None,
bazel_versions = None,
**kwargs):
"""Runs a bazel-in-bazel integration test.
Expand All @@ -60,6 +61,7 @@ def rules_python_integration_test(
py_main: Optional `.py` file to run tests using. When specified, a
python based test runner is used, and this source file is the main
entry point and responsible for executing tests.
py_deps: Optional test runner deps to use for setup.
bazel_versions: `list[str] | None`, the bazel versions to test. I
not specified, defaults to all configured bazel versions.
**kwargs: Passed to the upstream `bazel_integration_tests` rule.
Expand Down Expand Up @@ -91,6 +93,7 @@ def rules_python_integration_test(
name = name,
bazel_version = bazel_version,
py_main = py_main,
py_deps = py_deps or [],
bzlmod = bzlmod,
)
bazel_integration_test(
Expand Down
5 changes: 5 additions & 0 deletions tests/integration/uv_lock/.bazelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
build --enable_runfiles
common --experimental_isolated_extension_usages
common --action_env=UV_EXTRA_INDEX_URL

try-import %workspace%/user.bazelrc
1 change: 1 addition & 0 deletions tests/integration/uv_lock/.bazelversion
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
9.1.0
26 changes: 26 additions & 0 deletions tests/integration/uv_lock/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
load("@bazel_skylib//rules:diff_test.bzl", "diff_test")
load("@rules_python//python/uv:lock.bzl", "lock")
load(":uv_runner.bzl", "uv_runner")

lock(
name = "requirements",
srcs = ["requirements.in"],
out = "requirements.txt",
tags = ["no-remote-exec"],
)

uv_runner(
name = "uv",
is_windows = select({
"@platforms//os:windows": True,
"//conditions:default": False,
}),
tags = ["manual"],
)

diff_test(
name = "requirements_diff_test",
timeout = "short",
file1 = ":requirements",
file2 = ":requirements.txt",
)
Loading