diff --git a/.agents/rules/bzl.md b/.agents/rules/bzl.md new file mode 100644 index 0000000000..10aca72184 --- /dev/null +++ b/.agents/rules/bzl.md @@ -0,0 +1,10 @@ +--- +trigger: glob +description: Starlark / Bazel .bzl file coding style rules +globs: *.bzl +--- + +# Starlark Rules + +* Use triple-quoted strings for multi-line rule doc args. +* Don't use backslash line continuation in rule doc args. diff --git a/.agents/rules/github_actions_workflows.md b/.agents/rules/github_actions_workflows.md new file mode 100644 index 0000000000..29bf628a24 --- /dev/null +++ b/.agents/rules/github_actions_workflows.md @@ -0,0 +1,10 @@ +--- +name: github-actions-workflows +trigger: glob +globs: [".github/workflows/*.yml", ".github/workflows/*.yaml", ".github/*.yaml"] +--- + +# GitHub Actions Workflows Rule + +* When creating files in `.github/workflows/` (such as `.yml` or `.yaml` + files), always use the latest version of the referenced GitHub Actions. diff --git a/.agents/rules/when-to-use-copyright.md b/.agents/rules/when-to-use-copyright.md new file mode 100644 index 0000000000..a73cbbfd70 --- /dev/null +++ b/.agents/rules/when-to-use-copyright.md @@ -0,0 +1,8 @@ +--- +trigger: always_on +--- + +# When to Use Copyright + +Unless directed by the user otherwise, do not add Bazel copyright to new or +existing files. diff --git a/.agents/rules/workspace.md b/.agents/rules/workspace.md new file mode 100644 index 0000000000..b819925f83 --- /dev/null +++ b/.agents/rules/workspace.md @@ -0,0 +1,18 @@ +--- +trigger: always_on +--- + +# Workspace Rules + +To avoid confusion from using outdated code states, when starting a new +conversation/session or when first starting a new branch or worktree, unless +explicitly instructed otherwise, ensure the latest upstream code is used as the +basis: +* Fetch `upstream/main` (`git fetch upstream main`). +* Base any new branch or worktree upon `upstream/main` (e.g., + `git checkout -b upstream/main`). +* Run the workspace helper script to configure upstream tracking and safe + pushing: + ```bash + .agents/scripts/setup_triangle_branch.sh + ``` diff --git a/.agents/scripts/setup_triangle_branch.sh b/.agents/scripts/setup_triangle_branch.sh new file mode 100755 index 0000000000..b6bdc2bb20 --- /dev/null +++ b/.agents/scripts/setup_triangle_branch.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# Helper script to set up upstream tracking for fork-and-PR workflow. +set -e + +# Detect current branch if not provided as argument +BRANCH_NAME="${1:-$(git symbolic-ref --short HEAD)}" + +if [ -z "$BRANCH_NAME" ]; then + echo "Error: Could not detect current branch name." >&2 + exit 1 +fi + +echo "Setting up upstream tracking for branch: $BRANCH_NAME" + +# 1. Set the pull/fetch behavior to track the canonical repo's main branch +git config --local "branch.${BRANCH_NAME}.remote" upstream +git config --local "branch.${BRANCH_NAME}.merge" refs/heads/main + +# 2. Override the destination remote for pushing +git config --local "branch.${BRANCH_NAME}.pushRemote" origin + +# 3. Defend against global push.default settings (ensure it pushes to current branch name) +git config --local push.default current + +echo "Successfully configured upstream/main tracking and origin pushRemote for $BRANCH_NAME!" diff --git a/.agents/skills/analyze-ci-failure/SKILL.md b/.agents/skills/analyze-ci-failure/SKILL.md new file mode 100644 index 0000000000..2019c5b5f9 --- /dev/null +++ b/.agents/skills/analyze-ci-failure/SKILL.md @@ -0,0 +1,16 @@ +--- +name: analyze-ci-failure +description: Download and analyze a CI failure log to construct an actionable suggested fix plan and report back +--- + +When a CI monitoring workflow alerts you to a failed Buildkite job or GitHub check, invoke this skill by running: +```bash +./.agents/skills/analyze-ci-failure/scripts/analyze_ci_failure.py "" "" "" "" +``` + +### ✨ What this Skill Does +1. **Resolves Log**: Automatically resolves the Buildkite job download URL or locates existing local log artifacts. +2. **Downloads & Ingests**: Fetches the full raw CI log file and saves it locally. +3. **Smart Error Extraction**: Scans the log lines for critical failure signatures (`Traceback`, `ERROR:`, `FAILED:`, missing packages, compiler aborts). +4. **Fix Plan Synthesis**: Constructs a beautifully structured Markdown suggested plan on how to resolve the root cause. +5. **Natively Notifies**: Dispatches a high-priority summary notification message back to your active agent conversation via `agentapi send-message`! diff --git a/.agents/skills/analyze-ci-failure/scripts/analyze_ci_failure.py b/.agents/skills/analyze-ci-failure/scripts/analyze_ci_failure.py new file mode 100644 index 0000000000..3b975acfb3 --- /dev/null +++ b/.agents/skills/analyze-ci-failure/scripts/analyze_ci_failure.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 + +import argparse +import os +import re +import subprocess +import sys +import urllib.request + + +def fetch_log(build_id, job_id, output_path): + if build_id.startswith("http"): + log_url = build_id + elif job_id.startswith("http"): + log_url = job_id + else: + log_url = f"https://buildkite.com/organizations/bazel/pipelines/rules-python-python/builds/{build_id}/jobs/{job_id}/download.txt" + + if not log_url.endswith("/download.txt") and "buildkite.com" in log_url: + log_url = re.sub(r"/log$", "/download.txt", log_url) + + print(f"šŸ“„ Downloading CI failure log from {log_url}...") + req = urllib.request.Request(log_url, headers={"User-Agent": "ci-analyzer"}) + try: + with urllib.request.urlopen(req) as resp: + content = resp.read() + with open(output_path, "wb") as f: + f.write(content) + return True + except Exception as e: + print(f"āš ļø Failed to download log from {log_url}: {e}", file=sys.stderr) + with open(output_path, "w") as f: + f.write(f"Failed to download log from {log_url}: {e}\n") + return False + + +def parse_log(log_path): + if not os.path.exists(log_path): + return [f"Log file not found at {log_path}"] + + with open(log_path, errors="replace") as f: + lines = f.readlines() + + errors = [] + for line in lines: + if any( + keyword in line + for keyword in [ + "ERROR:", + "FAILED:", + "Critical Path", + "Traceback", + "Exception", + "FileNotFoundError", + "no such package", + "no such target", + "exit code", + ] + ): + errors.append(line.strip()) + + return errors[:30] + + +def create_plan(job_name, log_path, errors): + err_str = ( + "\n".join(errors) + if errors + else "No obvious keyword error lines matched. Please inspect the raw log file." + ) + + plan = f"""# 🚨 CI Failure Analysis Report: {job_name} + +## šŸ“ CI Log Path +`{log_path}` + +## šŸ”„ Extracted Failure Snippets +```text +{err_str} +``` + +## šŸ› ļø Suggested Plan to Fix +1. **Inspect Log**: Review the exact log snippets above or read the full raw log file at `{log_path}`. +2. **Reproduce Locally**: Run `./replicate_ci "{job_name}"` or the matching `bazel build/test` command locally. +3. **Apply Fix**: Resolve the root cause in the relevant `BUILD.bazel` or Starlark files. +4. **Verify & Push**: Run local verification with `--config=fast-tests` and push the updated branch to trigger a clean pipeline. +""" + return plan + + +def main(): + parser = argparse.ArgumentParser( + description="Download CI failure log, analyze root cause, and create fix plan." + ) + parser.add_argument("job_name", help="Name of the failed job") + parser.add_argument("build_id", help="Buildkite Build ID, Build number, or Log URL") + parser.add_argument("job_id", help="Buildkite Job ID or link") + parser.add_argument("conv_id", help="Conversation ID to report back to") + args = parser.parse_args() + + skill_dir = os.path.abspath(os.path.dirname(__file__)) + logs_dir = os.path.join(skill_dir, "ci_logs") + os.makedirs(logs_dir, exist_ok=True) + + safe_jname = re.sub(r"[^a-zA-Z0-9]", "_", args.job_name) + log_path = os.path.join(logs_dir, f"ci_{safe_jname}_{args.job_id}.log") + + fetch_log(args.build_id, args.job_id, log_path) + + print(f"šŸš€ Analyzing CI failure log for '{args.job_name}' at '{log_path}'...") + errors = parse_log(log_path) + plan = create_plan(args.job_name, log_path, errors) + + plan_file = os.path.join(logs_dir, f"ci_plan_{safe_jname}.md") + with open(plan_file, "w") as f: + f.write(plan) + + print( + f"šŸ“„ Plan generated at '{plan_file}'. Dispatching notification to conversation {args.conv_id}..." + ) + + msg = ( + f"āš ļø Remote CI Job '{args.job_name}' Analysis Complete!\n\n" + f"I downloaded and analyzed the failure log. Findings and suggested fix plan compiled at artifact file: `{plan_file}`.\n\n" + f"Raw CI Log Path: `{log_path}`" + ) + + res = subprocess.run( + [ + "agentapi", + "send-message", + "--title=CI Failure Analysis Plan", + args.conv_id, + msg, + ] + ) + if res.returncode != 0: + print(f"āŒ Failed to send agentapi message. Printing plan directly:\n{plan}") + + +if __name__ == "__main__": + main() diff --git a/.agents/skills/buildkite-get-results/SKILL.md b/.agents/skills/buildkite-get-results/SKILL.md new file mode 100644 index 0000000000..801e51c02d --- /dev/null +++ b/.agents/skills/buildkite-get-results/SKILL.md @@ -0,0 +1,10 @@ +--- +name: buildkite-get-results +description: Gets buildkite build results +--- + +Pass the PR number, Build URL, or Build ID to the `scripts/get_buildkite_results.py` script. + +The `--jobs` flag can do glob-style filtering of jobs. + +The `--download` flag will download job logs. diff --git a/.agents/skills/buildkite-get-results/scripts/get_buildkite_results.py b/.agents/skills/buildkite-get-results/scripts/get_buildkite_results.py new file mode 100755 index 0000000000..1af829a8e6 --- /dev/null +++ b/.agents/skills/buildkite-get-results/scripts/get_buildkite_results.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 + +import argparse +import json +import re +import subprocess +import sys + + +def check_cli(cmd_name, install_url): + try: + subprocess.run( + [cmd_name, "--version"], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + except Exception: + print( + f"āŒ Error: '{cmd_name}' CLI is not installed or not in PATH.", + file=sys.stderr, + ) + print(f"Please install it from {install_url}", file=sys.stderr) + sys.exit(1) + + +def get_build_url_from_pr(pr_number): + check_cli("gh", "https://cli.github.com/") + cmd = ["gh", "pr", "checks", str(pr_number), "--json", "name,link"] + try: + res = subprocess.run(cmd, capture_output=True, text=True, check=True) + checks = json.loads(res.stdout) + for c in checks: + link = c.get("link", "") + if "buildkite.com" in link: + return link.split("#")[0] + print(f"āŒ No Buildkite checks found for PR #{pr_number}.", file=sys.stderr) + sys.exit(1) + except subprocess.CalledProcessError as e: + print(f"āŒ Error fetching PR checks: {e.stderr}", file=sys.stderr) + sys.exit(1) + + +def normalize_build_target(target): + # Transforms https://buildkite.com/bazel/rules-python-python/builds/15707 + # into bazel/rules-python-python/15707 + m = re.search(r"buildkite\.com/([^/]+)/([^/]+)/builds/(\d+)", target) + if m: + return f"{m.group(1)}/{m.group(2)}/{m.group(3)}" + return target + + +def main(): + parser = argparse.ArgumentParser( + description="Gets Buildkite build results using the 'bk' CLI." + ) + parser.add_argument( + "pr", help="PR number, Build URL, or Build ID (org/pipeline/build)" + ) + parser.add_argument( + "--jobs", + help="Glob-style filtering of job names to display or download", + ) + parser.add_argument("--download", action="store_true", help="Download job logs") + args = parser.parse_args() + + check_cli("bk", "https://github.com/buildkite/cli") + + target = args.pr + if target.isdigit() and len(target) < 10: + print(f"šŸ” Inspecting PR #{target} via gh to find Buildkite URL...") + target = get_build_url_from_pr(target) + + build_id = normalize_build_target(target) + print(f"šŸš€ Querying Buildkite for build: {build_id}\n") + + # Run bk build view + res = subprocess.run(["bk", "build", "view", build_id]) + if res.returncode != 0: + print( + f"āŒ Failed to view build '{build_id}' via 'bk' CLI.", + file=sys.stderr, + ) + sys.exit(res.returncode) + + if args.download: + print(f"\nšŸ“„ Downloading logs for build: {build_id}") + dl_res = subprocess.run(["bk", "build", "download", build_id]) + if dl_res.returncode != 0: + print( + "āš ļø 'bk build download' failed or not supported. Try using 'bk job log ' for specific jobs." + ) + + +if __name__ == "__main__": + main() diff --git a/.agents/skills/buildkite-retry-job/SKILL.md b/.agents/skills/buildkite-retry-job/SKILL.md new file mode 100644 index 0000000000..3f43846da9 --- /dev/null +++ b/.agents/skills/buildkite-retry-job/SKILL.md @@ -0,0 +1,17 @@ +--- +name: buildkite-retry-job +description: Retry a failed build kite job +--- + +Use `scripts/retry_buildkite_jobs.py` to retry a job. This is best used +when there are network failures. + + +example: + +``` +retry_buildkite_jobs.py org pipeline build +``` +You can also simply pass a PR number or a direct Buildkite build URL. + +The `--jobs` flag can be used to retry specific jobs. diff --git a/.agents/skills/buildkite-retry-job/scripts/retry_buildkite_jobs.py b/.agents/skills/buildkite-retry-job/scripts/retry_buildkite_jobs.py new file mode 100755 index 0000000000..d501ce8f14 --- /dev/null +++ b/.agents/skills/buildkite-retry-job/scripts/retry_buildkite_jobs.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 + +import argparse +import json +import re +import subprocess +import sys + + +def check_cli(cmd_name, install_url): + try: + subprocess.run( + [cmd_name, "--version"], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + except Exception: + print( + f"āŒ Error: '{cmd_name}' CLI is not installed or not in PATH.", + file=sys.stderr, + ) + print(f"Please install it from {install_url}", file=sys.stderr) + sys.exit(1) + + +def get_build_url_from_pr(pr_number): + check_cli("gh", "https://cli.github.com/") + cmd = ["gh", "pr", "checks", str(pr_number), "--json", "name,link"] + try: + res = subprocess.run(cmd, capture_output=True, text=True, check=True) + checks = json.loads(res.stdout) + for c in checks: + link = c.get("link", "") + if "buildkite.com" in link: + return link.split("#")[0] + print(f"āŒ No Buildkite checks found for PR #{pr_number}.", file=sys.stderr) + sys.exit(1) + except subprocess.CalledProcessError as e: + print(f"āŒ Error fetching PR checks: {e.stderr}", file=sys.stderr) + sys.exit(1) + + +def normalize_build_target(target): + # Transforms https://buildkite.com/bazel/rules-python-python/builds/15707 + # into bazel/rules-python-python/15707 + m = re.search(r"buildkite\.com/([^/]+)/([^/]+)/builds/(\d+)", target) + if m: + return f"{m.group(1)}/{m.group(2)}/{m.group(3)}" + return target + + +def main(): + parser = argparse.ArgumentParser( + description="Retry failed Buildkite jobs using the 'bk' CLI." + ) + parser.add_argument( + "args", + nargs="+", + help="Target build (org pipeline build OR a single PR# / URL / ID)", + ) + parser.add_argument( + "--jobs", + "--job-name", + dest="job_name", + help="Specific job name or pattern to retry", + ) + args = parser.parse_args() + + check_cli("bk", "https://github.com/buildkite/cli") + + if len(args.args) == 3: + target = f"{args.args[0]}/{args.args[1]}/{args.args[2]}" + elif len(args.args) == 1: + target = args.args[0] + else: + print( + "āŒ Error: Invalid arguments. Provide either 'org pipeline build' or a single target (PR#, URL, or org/pipeline/build).", + file=sys.stderr, + ) + sys.exit(1) + + if target.isdigit() and len(target) < 10: + print(f"šŸ” Inspecting PR #{target} via gh to find Buildkite URL...") + target = get_build_url_from_pr(target) + + build_id = normalize_build_target(target) + + if args.job_name: + print(f"šŸš€ Retrying jobs matching '{args.job_name}' in build: {build_id}") + res = subprocess.run(["bk", "build", "retry", build_id, "--failed"]) + else: + print(f"šŸš€ Retrying all failed jobs in build: {build_id}") + res = subprocess.run(["bk", "build", "retry", build_id, "--failed"]) + + if res.returncode != 0: + print( + f"āŒ Failed to retry build '{build_id}' via 'bk' CLI.", + file=sys.stderr, + ) + sys.exit(res.returncode) + + print(f"šŸŽ‰ Successfully triggered retry for build: {build_id}") + + +if __name__ == "__main__": + main() diff --git a/.agents/skills/monitor-ci-results/SKILL.md b/.agents/skills/monitor-ci-results/SKILL.md new file mode 100644 index 0000000000..8f6429d7ce --- /dev/null +++ b/.agents/skills/monitor-ci-results/SKILL.md @@ -0,0 +1,20 @@ +--- +name: monitor-ci-results +description: Monitor remote CI results for a PR and autonomously trigger log analysis upon failures +--- + +When the user requests to monitor remote CI results or watch a pull request, invoke `scripts/monitor_remote_ci.py `. + +This long-running monitoring service runs in the background and continuously polls both GitHub PR checks and Buildkite workflow executions. + +### ✨ Autonomous Failure Orchestration +When any CI job completes with errors or returns a non-zero exit code: +1. It automatically downloads the raw CI log file to `ci_logs/`. +2. It launches an independent background analyzer script (`analyze_ci_failure.py`). +3. It authors a beautifully structured Markdown suggested plan for how to fix the failure. +4. It natively dispatches a high-priority notification message back to your active agent conversation (containing the downloaded log path and fix plan) using `agentapi send-message`! + +### Example Invocation +```bash +./scripts/monitor_remote_ci.py 3812 "0be435bd-96aa-4e1b-9c6f-727b31e80fa0" & +``` diff --git a/.agents/skills/monitor-ci-results/scripts/monitor_remote_ci.py b/.agents/skills/monitor-ci-results/scripts/monitor_remote_ci.py new file mode 100755 index 0000000000..fc1f8955d3 --- /dev/null +++ b/.agents/skills/monitor-ci-results/scripts/monitor_remote_ci.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 + +import argparse +import json +import os +import subprocess +import sys +import time +import urllib.request + + +def check_cli(cmd_name): + try: + subprocess.run( + [cmd_name, "--version"], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + return True + except Exception: + return False + + +def get_pr_checks(pr_number): + if not check_cli("gh"): + print("āŒ 'gh' CLI not installed.", file=sys.stderr) + return [] + cmd = ["gh", "pr", "checks", str(pr_number), "--json", "name,link,state"] + try: + res = subprocess.run(cmd, capture_output=True, text=True) + out = res.stdout + json_str = out[out.find("[") : out.rfind("]") + 1] if "[" in out else "[]" + return json.loads(json_str) + except Exception as e: + print(f"āš ļø Error fetching PR checks: {e}", file=sys.stderr) + return [] + + +def get_buildkite_jobs(build_url): + base_url = build_url.split("#")[0] + if base_url.endswith(".json"): + base_url = base_url[:-5] + + jobs_url = f"{base_url}/data/jobs.json" + req = urllib.request.Request(jobs_url, headers={"User-Agent": "ci-monitor"}) + try: + with urllib.request.urlopen(req) as resp: + data = json.loads(resp.read().decode()) + if isinstance(data, list): + return data + elif isinstance(data, dict) and "records" in data: + return data["records"] + except Exception as e: + print( + f"āš ļø Could not fetch Buildkite jobs from {jobs_url}: {e}", + file=sys.stderr, + ) + return [] + + +def main(): + parser = argparse.ArgumentParser( + description="Monitor remote CI for failures and trigger analysis." + ) + parser.add_argument("pr", help="PR number to monitor") + parser.add_argument("conv_id", help="Conversation ID to report back to") + parser.add_argument( + "--interval", + type=int, + default=60, + help="Monitoring polling interval in seconds", + ) + parser.add_argument( + "--max-iterations", + type=int, + default=120, + help="Maximum number of polling cycles", + ) + args = parser.parse_args() + + skill_dir = os.path.abspath(os.path.dirname(__file__)) + + state_file = os.path.join(skill_dir, f"monitored_state_pr_{args.pr}.json") + monitored = {} + if os.path.exists(state_file): + try: + with open(state_file) as f: + monitored = json.load(f) + except Exception: + pass + + print( + f"šŸš€ Starting continuous remote CI monitoring for PR #{args.pr} every {args.interval}s..." + ) + + for i in range(args.max_iterations): + print( + f"šŸ” [Cycle {i + 1}/{args.max_iterations}] Polling GitHub PR #{args.pr} checks..." + ) + checks = get_pr_checks(args.pr) + + for check in checks: + name = check.get("name", "unknown") + state = check.get("state", "UNKNOWN") + link = check.get("link", "") + + if "buildkite" in name.lower() and link: + jobs = get_buildkite_jobs(link) + + passed = 0 + failed = 0 + running = 0 + other = 0 + + for job in jobs: + jstate = job.get("state", "unknown") + exit_status = job.get("exit_status") + is_soft_failed = job.get("soft_failed") is True + is_failed = ( + jstate in ["failed", "failing"] + or (exit_status != 0 and exit_status is not None) + ) and not is_soft_failed + is_passed = ( + jstate in ["passed", "success"] + or (jstate == "finished" and exit_status == 0) + or is_soft_failed + ) + is_running = jstate in ["running", "scheduled"] + + if is_failed: + failed += 1 + elif is_passed: + passed += 1 + elif is_running: + running += 1 + else: + other += 1 + + build_id = link.split("/")[-1].split("#")[0] + print( + f"Buildkite #{build_id}: {len(jobs)} total jobs " + f"(Passed: {passed}, Failed: {failed}, Running: {running}, Other: {other})" + ) + + for job in jobs: + jname = job.get("name", "unknown_job") + jstate = job.get("state", "unknown") + jid = job.get("id", "") + jkey = f"bk_{jid}" + + exit_status = job.get("exit_status") + is_soft_failed = job.get("soft_failed") is True + is_failed = ( + jstate in ["failed", "failing"] + or (exit_status != 0 and exit_status is not None) + ) and not is_soft_failed + + if is_failed and jkey not in monitored: + print( + f"🚨 Notifying failure for Buildkite job '{jname}' (ID: {jid})..." + ) + msg = ( + f"āš ļø Remote CI Buildkite Job '{jname}' completed with errors!\n\n" + f"Build ID: {build_id} | Job ID: {jid}\n" + f"Log URL: {job.get('log_url', link)}\n\n" + f"Start subagent: run analyze-ci-failure skill on this failure" + ) + subprocess.run( + [ + "agentapi", + "send-message", + "--title=CI Job Failed", + args.conv_id, + msg, + ] + ) + monitored[jkey] = time.time() + with open(state_file, "w") as f: + json.dump(monitored, f) + + elif state in ["FAILURE", "failed"] and name not in monitored: + print(f"🚨 Notifying failure for GitHub check '{name}'...") + msg = ( + f"āš ļø Remote CI GitHub Check '{name}' completed with errors!\n\n" + f"Link: {link}\n\n" + f"Start subagent: run analyze-ci-failure skill on this failure" + ) + subprocess.run( + [ + "agentapi", + "send-message", + "--title=CI Check Failed", + args.conv_id, + msg, + ] + ) + monitored[name] = time.time() + with open(state_file, "w") as f: + json.dump(monitored, f) + + time.sleep(args.interval) + + print("šŸ CI monitoring service completed its scheduled iterations.") + + +if __name__ == "__main__": + main() diff --git a/.agents/skills/rule-creator/SKILL.md b/.agents/skills/rule-creator/SKILL.md new file mode 100644 index 0000000000..9ed7978120 --- /dev/null +++ b/.agents/skills/rule-creator/SKILL.md @@ -0,0 +1,56 @@ +--- +name: rule-creator +description: Create and format agent rules with proper front matter in the workspace +--- + +Use this skill when you need to create a new rule for the agent in the +workspace. + +### Rule File Location + +All workspace-specific rules must be created as individual Markdown files under +the `.agents/rules/` directory: +``` +.agents/rules/.md +``` + +### Rule Format + +Every rule file must start with a YAML front matter block defining the trigger +condition, followed by the rule content in Markdown. + +```yaml +--- +trigger: +--- + +# + + +``` + +#### Trigger Conditions +* `always_on`: The rule is always active and must be followed for all tasks. +* Custom triggers: You can specify other trigger conditions if the rule only + applies in certain contexts. + +### Formatting Guidelines +* **Line Wrapping:** Always wrap all text in the rule file (including the + title and description) to **80 columns** to ensure readability and + compatibility. +* **Clarity:** Write clear, actionable directives. + +### Example + +To create a rule that prevents adding copyrights: + +```markdown +--- +trigger: always_on +--- + +# No Copyrights Rule + +Unless directed by the user otherwise, do not add Bazel copyright to new or +existing files. +``` diff --git a/.bazelci/isolate.sh b/.bazelci/isolate.sh new file mode 100755 index 0000000000..32a1de3fcf --- /dev/null +++ b/.bazelci/isolate.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail + +module_dir="${1:-}" + +if [[ -z "${module_dir}" ]]; then + echo "Usage: $0 " + exit 1 +fi + +# Find the repository root assuming this script is in .bazelci/ +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(dirname "${script_dir}")" + +cd "${repo_root}" + +if [[ ! -f "MODULE.bazel" ]]; then + echo "Error: MODULE.bazel not found in ${repo_root}. Are you sure this is the repo root?" + exit 1 +fi + +if [[ ! -d "${module_dir}" ]]; then + echo "Error: Module directory '${module_dir}' not found in ${repo_root}." + exit 1 +fi + +echo "Removing files outside of ${module_dir} to simulate BCR environment..." +find . -maxdepth 1 -mindepth 1 \ + ! -name "${module_dir}" \ + ! -name ".git" \ + ! -name ".bazelci" \ + -exec rm -rf '{}' + diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml index 119ad498b0..b3e308c7c7 100644 --- a/.bazelci/presubmit.yml +++ b/.bazelci/presubmit.yml @@ -16,7 +16,7 @@ buildifier: # keep these arguments in sync with .pre-commit-config.yaml # Use a specific version to avoid skew issues when new versions are released. - version: 6.1.0 + version: 8.2.1 warnings: "all" # NOTE: Minimum supported version is 7.x .minimum_supported_version: &minimum_supported_version @@ -25,15 +25,21 @@ buildifier: bazel: 7.x skip_in_bazel_downstream_pipeline: "Bazel 7 required" .reusable_config: &reusable_config + # USE_BAZEL_DIFF is disabled because target_compatible_with is used heavily, + # but BazelCI's bazel-diff integration errors when handling them. + # environment: + # USE_BAZEL_DIFF: "1" build_targets: - "--" - "..." # As a regression test for #225, check that wheel targets still build when # their package path is qualified with the repo name. - "@rules_python//examples/wheel/..." - build_flags: + build_flags: &reusable_config_build_flags + - "--experimental_repository_cache_hardlinks=false" - "--keep_going" - "--build_tag_filters=-integration-test" + - "--verbose_failures" test_targets: - "--" - "..." @@ -41,6 +47,7 @@ buildifier: - "--test_tag_filters=-integration-test" .common_workspace_flags_min_bazel: &common_workspace_flags_min_bazel build_flags: + - "--experimental_repository_cache_hardlinks=false" - "--noenable_bzlmod" - "--build_tag_filters=-integration-test" test_flags: @@ -77,17 +84,83 @@ buildifier: coverage_targets: ["..."] .coverage_targets_example_bzlmod_build_file_generation: &coverage_targets_example_bzlmod_build_file_generation coverage_targets: ["//:bzlmod_build_file_generation_test"] +.coverage_targets_bootstrap: &coverage_targets_bootstrap + coverage_targets: + - //tests/bootstrap_impls:stdlib_shadowing_system_python_test .coverage_targets_example_multi_python: &coverage_targets_example_multi_python coverage_targets: - //tests:my_lib_3_10_test - //tests:my_lib_3_11_test - - //tests:my_lib_3_9_test + - //tests:my_lib_3_12_test + - //tests:my_lib_3_13_test + - //tests:my_lib_3_14_test - //tests:my_lib_default_test - //tests:version_3_10_test - //tests:version_3_11_test - - //tests:version_3_9_test + - //tests:version_3_12_test + - //tests:version_3_13_test + - //tests:version_3_14_test - //tests:version_default_test + +# Keep in sync with .bcr/gazelle/presubmit.yml +.gazelle_common_bcr: &gazelle_common_bcr + bazel: ${{ bazel }} + working_directory: gazelle/examples/bzlmod_build_file_generation + shell_commands: + - "echo 'common --override_module=rules_python=' >> .bazelrc" + - "bazel run //:gazelle_python_manifest.update" + - "bazel run //:gazelle -- update" + batch_commands: + - "echo common --override_module=rules_python= >> .bazelrc " + - " bazel run //:gazelle_python_manifest.update " + - " bazel run //:gazelle -- update" + build_targets: + - "//..." + - ":modules_map" + test_targets: + - "//..." + + +matrix: + # Keep in sync with .bcr/presubmit.yml + platform: + - ubuntu2204 + - debian11 + - macos_arm64 + - windows + # Keep in sync with .bcr/presubmit.yml + bazel: [7.x, 8.x, 9.x] + tasks: + # Keep in sync with .bcr/presubmit.yml + bcr_test: + name: "BCR: Bazel {bazel}" + platform: ${{ platform }} + working_directory: examples/bzlmod + bazel: ${{ bazel }} + build_flags: + - "--keep_going" + test_flags: + - "--keep_going" + build_targets: + - "//..." + test_targets: + - "//..." + + # Keep in sync with .bcr/gazelle/presubmit.yml + gazelle_bcr_ubuntu: + <<: *gazelle_common_bcr + name: "Gazelle: BCR, Bazel {bazel}" + platform: ubuntu2204 + gazelle_bcr_debian11: + <<: *gazelle_common_bcr + name: "Gazelle: BCR, Bazel {bazel}" + platform: debian11 + gazelle_bcr_macos_arm64: + <<: *gazelle_common_bcr + name: "Gazelle: BCR, Bazel {bazel}" + platform: macos_arm64 + gazelle_extension_min: <<: *common_workspace_flags_min_bazel <<: *minimum_supported_version @@ -110,6 +183,27 @@ tasks: test_targets: ["//..."] working_directory: gazelle + sphinxdocs_ubuntu: + name: "Sphinxdocs: Ubuntu" + working_directory: "sphinxdocs" + platform: ubuntu2204 + shell_commands: + - "../.bazelci/isolate.sh sphinxdocs" + build_targets: + - "//..." + test_targets: + - "//..." + sphinxdocs_mac: + name: "Sphinxdocs: Mac" + working_directory: "sphinxdocs" + platform: macos_arm64 + shell_commands: + - "../.bazelci/isolate.sh sphinxdocs" + build_targets: + - "//..." + test_targets: + - "//..." + ubuntu_min_workspace: <<: *minimum_supported_version <<: *reusable_config @@ -125,13 +219,20 @@ tasks: bazel: 7.x ubuntu: <<: *reusable_config - name: "Default: Ubuntu" + <<: *coverage_targets_bootstrap + name: "Default: Ubuntu, Bazel {bazel}" platform: ubuntu2204 + bazel: ${{ bazel }} ubuntu_upcoming: <<: *reusable_config name: "Default: Ubuntu, upcoming Bazel" platform: ubuntu2204 bazel: last_rc + # This is an advisory job; doesn't block merges + # RCs may have regressions, so don't fail our CI on them + soft_fail: + - exit_status: 1 + - exit_status: 3 ubuntu_rolling: name: "Default: Ubuntu, rolling Bazel" platform: ubuntu2204 @@ -155,7 +256,7 @@ tasks: <<: *reusable_config <<: *common_workspace_flags name: "Default: Mac, workspace" - platform: macos + platform: macos_arm64 windows_workspace: <<: *reusable_config <<: *common_workspace_flags @@ -184,24 +285,28 @@ tasks: debian: <<: *reusable_config - name: "Default: Debian" + name: "Default: Debian, Bazel {bazel}" platform: debian11 - macos: + bazel: ${{ bazel }} + macos_arm64: <<: *reusable_config - name: "Default: MacOS" - platform: macos + name: "Default: MacOS, Bazel {bazel}" + platform: macos_arm64 + bazel: ${{ bazel }} windows: <<: *reusable_config - name: "Default: Windows" + name: "Default: Windows, Bazel {bazel}" platform: windows + bazel: ${{ bazel }} test_flags: - "--test_tag_filters=-integration-test,-fix-windows" rbe_min: <<: *minimum_supported_version <<: *reusable_config name: "RBE: Ubuntu, minimum Bazel" - platform: rbe_ubuntu2204 + platform: rbe_ubuntu2404 build_flags: + - "--experimental_repository_cache_hardlinks=false" # BazelCI sets --action_env=BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1, # which prevents cc toolchain autodetection from working correctly # on Bazel 5.4 and earlier. To workaround this, manually specify the @@ -218,10 +323,10 @@ tasks: rbe: <<: *reusable_config name: "RBE: Ubuntu" - platform: rbe_ubuntu2204 + platform: rbe_ubuntu2404 # TODO @aignas 2024-12-11: get the RBE working in CI for bazel 8.0 # See https://github.com/bazelbuild/rules_python/issues/2499 - bazel: 7.x + bazel: 8.x test_flags: - "--test_tag_filters=-integration-test,-acceptance-test" - "--extra_toolchains=@buildkite_config//config:cc-toolchain" @@ -250,7 +355,7 @@ tasks: <<: *common_workspace_flags name: "examples/build_file_generation: macOS, workspace" working_directory: examples/build_file_generation - platform: macos + platform: macos_arm64 integration_test_build_file_generation_windows_workspace: <<: *reusable_build_test_all <<: *common_workspace_flags @@ -258,113 +363,15 @@ tasks: working_directory: examples/build_file_generation platform: windows - integration_test_bzlmod_ubuntu_min: - <<: *minimum_supported_version - <<: *reusable_build_test_all - coverage_targets: ["//:test"] - name: "examples/bzlmod: Ubuntu, minimum Bazel" - working_directory: examples/bzlmod - platform: ubuntu2204 - bazel: 7.x - integration_test_bzlmod_ubuntu: - <<: *reusable_build_test_all - <<: *coverage_targets_example_bzlmod - name: "examples/bzlmod: Ubuntu" - working_directory: examples/bzlmod - platform: ubuntu2204 - bazel: 7.x - integration_test_bzlmod_ubuntu_upcoming: - <<: *reusable_build_test_all - <<: *coverage_targets_example_bzlmod - name: "examples/bzlmod: Ubuntu, upcoming Bazel" - working_directory: examples/bzlmod - platform: ubuntu2204 - bazel: last_rc - integration_test_bzlmod_debian: - <<: *reusable_build_test_all - <<: *coverage_targets_example_bzlmod - name: "examples/bzlmod: Debian" - working_directory: examples/bzlmod - platform: debian11 - bazel: 7.x integration_test_bzlmod_ubuntu_vendor: <<: *reusable_build_test_all name: "examples/bzlmod: bazel vendor" working_directory: examples/bzlmod platform: ubuntu2204 shell_commands: - - "bazel vendor --vendor_dir=./vendor //..." - - "bazel build --vendor_dir=./vendor //..." - - "rm -rf ./vendor" - integration_test_bzlmod_macos: - <<: *reusable_build_test_all - <<: *coverage_targets_example_bzlmod - name: "examples/bzlmod: macOS" - working_directory: examples/bzlmod - platform: macos - bazel: 7.x - integration_test_bzlmod_macos_upcoming: - <<: *reusable_build_test_all - <<: *coverage_targets_example_bzlmod - name: "examples/bzlmod: macOS, upcoming Bazel" - working_directory: examples/bzlmod - platform: macos - bazel: last_rc - integration_test_bzlmod_windows: - <<: *reusable_build_test_all - # coverage is not supported on Windows - name: "examples/bzlmod: Windows" - working_directory: examples/bzlmod - platform: windows - bazel: 7.x - integration_test_bzlmod_windows_upcoming: - <<: *reusable_build_test_all - # coverage is not supported on Windows - name: "examples/bzlmod: Windows, upcoming Bazel" - working_directory: examples/bzlmod - platform: windows - bazel: last_rc - - integration_test_bzlmod_generate_build_file_generation_ubuntu_min: - <<: *minimum_supported_version - <<: *reusable_build_test_all - <<: *coverage_targets_example_bzlmod_build_file_generation - name: "examples/bzlmod_build_file_generation: Ubuntu, minimum Bazel" - working_directory: examples/bzlmod_build_file_generation - platform: ubuntu2204 - bazel: 7.x - integration_test_bzlmod_generation_build_files_ubuntu: - <<: *reusable_build_test_all - <<: *coverage_targets_example_bzlmod_build_file_generation - name: "examples/bzlmod_build_file_generation: Ubuntu" - working_directory: examples/bzlmod_build_file_generation - platform: ubuntu2204 - integration_test_bzlmod_generation_build_files_ubuntu_run: - <<: *reusable_build_test_all - name: "examples/bzlmod_build_file_generation: Ubuntu, Gazelle and pip" - working_directory: examples/bzlmod_build_file_generation - platform: ubuntu2204 - shell_commands: - - "bazel run //:gazelle_python_manifest.update" - - "bazel run //:gazelle -- update" - integration_test_bzlmod_build_file_generation_debian: - <<: *reusable_build_test_all - <<: *coverage_targets_example_bzlmod_build_file_generation - name: "examples/bzlmod_build_file_generation: Debian" - working_directory: examples/bzlmod_build_file_generation - platform: debian11 - integration_test_bzlmod_build_file_generation_macos: - <<: *reusable_build_test_all - <<: *coverage_targets_example_bzlmod_build_file_generation - name: "examples/bzlmod_build_file_generation: MacOS" - working_directory: examples/bzlmod_build_file_generation - platform: macos - integration_test_bzlmod_build_file_generation_windows: - <<: *reusable_build_test_all - # coverage is not supported on Windows - name: "examples/bzlmod_build_file_generation: Windows" - working_directory: examples/bzlmod_build_file_generation - platform: windows + - "bazel vendor --vendor_dir=./vendor //..." + - "bazel build --vendor_dir=./vendor //..." + - "rm -rf ./vendor" integration_test_multi_python_versions_ubuntu_workspace: <<: *reusable_build_test_all @@ -386,7 +393,7 @@ tasks: <<: *coverage_targets_example_multi_python name: "examples/multi_python_versions: MacOS, workspace" working_directory: examples/multi_python_versions - platform: macos + platform: macos_arm64 integration_test_multi_python_versions_windows_workspace: <<: *reusable_build_test_all <<: *common_workspace_flags @@ -423,7 +430,7 @@ tasks: <<: *reusable_build_test_all name: "examples/pip_parse: MacOS" working_directory: examples/pip_parse - platform: macos + platform: macos_arm64 integration_test_pip_parse_windows: <<: *reusable_build_test_all name: "examples/pip_parse: Windows" @@ -454,62 +461,10 @@ tasks: <<: *common_workspace_flags name: "examples/pip_parse_vendored: MacOS" working_directory: examples/pip_parse_vendored - platform: macos + platform: macos_arm64 # We don't run pip_parse_vendored under Windows as the file checked in is # generated from a repository rule containing OS-specific rendered paths. - # The proto example is workspace-only; bzlmod functionality is covered - # by examples/bzlmod/py_proto_library - integration_test_py_proto_library_ubuntu_workspace: - <<: *reusable_build_test_all - <<: *common_workspace_flags - name: "examples/py_proto_library: Ubuntu, workspace" - working_directory: examples/py_proto_library - platform: ubuntu2204 - integration_test_py_proto_library_debian_workspace: - <<: *reusable_build_test_all - <<: *common_workspace_flags - name: "examples/py_proto_library: Debian, workspace" - working_directory: examples/py_proto_library - platform: debian11 - integration_test_py_proto_library_macos_workspace: - <<: *reusable_build_test_all - <<: *common_workspace_flags - name: "examples/py_proto_library: MacOS, workspace" - working_directory: examples/py_proto_library - platform: macos - integration_test_py_proto_library_windows_workspace: - <<: *reusable_build_test_all - <<: *common_workspace_flags - name: "examples/py_proto_library: Windows, workspace" - working_directory: examples/py_proto_library - platform: windows - - integration_test_pip_repository_annotations_ubuntu_workspace: - <<: *reusable_build_test_all - <<: *common_workspace_flags - name: "examples/pip_repository_annotations: Ubuntu, workspace" - working_directory: examples/pip_repository_annotations - platform: ubuntu2204 - integration_test_pip_repository_annotations_debian_workspace: - <<: *reusable_build_test_all - <<: *common_workspace_flags - name: "examples/pip_repository_annotations: Debian, workspace" - working_directory: examples/pip_repository_annotations - platform: debian11 - integration_test_pip_repository_annotations_macos_workspace: - <<: *reusable_build_test_all - <<: *common_workspace_flags - name: "examples/pip_repository_annotations: macOS, workspace" - working_directory: examples/pip_repository_annotations - platform: macos - integration_test_pip_repository_annotations_windows_workspace: - <<: *reusable_build_test_all - <<: *common_workspace_flags - name: "examples/pip_repository_annotations: Windows, workspace" - working_directory: examples/pip_repository_annotations - platform: windows - integration_test_bazelinbazel_ubuntu: <<: *common_bazelinbazel_config name: "tests/integration bazel-in-bazel: Ubuntu" @@ -524,9 +479,9 @@ tasks: integration_test_bazelinbazel_macos: <<: *common_bazelinbazel_config name: "tests/integration bazel-in-bazel: macOS (subset)" - platform: macos - build_targets: ["//tests/integration:local_toolchains_test_bazel_self"] - test_targets: ["//tests/integration:local_toolchains_test_bazel_self"] + platform: macos_arm64 + build_targets: ["//tests/integration:subset"] + test_targets: ["//tests/integration:subset"] # The bazelinbazel tests were disabled on Windows to save CI jobs slots, and # have bitrotted a bit. For now, just run a subset of what we're most # interested in. @@ -534,8 +489,8 @@ tasks: <<: *common_bazelinbazel_config name: "tests/integration bazel-in-bazel: Windows (subset)" platform: windows - build_targets: ["//tests/integration:local_toolchains_test_bazel_self"] - test_targets: ["//tests/integration:local_toolchains_test_bazel_self"] + build_targets: ["//tests/integration:subset"] + test_targets: ["//tests/integration:subset"] integration_test_compile_pip_requirements_ubuntu: <<: *reusable_build_test_all @@ -577,7 +532,7 @@ tasks: <<: *reusable_build_test_all name: "compile_pip_requirements: MacOS" working_directory: tests/integration/compile_pip_requirements - platform: macos + platform: macos_arm64 shell_commands: # Make a change to the locked requirements and then assert that //:requirements.update does the # right thing. @@ -610,20 +565,6 @@ tasks: - "bazel run //:os_specific_requirements.update" - "git diff --exit-code" - - integration_test_ignore_root_user_error_macos_workspace: - <<: *reusable_build_test_all - <<: *common_workspace_flags - name: "ignore_root_user_error: macOS, workspace" - working_directory: tests/integration/ignore_root_user_error - platform: macos - integration_test_ignore_root_user_error_windows_workspace: - <<: *reusable_build_test_all - <<: *common_workspace_flags - name: "ignore_root_user_error: Windows, workspace" - working_directory: tests/integration/ignore_root_user_error - platform: windows - integration_compile_pip_requirements_test_from_external_repo_ubuntu_min_workspace: <<: *minimum_supported_version <<: *common_workspace_flags_min_bazel @@ -659,7 +600,7 @@ tasks: integration_compile_pip_requirements_test_from_external_repo_macos: name: "compile_pip_requirements_test_from_external_repo: macOS" working_directory: tests/integration/compile_pip_requirements_test_from_external_repo - platform: macos + platform: macos_arm64 shell_commands: # Assert that @compile_pip_requirements//:requirements_test does the right thing. - "bazel test @compile_pip_requirements//..." diff --git a/.bazelignore b/.bazelignore index fb999097f5..88a4c32056 100644 --- a/.bazelignore +++ b/.bazelignore @@ -19,14 +19,22 @@ examples/bzlmod/other_module/bazel-other_module examples/bzlmod/other_module/bazel-out examples/bzlmod/other_module/bazel-testlogs examples/bzlmod/py_proto_library/foo_external -examples/bzlmod_build_file_generation/bazel-bzlmod_build_file_generation examples/multi_python_versions/bazel-multi_python_versions examples/pip_parse/bazel-pip_parse examples/pip_parse_vendored/bazel-pip_parse_vendored examples/pip_repository_annotations/bazel-pip_repository_annotations examples/py_proto_library/bazel-py_proto_library gazelle/bazel-gazelle +gazelle/examples/bzlmod_build_file_generation/bazel-bin +gazelle/examples/bzlmod_build_file_generation/bazel-bzlmod_build_file_generation +gazelle/examples/bzlmod_build_file_generation/bazel-out +gazelle/examples/bzlmod_build_file_generation/bazel-testlog +sphinxdocs +tests/integration/bzlmod_lockfile/bazel-bzlmod_lockfile tests/integration/compile_pip_requirements/bazel-compile_pip_requirements -tests/integration/ignore_root_user_error/bazel-ignore_root_user_error 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/unified_pypi/bazel-unified_pypi +tests/integration/uv_lock/bazel-uv_lock +tests/integration/validate_test_main/bazel-module_under_test diff --git a/.bazelrc b/.bazelrc index d7e1771336..044990fdb1 100644 --- a/.bazelrc +++ b/.bazelrc @@ -2,10 +2,12 @@ # Trick bazel into treating BUILD files under examples/* as being regular files # This lets us glob() up all the files inside the examples to make them inputs to tests # (Note, we cannot use `common --deleted_packages` because the bazel version command doesn't support it) -# To update these lines, execute -# `bazel run @rules_bazel_integration_test//tools:update_deleted_packages` -build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,rules_python-repro,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/another_module,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma,tests/modules/other/nspkg_single,tests/modules/other/simple_v1,tests/modules/other/simple_v2,tests/modules/other/with_external_data,tests/whl_with_build_files/testdata,tests/whl_with_build_files/testdata/somepkg,tests/whl_with_build_files/testdata/somepkg-1.0.dist-info,tests/whl_with_build_files/testdata/somepkg/subpkg -query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,rules_python-repro,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/another_module,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma,tests/modules/other/nspkg_single,tests/modules/other/simple_v1,tests/modules/other/simple_v2,tests/modules/other/with_external_data,tests/whl_with_build_files/testdata,tests/whl_with_build_files/testdata/somepkg,tests/whl_with_build_files/testdata/somepkg-1.0.dist-info,tests/whl_with_build_files/testdata/somepkg/subpkg +# To update the file, execute +import %workspace%/.bazelrc.deleted_packages + +# Symlinks are used extensively because of runfiles, venvs, and other reasons. +# Supporting otherwise is very complex with little benefit. +startup --windows_enable_symlinks test --test_output=errors @@ -16,9 +18,23 @@ test --test_output=errors # creating (possibly empty) __init__.py files and adding them to the srcs of # Python targets as required. build --incompatible_default_to_explicit_init_py +build --//python/config_settings:incompatible_default_to_explicit_init_py=True # Ensure ongoing compatibility with this flag. common --incompatible_disallow_struct_provider_syntax +# Makes Bazel 7 act more like Bazel 8 +common --incompatible_use_plus_in_repo_names + +# Implicitly adds --config=linux|macos|windows, depending on the host platform +common --enable_platform_specific_config + +# Needed to make Windows with a py_binary in data deps work. The Bazel launcher +# is used, which falls back to finding python.exe on PATH to bootstrap. +# See https://github.com/bazel-contrib/rules_python/issues/3655 +common --incompatible_strict_action_env=false + +# To work around bug on bazel 7 +common:ci --experimental_repository_cache_hardlinks=false # Windows makes use of runfiles for some rules build --enable_runfiles @@ -26,6 +42,15 @@ build --enable_runfiles # Make Bazel 7 use bzlmod by default common --enable_bzlmod +# Local disk cache greatly speeds up builds if the regular cache is lost +common --disk_cache=~/.cache/bazel/bazel-disk-cache +# Drop `experimental_` prefix once Bazel 7 is no longer supported +common --experimental_downloader_config=downloader_config.cfg +common --http_timeout_scaling=10.0 +common --experimental_repository_downloader_retries=10 + + + # Additional config to use for readthedocs builds. # See .readthedocs.yml for additional flags that can only be determined from # the runtime environment. @@ -34,5 +59,24 @@ build:rtd --stamp build:rtd --enable_bzlmod common --incompatible_python_disallow_native_rules +common --incompatible_no_implicit_file_export build --lockfile_mode=update + +# Silence spammy C++ compile warnings +build:linux --copt=-Wno-deprecated-declarations +build:linux --copt=-Wno-stringop-overread +build:linux --copt=-Wno-sign-compare +build:linux --host_copt=-Wno-deprecated-declarations +build:linux --host_copt=-Wno-stringop-overread +build:linux --host_copt=-Wno-sign-compare +build:macos --copt=-Wno-deprecated-declarations +build:macos --copt=-Wno-stringop-overread +build:macos --copt=-Wno-sign-compare +build:macos --host_copt=-Wno-deprecated-declarations +build:macos --host_copt=-Wno-stringop-overread +build:macos --host_copt=-Wno-sign-compare + +import %workspace%/specialized_configs.bazelrc + +try-import user.bazelrc diff --git a/.bazelrc.deleted_packages b/.bazelrc.deleted_packages new file mode 100644 index 0000000000..db42040b5c --- /dev/null +++ b/.bazelrc.deleted_packages @@ -0,0 +1,54 @@ +# Generated via './tools/update_deleted_packages.sh' +common --deleted_packages=examples/build_file_generation +common --deleted_packages=examples/build_file_generation/random_number_generator +common --deleted_packages=examples/bzlmod +common --deleted_packages=examples/bzlmod/entry_points +common --deleted_packages=examples/bzlmod/entry_points/tests +common --deleted_packages=examples/bzlmod/libs/my_lib +common --deleted_packages=examples/bzlmod/other_module +common --deleted_packages=examples/bzlmod/other_module/other_module/pkg +common --deleted_packages=examples/bzlmod/other_module/other_module/rule_builder +common --deleted_packages=examples/bzlmod/patches +common --deleted_packages=examples/bzlmod/runfiles +common --deleted_packages=examples/bzlmod/tests +common --deleted_packages=examples/bzlmod/tests/other_module +common --deleted_packages=examples/bzlmod/whl_mods +common --deleted_packages=examples/multi_python_versions/libs/my_lib +common --deleted_packages=examples/multi_python_versions/requirements +common --deleted_packages=examples/multi_python_versions/tests +common --deleted_packages=examples/pip_parse +common --deleted_packages=examples/pip_parse_vendored +common --deleted_packages=gazelle +common --deleted_packages=gazelle/examples/bzlmod_build_file_generation +common --deleted_packages=gazelle/examples/bzlmod_build_file_generation/other_module/other_module/pkg +common --deleted_packages=gazelle/examples/bzlmod_build_file_generation/runfiles +common --deleted_packages=gazelle/manifest +common --deleted_packages=gazelle/manifest/generate +common --deleted_packages=gazelle/manifest/hasher +common --deleted_packages=gazelle/manifest/test +common --deleted_packages=gazelle/modules_mapping +common --deleted_packages=gazelle/python +common --deleted_packages=gazelle/pythonconfig +common --deleted_packages=gazelle/python/private +common --deleted_packages=tests/integration/bzlmod_lockfile +common --deleted_packages=tests/integration/compile_pip_requirements +common --deleted_packages=tests/integration/compile_pip_requirements_test_from_external_repo +common --deleted_packages=tests/integration/custom_commands +common --deleted_packages=tests/integration/local_toolchains +common --deleted_packages=tests/integration/pip_parse +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/runtime_manifests +common --deleted_packages=tests/integration/toolchain_target_settings +common --deleted_packages=tests/integration/unified_pypi +common --deleted_packages=tests/integration/uv_lock +common --deleted_packages=tests/integration/validate_test_main +common --deleted_packages=tests/modules/another_module +common --deleted_packages=tests/modules/other +common --deleted_packages=tests/modules/other/nspkg_delta +common --deleted_packages=tests/modules/other/nspkg_gamma +common --deleted_packages=tests/modules/other/nspkg_single +common --deleted_packages=tests/modules/other/simple_v1 +common --deleted_packages=tests/modules/other/simple_v2 +common --deleted_packages=tests/modules/other/with_external_data diff --git a/.bazelversion b/.bazelversion index c6b7980b68..512e4c889e 100644 --- a/.bazelversion +++ b/.bazelversion @@ -1 +1 @@ -8.x +9.x diff --git a/.bcr/config.yml b/.bcr/config.yml index 7672aa554d..038761b6df 100644 --- a/.bcr/config.yml +++ b/.bcr/config.yml @@ -12,7 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -fixedReleaser: - login: f0rmiga - email: 3149049+f0rmiga@users.noreply.github.com -moduleRoots: [".", "gazelle"] +moduleRoots: [".", "gazelle", "sphinxdocs"] diff --git a/.bcr/gazelle/presubmit.yml b/.bcr/gazelle/presubmit.yml index bceed4f9e1..99647cac6f 100644 --- a/.bcr/gazelle/presubmit.yml +++ b/.bcr/gazelle/presubmit.yml @@ -13,16 +13,27 @@ # limitations under the License. bcr_test_module: - module_path: "../examples/bzlmod_build_file_generation" + module_path: "examples/bzlmod_build_file_generation" matrix: - platform: ["debian11", "macos", "ubuntu2004", "windows"] - # last_rc is to get latest 8.x release. Replace with 8.x when available. - bazel: [7.x, last_rc] + platform: [ + "debian11", + "macos", + "ubuntu2204", + ] + bazel: [7.*, 8.*, 9.*] tasks: run_tests: name: "Run test module" platform: ${{ platform }} bazel: ${{ bazel }} + shell_commands: + - "echo 'common --override_module=rules_python=' >> .bazelrc" + - "bazel run //:gazelle_python_manifest.update" + - "bazel run //:gazelle -- update" + batch_commands: + - "echo common --override_module=rules_python= >> .bazelrc" + - "bazel run //:gazelle_python_manifest.update" + - "bazel run //:gazelle -- update" build_targets: - "//..." - ":modules_map" diff --git a/.bcr/presubmit.yml b/.bcr/presubmit.yml index e1ddb7a1aa..a38c6bace5 100644 --- a/.bcr/presubmit.yml +++ b/.bcr/presubmit.yml @@ -15,18 +15,19 @@ bcr_test_module: module_path: "examples/bzlmod" matrix: - platform: ["debian11", "macos", "ubuntu2004", "windows"] - # last_rc is to get latest 8.x release. Replace with 8.x when available. - bazel: [7.x, last_rc] + platform: ["debian11", "macos", "ubuntu2204", "windows"] + bazel: [7.x, 8.x, 9.x] tasks: run_tests: name: "Run test module" platform: ${{ platform }} bazel: ${{ bazel }} test_flags: + # Minimum bazel supported C++ - "--keep_going" - # Without these cxxopts, BCR's Mac builds fail - - '--cxxopt=-std=c++14' - - '--host_cxxopt=-std=c++14' + - '--cxxopt=-std=c++17' + - '--host_cxxopt=-std=c++17' + build_targets: + - "//..." test_targets: - "//..." diff --git a/.bcr/sphinxdocs/metadata.template.json b/.bcr/sphinxdocs/metadata.template.json new file mode 100644 index 0000000000..017f9d3774 --- /dev/null +++ b/.bcr/sphinxdocs/metadata.template.json @@ -0,0 +1,21 @@ +{ + "homepage": "https://github.com/bazel-contrib/rules_python", + "maintainers": [ + { + "name": "Richard Levasseur", + "email": "richardlev@gmail.com", + "github": "rickeylev" + }, + { + "name": "Ignas Anikevicius", + "email": "bcr-ignas@use.startmail.com", + "github": "aignas" + } + ], + "repository": [ + "github:bazelbuild/rules_python", + "github:bazel-contrib/rules_python" + ], + "versions": [], + "yanked_versions": {} +} diff --git a/.bcr/sphinxdocs/presubmit.yml b/.bcr/sphinxdocs/presubmit.yml new file mode 100644 index 0000000000..00a6bd37aa --- /dev/null +++ b/.bcr/sphinxdocs/presubmit.yml @@ -0,0 +1,23 @@ +bcr_test_module: + module_path: "integration_tests/bcr" + matrix: + platform: ["debian11", "macos", "ubuntu2204"] + bazel: [8.*, 9.*] + tasks: + run_tests: + name: "Run test module" + platform: ${{ platform }} + bazel: ${{ bazel }} + shell_commands: + - "echo 'common --override_module=rules_python=' >> .bazelrc" + batch_commands: + - "echo common --override_module=rules_python= >> .bazelrc" + test_flags: + # Minimum bazel supported C++ + - "--keep_going" + - '--cxxopt=-std=c++17' + - '--host_cxxopt=-std=c++17' + build_targets: + - "//..." + test_targets: + - "//..." diff --git a/.bcr/sphinxdocs/source.template.json b/.bcr/sphinxdocs/source.template.json new file mode 100644 index 0000000000..b2a2d1ef23 --- /dev/null +++ b/.bcr/sphinxdocs/source.template.json @@ -0,0 +1,5 @@ +{ + "integrity": "", + "strip_prefix": "{REPO}-{VERSION}/sphinxdocs", + "url": "https://github.com/{OWNER}/{REPO}/releases/download/{TAG}/rules_python-{TAG}.tar.gz" +} diff --git a/.gitattributes b/.gitattributes index eae260e931..4f93d89d33 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,7 @@ python/features.bzl export-subst tools/publish/*.txt linguist-generated=true +tests/uv/lock/testdata/requirements.txt text eol=lf +python/private/runtimes_manifest_workspace.bzl text eol=lf +python/private/runtimes_manifest.txt text eol=lf + +*.bat text eol=crlf diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4df29bacdf..a0b4be250a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -7,5 +7,5 @@ /examples/build_file_generation/ @dougthor42 @aignas # PyPI integration related code -/python/private/pypi/ @rickeylev @aignas @groodt -/tests/pypi/ @rickeylev @aignas @groodt +/python/private/pypi/ @rickeylev @aignas +/tests/pypi/ @rickeylev @aignas diff --git a/.github/ISSUE_TEMPLATE/release_tracking_template.md b/.github/ISSUE_TEMPLATE/release_tracking_template.md new file mode 100644 index 0000000000..57a51a16ef --- /dev/null +++ b/.github/ISSUE_TEMPLATE/release_tracking_template.md @@ -0,0 +1,32 @@ +--- +name: Release Tracking Issue +about: Checklist for tracking a new release of rules_python. +title: 'Release ' +labels: ['type: release'] +--- +# Release tasks +- [ ] Prepare Release | status=awaiting-preparation +- [ ] Create Release branch +- [ ] Tag RC0 +- [ ] Tag Final + +## Backports + +To request a backport, add it to the checklist below and process it. See [RELEASING.md: How to add backports](https://github.com/bazel-contrib/rules_python/blob/main/RELEASING.md#how-to-add-backports) for details. + +--- + +To manually control the release flow, see the [RELEASING.md: Manual Editing](https://github.com/bazel-contrib/rules_python/blob/main/RELEASING.md#manual-editing-of-tracking-issue) section. + +
+Available Commands + +Comment commands: +- `/prepare`: Determines version, creates tracking issue and preparation PR. +- `/create-rc`: Tags and publishes a new release candidate (RC). +- `/process-backports`: Cherry-picks pending backports. +- `/add-backports `: Adds PRs to the backports and processes backports. +- `/promote`: Promotes the latest RC to final release. + +See [RELEASING.md](https://github.com/bazel-contrib/rules_python/blob/main/RELEASING.md) for details on how to use them. +
diff --git a/.github/workflows/automated_pr_review.yaml b/.github/workflows/automated_pr_review.yaml new file mode 100644 index 0000000000..927adbbd4c --- /dev/null +++ b/.github/workflows/automated_pr_review.yaml @@ -0,0 +1,79 @@ +name: Automated Code Review + +# TODO: Eventually, use pull_request_target instead of pull_request. +# pull_request_target runs in the base branch context and has access +# to secrets (like GEMINI_API_KEY) even for fork PRs. +# Using pull_request for now during setup/testing. +on: + issue_comment: + types: [created] + +permissions: + contents: read + pull-requests: read + +jobs: + # Always runs so the workflow run exits cleanly without a "No jobs ran" error + # when the review job's if-condition evaluates to false. + noop: + runs-on: ubuntu-latest + steps: + - name: Workflow trigger check + run: echo "Workflow triggered successfully." + + review: + runs-on: ubuntu-latest + # Trigger only if it is a regular comment on a pull request, the comment body has a line + # starting with "/review", and the commenter is a maintainer (OWNER, MEMBER, or COLLABORATOR). + if: > + github.event_name == 'issue_comment' && github.event.issue.pull_request != null && + (startsWith(github.event.comment.body, '/review') || + contains(github.event.comment.body, '\n/review') || + contains(github.event.comment.body, '\r\n/review')) && + contains(fromJson('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) + steps: + - name: Checkout PR Branch + uses: actions/checkout@v7 + with: + # Note: In GHA, during an issue_comment event on a PR, github.event.pull_request is null + # and the PR number is placed inside github.event.issue.number (because every PR is an issue). + # Therefore, github.event.issue.number is the PR number when checking out refs/pull//head. + ref: refs/pull/${{ github.event.pull_request.number || github.event.issue.number }}/head + persist-credentials: false + + - name: Fetch Base Branch and Negotiate Minimal Diff History + env: + PR_COMMITS: ${{ github.event.pull_request.commits }} + run: | + # 1. Fetch the tip of main + git fetch origin main:refs/remotes/origin/main --depth=1 + + # 2. Check if we already have the merge base (common ancestor) + if ! git merge-base origin/main HEAD >/dev/null 2>&1; then + # If we know the exact number of commits in the PR, deepen by (PR_COMMITS + 10) + if [ -n "$PR_COMMITS" ] && [ "$PR_COMMITS" != "null" ]; then + git fetch --deepen="$((PR_COMMITS + 10))" + fi + # 3. If it's an issue_comment event (where PR_COMMITS is null) or still shallow, unshallow/deepen + if ! git merge-base origin/main HEAD >/dev/null 2>&1; then + git fetch --unshallow || git fetch --deepen=50 + fi + fi + + - name: Checkout Reviewbot (Base Branch) + uses: actions/checkout@v7 + with: + sparse-checkout: | + tools/private/reviewbot + path: reviewbot + + - name: Install uv + uses: astral-sh/setup-uv@v8.3.0 + + - name: Run Antigravity Review + env: + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + uv run reviewbot/tools/private/reviewbot/antigravity_review.py \ + --prompt reviewbot/tools/private/reviewbot/prompt.txt diff --git a/.github/workflows/check_do_not_merge_label.yml b/.github/workflows/check_do_not_merge_label.yml deleted file mode 100644 index 97b91b156a..0000000000 --- a/.github/workflows/check_do_not_merge_label.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: "Check 'do not merge' label" - -on: - pull_request_target: - types: - - opened - - synchronize - - reopened - - labeled - - unlabeled - -jobs: - block-do-not-merge: - runs-on: ubuntu-latest - steps: - - name: Check for "do not merge" label - if: "contains(github.event.pull_request.labels.*.name, 'do not merge')" - run: | - echo "This PR has the 'do not merge' label and cannot be merged." - exit 1 diff --git a/.github/workflows/check_version_markers.sh b/.github/workflows/check_version_markers.sh new file mode 100755 index 0000000000..15a0a67dc8 --- /dev/null +++ b/.github/workflows/check_version_markers.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +set -o nounset +set -o pipefail +set -o errexit + +set -x + +TAG=${1:-} +if [ -n "$TAG" ]; then + # If the workflow checks out one commit, but is releasing another + git fetch origin tag "$TAG" + # Update our local state so the grep command below searches what we expect + git checkout "$TAG" +fi + +grep_exit_code=0 +# Exclude dot directories, specifically, this file so that we don't +# find the substring we're looking for in our own file. +# Exclude CONTRIBUTING.md, RELEASING.md because they document how to use these strings. +grep --exclude=CONTRIBUTING.md \ + --exclude=RELEASING.md \ + --exclude-dir=.* \ + --exclude-dir=release \ + VERSION_NEXT_ -r || grep_exit_code=$? + +if [[ $grep_exit_code -eq 0 ]]; then + echo + echo "Found VERSION_NEXT markers indicating version needs to be specified" + exit 1 +fi diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000000..e7b2a4d956 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,43 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + types: + - opened + - synchronize + +defaults: + run: + shell: bash + +permissions: + contents: read + +jobs: + mypy: + runs-on: ubuntu-latest + steps: + # Checkout the code + - uses: actions/checkout@v7 + - uses: jpetrucciani/mypy-check@master + with: + path: 'python/runfiles' + - uses: jpetrucciani/mypy-check@master + with: + path: 'tests/runfiles' + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: astral-sh/ruff-action@v4.1.0 + with: + # Keep in sync with .pre-commit-config.yaml + version: 0.15.14 + args: check --extend-exclude testdata + - uses: astral-sh/ruff-action@v4.1.0 + with: + version: 0.15.14 + args: format --check --exclude testdata diff --git a/.github/workflows/create_archive_and_notes.sh b/.github/workflows/create_archive_and_notes.sh index a21585f866..506e2f9784 100755 --- a/.github/workflows/create_archive_and_notes.sh +++ b/.github/workflows/create_archive_and_notes.sh @@ -13,25 +13,29 @@ # See the License for the specific language governing permissions and # limitations under the License. -set -o errexit -o nounset -o pipefail - -# Exclude dot directories, specifically, this file so that we don't -# find the substring we're looking for in our own file. -# Exclude CONTRIBUTING.md, RELEASING.md because they document how to use these strings. -if grep --exclude=CONTRIBUTING.md --exclude=RELEASING.md --exclude-dir=.* VERSION_NEXT_ -r; then - echo - echo "Found VERSION_NEXT markers indicating version needs to be specified" +set -o nounset +set -o pipefail +set -o errexit + +set -x + +TAG=$1 +if [ -z "$TAG" ]; then + echo "ERROR: TAG env var must be set" exit 1 fi +# If the workflow checks out one commit, but is releasing another +git fetch origin tag "$TAG" + +# Update our local state so that check_version_markers searches what we expect +git checkout "$TAG" +$(dirname $0)/check_version_markers.sh -# Set by GH actions, see -# https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables -TAG=${GITHUB_REF_NAME} # A prefix is added to better match the GitHub generated archives. PREFIX="rules_python-${TAG}" ARCHIVE="rules_python-$TAG.tar.gz" -git archive --format=tar --prefix=${PREFIX}/ ${TAG} | gzip > $ARCHIVE -SHA=$(shasum -a 256 $ARCHIVE | awk '{print $1}') +git archive --format=tar "--prefix=${PREFIX}/" "$TAG" | gzip > "$ARCHIVE" +SHA=$(shasum -a 256 "$ARCHIVE" | awk '{print $1}') cat > release_notes.txt << EOF diff --git a/.github/workflows/mypy.yaml b/.github/workflows/mypy.yaml deleted file mode 100644 index b83b5d4b37..0000000000 --- a/.github/workflows/mypy.yaml +++ /dev/null @@ -1,31 +0,0 @@ -name: mypy - -on: - push: - branches: - - main - pull_request: - types: - - opened - - synchronize - -defaults: - run: - shell: bash - -jobs: - ci: - runs-on: ubuntu-latest - steps: - # Checkout the code - - uses: actions/checkout@v5 - - uses: jpetrucciani/mypy-check@master - with: - requirements: 1.6.0 - python_version: 3.9 - path: 'python/runfiles' - - uses: jpetrucciani/mypy-check@master - with: - requirements: 1.6.0 - python_version: 3.9 - path: 'tests/runfiles' diff --git a/.github/workflows/on_comment.yaml b/.github/workflows/on_comment.yaml new file mode 100644 index 0000000000..ed7813adac --- /dev/null +++ b/.github/workflows/on_comment.yaml @@ -0,0 +1,140 @@ +name: "On Comment" + +on: + issue_comment: + types: [created] + +permissions: + contents: write + issues: write + pull-requests: write + +jobs: + # This job always runs to prevent GHA from marking the run as failed when + # all other jobs are skipped. + noop: + runs-on: ubuntu-latest + steps: + - run: echo "No-op" + + parse_comment: + runs-on: ubuntu-latest + if: | + github.event.comment.author_association == 'OWNER' || + github.event.comment.author_association == 'MEMBER' || + github.event.comment.author_association == 'COLLABORATOR' + outputs: + command: ${{ steps.parse.outputs.command }} + issue_number: ${{ steps.parse.outputs.issue_number }} + pr_number: ${{ steps.parse.outputs.pr_number }} + backports: ${{ steps.parse.outputs.backports }} + steps: + - name: Parse comment + id: parse + env: + COMMENT_BODY: ${{ github.event.comment.body }} + IS_PR: "${{ github.event.issue.pull_request != null }}" + EVENT_NUMBER: "${{ github.event.issue.number }}" + HAS_RELEASE_LABEL: "${{ contains(github.event.issue.labels.*.name, 'type: release') }}" + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + if [ "$IS_PR" = "false" ] && [ "$HAS_RELEASE_LABEL" = "true" ]; then + issue_number=$EVENT_NUMBER + if echo "$COMMENT_BODY" | grep -qE '^[[:space:]]*/create-rc([[:space:]]|$)'; then + echo "command=create-rc" >> "$GITHUB_OUTPUT" + echo "issue_number=$issue_number" >> "$GITHUB_OUTPUT" + elif echo "$COMMENT_BODY" | grep -qE '^[[:space:]]*/prepare([[:space:]]|$)'; then + echo "command=prepare" >> "$GITHUB_OUTPUT" + echo "issue_number=$issue_number" >> "$GITHUB_OUTPUT" + elif echo "$COMMENT_BODY" | grep -qE '^[[:space:]]*/process-backports([[:space:]]|$)'; then + echo "command=process-backports" >> "$GITHUB_OUTPUT" + echo "issue_number=$issue_number" >> "$GITHUB_OUTPUT" + elif echo "$COMMENT_BODY" | grep -qE '^[[:space:]]*/add-backports([[:space:]]|$)'; then + args=$(echo "$COMMENT_BODY" | grep -E '^[[:space:]]*/add-backports([[:space:]]|$)' | sed -E 's/^[[:space:]]*\/add-backports[[:space:]]*//') + args=$(echo "$args" | sed -e 's/^[[:space:],]*//' -e 's/[[:space:],]*$//') + csv=$(echo "$args" | sed -E 's/[[:space:],]+/ /g' | tr ' ' ',') + if [ -n "$csv" ]; then + echo "command=add-backports" >> "$GITHUB_OUTPUT" + echo "backports=$csv" >> "$GITHUB_OUTPUT" + echo "issue_number=$issue_number" >> "$GITHUB_OUTPUT" + else + echo "command=none" >> "$GITHUB_OUTPUT" + echo "Error: No PRs specified for add-backports." >&2 + gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions \ + -f "content=-1" + fi + elif echo "$COMMENT_BODY" | grep -qE '^[[:space:]]*/promote([[:space:]]|$)'; then + echo "command=promote" >> "$GITHUB_OUTPUT" + echo "issue_number=$issue_number" >> "$GITHUB_OUTPUT" + else + echo "command=none" >> "$GITHUB_OUTPUT" + fi + elif [ "$IS_PR" = "true" ]; then + pr_number=$EVENT_NUMBER + if echo "$COMMENT_BODY" | grep -qE '^[[:space:]]*/backport([[:space:]]|$)'; then + echo "command=pr-backport" >> "$GITHUB_OUTPUT" + echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT" + else + echo "command=none" >> "$GITHUB_OUTPUT" + fi + else + echo "command=none" >> "$GITHUB_OUTPUT" + fi + + call_create_rc: + needs: parse_comment + if: needs.parse_comment.outputs.command == 'create-rc' + uses: ./.github/workflows/release_create_rc.yaml + with: + issue: ${{ needs.parse_comment.outputs.issue_number }} + comment_id: "${{ github.event.comment.id }}" + secrets: inherit + + call_prepare: + needs: parse_comment + if: needs.parse_comment.outputs.command == 'prepare' + uses: ./.github/workflows/release_prepare.yaml + with: + issue: ${{ needs.parse_comment.outputs.issue_number }} + secrets: inherit + + call_add_backports: + needs: parse_comment + if: | + needs.parse_comment.outputs.command == 'add-backports' || + needs.parse_comment.outputs.command == 'pr-backport' + uses: ./.github/workflows/release_add_backports.yaml + with: + prs: ${{ needs.parse_comment.outputs.command == 'pr-backport' && needs.parse_comment.outputs.pr_number || needs.parse_comment.outputs.backports }} + issue: ${{ needs.parse_comment.outputs.issue_number }} + secrets: inherit + + call_process_backports_after_add: + needs: [parse_comment, call_add_backports] + if: needs.parse_comment.outputs.command == 'add-backports' + uses: ./.github/workflows/release_process_backports.yaml + with: + issue: ${{ needs.parse_comment.outputs.issue_number }} + comment_id: "${{ github.event.comment.id }}" + secrets: inherit + + call_process_backports_only: + needs: parse_comment + if: needs.parse_comment.outputs.command == 'process-backports' + uses: ./.github/workflows/release_process_backports.yaml + with: + issue: ${{ needs.parse_comment.outputs.issue_number }} + comment_id: "${{ github.event.comment.id }}" + secrets: inherit + + call_promote: + needs: parse_comment + if: needs.parse_comment.outputs.command == 'promote' + uses: ./.github/workflows/release_promote_rc.yaml + with: + issue: ${{ needs.parse_comment.outputs.issue_number }} + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/on_pr_closed.yaml b/.github/workflows/on_pr_closed.yaml new file mode 100644 index 0000000000..94c49b6561 --- /dev/null +++ b/.github/workflows/on_pr_closed.yaml @@ -0,0 +1,113 @@ +name: "On PR Closed" + +on: + pull_request: + types: [closed] + +permissions: + contents: read + issues: read + pull-requests: read + +jobs: + # This job always runs to prevent GHA from marking the run as failed when + # all other jobs are skipped. + noop: + runs-on: ubuntu-latest + steps: + - run: echo "No-op" + + check_if_backport: + runs-on: ubuntu-latest + if: github.event.pull_request.merged == true + outputs: + should_process: ${{ steps.check.outputs.should_process }} + steps: + - name: Check if PR is a backport candidate + id: check + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + # Check if there is any active release issue + ACTIVE_ISSUES=$(gh issue list --repo ${{ github.repository }} --label "type: release" --state open --json number) + if [ "$ACTIVE_ISSUES" = "[]" ] || [ -z "$ACTIVE_ISSUES" ]; then + echo "No active release tracking issue found. Skipping." + echo "should_process=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Check if PR has "/backport" in comments (only comments, not body) + PR_DATA=$(gh pr view "$PR_NUMBER" --repo ${{ github.repository }} --json comments) + + if echo "$PR_DATA" | jq -r '.comments[].body' | grep -qE '^[[:space:]]*/backport([[:space:]]|$)'; then + echo "Found /backport comment. Proceeding." + echo "should_process=true" >> "$GITHUB_OUTPUT" + else + echo "No /backport comment found. Skipping." + echo "should_process=false" >> "$GITHUB_OUTPUT" + fi + + process_backports: + needs: check_if_backport + if: needs.check_if_backport.outputs.should_process == 'true' + runs-on: ubuntu-latest + permissions: + contents: write + issues: write + pull-requests: read + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup Bazel + uses: bazel-contrib/setup-bazel@0.19.0 + with: + bazelisk-version: 1.20.0 + + - name: Configure Git Identity + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Process Backports + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + bazel run //tools/private/release -- on-pr-merged \ + "$PR_NUMBER" \ + --remote origin \ + --no-dry-run + + complete_sync_changelog: + if: | + github.event.pull_request.merged == true && + contains(github.event.pull_request.labels.*.name, 'type: sync-changelog') + runs-on: ubuntu-latest + permissions: + contents: write + issues: write + pull-requests: read + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup Bazel + uses: bazel-contrib/setup-bazel@0.19.0 + with: + bazelisk-version: 1.20.0 + + - name: Complete Sync Changelog + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + bazel run //tools/private/release -- complete-sync-changelog \ + --pr "$PR_NUMBER" + + diff --git a/.github/workflows/pr-metadata-checks.yaml b/.github/workflows/pr-metadata-checks.yaml new file mode 100644 index 0000000000..8060ff921e --- /dev/null +++ b/.github/workflows/pr-metadata-checks.yaml @@ -0,0 +1,60 @@ +name: "PR Metadata Checks" + +on: + pull_request_target: + types: + - opened + - synchronize + - reopened + - labeled + - unlabeled + - edited + +jobs: + blocks-do-not-merge: + runs-on: ubuntu-latest + steps: + - name: Check for "do not merge" label + if: "contains(github.event.pull_request.labels.*.name, 'do not merge')" + run: | + echo "::error::This PR has the 'do not merge' label" \ + "and cannot be merged." + exit 1 + + - name: Check PR description for DO NOT SUBMIT/MERGE + env: + PR_BODY: ${{ github.event.pull_request.body }} + run: | + echo "Checking PR description..." + if echo "$PR_BODY" | grep -Ei "DO NOT SUBMIT|DO NOT MERGE"; then + echo "::error::This PR description contains" \ + "'DO NOT SUBMIT' or 'DO NOT MERGE'" \ + "and cannot be merged." + exit 1 + fi + echo "PR description is clean." + + block-transient-agent-files: + runs-on: ubuntu-latest + permissions: + pull-requests: read + steps: + - name: Check for blocked files + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + echo "Checking for blocked files..." + # We use --repo to avoid needing actions/checkout. + # This keeps the job lightweight and avoids "not a git repository" errors. + changed_files=$(gh pr diff "$PR_NUMBER" --name-only --repo "${{ github.repository }}") + blocked_files=$(echo "$changed_files" | grep -E '^.agents/(plans|scratch)(/|$)' || true) + if [ -n "$blocked_files" ]; then + echo "::error::Files in .agents/plans and" \ + ".agents/scratch are permitted in PRs to" \ + "facilitate discussion, but are not allowed" \ + "to be merged. Please remove them before merging:" + echo "$blocked_files" + exit 1 + fi + echo "No blocked files found." diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000000..c5db587f18 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,38 @@ +# Publish new releases to Bazel Central Registry. +name: Publish to BCR +on: + # Run the publish workflow after a successful release + # Will be triggered from the release.yaml workflow + workflow_call: + inputs: + tag_name: + required: true + type: string + secrets: + publish_token: + required: true + # In case of problems, let release engineers retry by manually dispatching + # the workflow from the GitHub UI + workflow_dispatch: + inputs: + tag_name: + description: git tag being released + required: true + type: string +jobs: + publish: + uses: bazel-contrib/publish-to-bcr/.github/workflows/publish.yaml@v1.4.1 + with: + tag_name: ${{ inputs.tag_name }} + # GitHub repository which is a fork of the upstream where the Pull Request will be opened. + registry_fork: bazel-contrib/bazel-central-registry + attest: false + # Create non-draft PRs so the BCR bazel-io bot processes them. Otherwise, + # since a bazel-contrib bot, we have to wait for BCR maintainers to mark it + # non-draft or approve it. + draft: false + permissions: + contents: write + secrets: + # Necessary to push to the BCR fork, and to open a pull request against a registry + publish_token: ${{ secrets.publish_token || secrets.BCR_PUBLISH_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 7a25c6eca0..0000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright 2023 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. - -# Cut a release whenever a new tag is pushed to the repo. -name: Release - -on: - push: - tags: - - "*.*.*" - workflow_dispatch: - inputs: - publish_to_pypi: - description: 'Publish to PyPI' - required: true - type: boolean - default: true - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v5 - - name: Create release archive and notes - run: .github/workflows/create_archive_and_notes.sh - - name: Publish wheel dist - if: github.event_name == 'push' || github.event.inputs.publish_to_pypi - env: - # This special value tells pypi that the user identity is supplied within the token - TWINE_USERNAME: __token__ - # Note, the PYPI_API_TOKEN is for the rules-python pypi user, added by @rickylev on - # https://github.com/bazel-contrib/rules_python/settings/secrets/actions - TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} - run: bazel run --stamp --embed_label=${{ github.ref_name }} //python/runfiles:wheel.publish - - name: Release - uses: softprops/action-gh-release@v2 - with: - # Use GH feature to populate the changelog automatically - generate_release_notes: true - body_path: release_notes.txt - prerelease: ${{ contains(github.ref, '-rc') }} - fail_on_unmatched_files: true - files: rules_python-*.tar.gz diff --git a/.github/workflows/release_add_backports.yaml b/.github/workflows/release_add_backports.yaml new file mode 100644 index 0000000000..d9d6d84e34 --- /dev/null +++ b/.github/workflows/release_add_backports.yaml @@ -0,0 +1,58 @@ +name: "Release: Add Backports" + +on: + workflow_dispatch: + inputs: + prs: + description: 'CSV list of PR numbers to add (e.g., 123,456)' + required: true + type: string + issue: + description: 'The Release Tracking Issue Number (optional)' + required: false + type: string + workflow_call: + inputs: + prs: + description: 'CSV list of PR numbers to add (e.g., 123,456)' + required: true + type: string + issue: + description: 'The Release Tracking Issue Number (optional)' + required: false + type: string + +permissions: + issues: write + +jobs: + add_backports: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup Bazel + uses: bazel-contrib/setup-bazel@0.19.0 + with: + bazelisk-version: 1.20.0 + + - name: Add Backports to Tracking Issue + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PRS: ${{ inputs.prs }} + ISSUE: ${{ inputs.issue }} + run: | + ARGS=() + if [ -n "$ISSUE" ]; then + ARGS+=("--issue=$ISSUE") + fi + + # Convert CSV to array + IFS=',' read -r -a pr_array <<< "$PRS" + + bazel run //tools/private/release -- add-backports \ + "${pr_array[@]}" \ + "${ARGS[@]}" diff --git a/.github/workflows/release_complete_prepare.yaml b/.github/workflows/release_complete_prepare.yaml new file mode 100644 index 0000000000..6d8bc8fd03 --- /dev/null +++ b/.github/workflows/release_complete_prepare.yaml @@ -0,0 +1,35 @@ +name: "Release: Complete Prepare" + +on: + pull_request: + types: [closed] + +permissions: + contents: write + issues: write + +jobs: + on_pr_merged: + # Run only if the release-prepared PR was merged + if: | + github.event.pull_request.merged == true && + contains(github.event.pull_request.labels.*.name, 'release-prepared') + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup Bazel + uses: bazel-contrib/setup-bazel@0.19.0 + with: + bazelisk-version: 1.20.0 + + - name: Mark Prepare Release Complete + run: | + # Run the complete-prepare subcommand in the release tool to cleanly update checklist metadata + bazel run //tools/private/release -- \ + complete-prepare --pr ${{ github.event.pull_request.number }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release_create_rc.yaml b/.github/workflows/release_create_rc.yaml new file mode 100644 index 0000000000..a96a04a0ec --- /dev/null +++ b/.github/workflows/release_create_rc.yaml @@ -0,0 +1,69 @@ +name: "Release: Create RC" + +on: + workflow_dispatch: + inputs: + issue: + description: 'The Release Tracking Issue Number (e.g., 142)' + required: true + type: string + comment_id: + description: 'The ID of the comment that triggered this run (optional)' + required: false + type: string + workflow_call: + inputs: + issue: + description: 'The Release Tracking Issue Number (e.g., 142)' + required: true + type: string + comment_id: + description: 'The ID of the comment that triggered this run (optional)' + required: false + type: string + +permissions: + contents: write + issues: write + +jobs: + tag_rc: + runs-on: ubuntu-latest + outputs: + tag_name: ${{ steps.tagger.outputs.tag_name }} + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup Bazel + uses: bazel-contrib/setup-bazel@0.19.0 + with: + bazelisk-version: 1.20.0 + + - name: Configure Git Identity + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Attempt RC Tagging + id: tagger + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE: ${{ inputs.issue }} + COMMENT_ID: ${{ inputs.comment_id }} + run: | + ARGS=() + if [ -n "$COMMENT_ID" ]; then + ARGS+=("--triggering-comment=$COMMENT_ID") + fi + bazel run //tools/private/release -- \ + create-rc --issue "$ISSUE" --remote origin "${ARGS[@]}" + + call_release: + needs: tag_rc + uses: ./.github/workflows/release_publish.yaml + with: + tag_name: ${{ needs.tag_rc.outputs.tag_name }} + secrets: inherit diff --git a/.github/workflows/release_create_release_branch.yaml b/.github/workflows/release_create_release_branch.yaml new file mode 100644 index 0000000000..4cbabc7d12 --- /dev/null +++ b/.github/workflows/release_create_release_branch.yaml @@ -0,0 +1,46 @@ +name: "Release: Create Release Branch" + +on: + issues: + types: [edited] + +permissions: + contents: write + issues: write + +jobs: + cut_branch: + # Run only if the issue has the type: release label + if: "contains(github.event.issue.labels.*.name, 'type: release')" + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup Bazel + uses: bazel-contrib/setup-bazel@0.19.0 + with: + bazelisk-version: 1.20.0 + + - name: Configure Git Identity + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Attempt Branch Creation + run: | + bazel run //tools/private/release -- \ + create-release-branch --issue ${{ github.event.issue.number }} --remote origin + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # A no-op job that always runs to prevent "no jobs ran" failures + # when the main job is skipped. + suppress-no-jobs-ran-error: + runs-on: ubuntu-latest + steps: + - name: Echo Success + run: echo "Success" + diff --git a/.github/workflows/release_prepare.yaml b/.github/workflows/release_prepare.yaml new file mode 100644 index 0000000000..c83a7b74f9 --- /dev/null +++ b/.github/workflows/release_prepare.yaml @@ -0,0 +1,51 @@ +name: "Release: Prepare" + +on: + workflow_dispatch: + inputs: + issue: + description: 'The Release Tracking Issue Number (e.g., 142)' + required: false + type: string + workflow_call: + inputs: + issue: + description: 'The Release Tracking Issue Number (e.g., 142)' + required: false + type: string + +permissions: + contents: write + issues: write + pull-requests: write + +jobs: + prepare_release: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup Bazel + uses: bazel-contrib/setup-bazel@0.19.0 + with: + bazelisk-version: 1.20.0 + + - name: Configure Git Identity + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Run Release Preparation Pipeline + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE: ${{ inputs.issue }} + run: | + ARGS=() + if [ -n "$ISSUE" ]; then + ARGS+=("--issue=$ISSUE") + fi + bazel run //tools/private/release -- \ + prepare "${ARGS[@]}" --no-dry-run diff --git a/.github/workflows/release_process_backports.yaml b/.github/workflows/release_process_backports.yaml new file mode 100644 index 0000000000..ca719778df --- /dev/null +++ b/.github/workflows/release_process_backports.yaml @@ -0,0 +1,76 @@ +name: "Release: Process Backports" + +on: + workflow_dispatch: + inputs: + issue: + description: 'The Release Tracking Issue Number (e.g., 142)' + required: true + type: string + add_backports: + description: 'CSV list of PR numbers to add and process (optional)' + required: false + type: string + comment_id: + description: 'The ID of the comment that triggered this run (optional)' + required: false + type: string + workflow_call: + inputs: + issue: + description: 'The Release Tracking Issue Number (e.g., 142)' + required: true + type: string + add_backports: + description: 'CSV list of PR numbers to add and process (optional)' + required: false + type: string + comment_id: + description: 'The ID of the comment that triggered this run (optional)' + required: false + type: string + +permissions: + contents: write + issues: write + +jobs: + process_backports: + # Always gate GHA runs to ensure we are operating on a type: release labeled issue if metadata is queried + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup Bazel + uses: bazel-contrib/setup-bazel@0.19.0 + with: + bazelisk-version: 1.20.0 + + - name: Configure Git Identity + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Process Pending Backports + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ADD_BACKPORTS: ${{ inputs.add_backports }} + COMMENT_ID: ${{ inputs.comment_id }} + ISSUE: ${{ inputs.issue }} + run: | + ARGS=() + if [ -n "$ADD_BACKPORTS" ]; then + ARGS+=("--add=$ADD_BACKPORTS") + fi + if [ -n "$COMMENT_ID" ]; then + ARGS+=("--triggering-comment=$COMMENT_ID") + fi + + bazel run //tools/private/release -- process-backports \ + --issue "$ISSUE" \ + --remote origin \ + --no-dry-run \ + "${ARGS[@]}" diff --git a/.github/workflows/release_promote_rc.yaml b/.github/workflows/release_promote_rc.yaml new file mode 100644 index 0000000000..399e7b1fa2 --- /dev/null +++ b/.github/workflows/release_promote_rc.yaml @@ -0,0 +1,71 @@ +name: "Release: Promote RC" + +on: + workflow_dispatch: + inputs: + version: + description: 'The final version to release (e.g., 0.38.0)' + required: true + type: string + workflow_call: + inputs: + version: + description: 'The final version to release (e.g., 0.38.0)' + required: false + type: string + issue: + description: 'The tracking issue number' + required: true + type: string + +permissions: + contents: write + issues: write + +jobs: + promote: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.promote.outputs.version }} + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup Bazel + uses: bazel-contrib/setup-bazel@0.19.0 + with: + bazelisk-version: 1.20.0 + + - name: Configure Git Identity + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Run Promote RC + id: promote + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ inputs.version }} + ISSUE: ${{ inputs.issue }} + run: | + ARGS=() + if [ -n "$VERSION" ]; then + ARGS+=("$VERSION") + fi + if [ -n "$ISSUE" ]; then + ARGS+=("--issue=$ISSUE") + fi + ARGS+=("--remote=origin") + ARGS+=("--no-dry-run") + + bazel run //tools/private/release -- promote-rc "${ARGS[@]}" + + publish: + needs: promote + uses: ./.github/workflows/release_publish.yaml + with: + tag_name: ${{ needs.promote.outputs.version }} + publish_to_pypi: true + secrets: inherit diff --git a/.github/workflows/release_publish.yaml b/.github/workflows/release_publish.yaml new file mode 100644 index 0000000000..8379c3d46d --- /dev/null +++ b/.github/workflows/release_publish.yaml @@ -0,0 +1,95 @@ +# Copyright 2023 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. + +# Cut a release whenever a new tag is pushed to the repo. +name: "Release: Publish" + +on: + push: + tags: + - "*.*.*" + workflow_dispatch: + inputs: + tag_name: + description: "release tag: tag that will be released" + required: true + type: string + publish_to_pypi: + description: 'Publish to PyPI' + required: true + type: boolean + default: true + secrets: + publish_token: + required: false + workflow_call: + inputs: + tag_name: + description: "release tag: tag that will be released" + required: true + type: string + publish_to_pypi: + description: 'Publish to PyPI' + required: false + type: boolean + default: true + +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + ref: ${{ inputs.tag_name || github.ref_name }} + - name: Create release archive and notes + run: .github/workflows/create_archive_and_notes.sh ${{ inputs.tag_name || github.ref_name }} + - name: Release + uses: softprops/action-gh-release@v3 + with: + # Use GH feature to populate the changelog automatically + generate_release_notes: true + body_path: release_notes.txt + prerelease: ${{ contains( (inputs.tag_name || github.ref), '-rc') }} + fail_on_unmatched_files: true + files: rules_python-*.tar.gz + tag_name: ${{ inputs.tag_name || github.ref_name }} + + publish_bcr: + needs: release + uses: ./.github/workflows/publish.yml + with: + tag_name: ${{ inputs.tag_name || github.ref_name }} + secrets: + publish_token: ${{ secrets.publish_token || secrets.BCR_PUBLISH_TOKEN }} + + publish_pypi: + # We just want publish_pypi last, since once uploaded, it can't be changed. + name: Publish runfiles to PyPI + needs: publish_bcr + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + ref: ${{ inputs.tag_name || github.ref_name }} + - if: github.event_name == 'push' || github.event.inputs.publish_to_pypi + env: + # This special value tells pypi that the user identity is supplied within the token + TWINE_USERNAME: __token__ + # Note, the PYPI_API_TOKEN is for the rules-python pypi user, added by @rickylev on + # https://github.com/bazel-contrib/rules_python/settings/secrets/actions + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: bazel run --stamp --embed_label=${{ inputs.tag_name || github.ref_name }} //python/runfiles:wheel.publish diff --git a/.gitignore b/.gitignore index fb1b17e466..efce592aa0 100644 --- a/.gitignore +++ b/.gitignore @@ -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 + diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 67a02fc6c0..f4107880b2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: check-merge-conflict - repo: https://github.com/keith/pre-commit-buildifier - rev: 6.1.0 + rev: 8.2.1 hooks: - id: buildifier args: &args @@ -29,18 +29,15 @@ repos: - --warnings=all - id: buildifier-lint args: *args - - repo: https://github.com/pycqa/isort - rev: 5.12.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + # Keep in sync with .github/workflows/ruff.yaml + rev: v0.15.14 hooks: - - id: isort - name: isort (python) - args: - - --profile - - black - - repo: https://github.com/psf/black - rev: 25.1.0 - hooks: - - id: black + - id: ruff-check + args: [--fix] + exclude: testdata + - id: ruff-format + exclude: testdata - repo: local hooks: - id: update-deleted-packages @@ -48,6 +45,29 @@ repos: language: system # 7.x is necessary until https://github.com/bazel-contrib/rules_bazel_integration_test/pull/414 # is merged and released - entry: env USE_BAZEL_VERSION=7.x bazel run @rules_bazel_integration_test//tools:update_deleted_packages + entry: ./tools/update_deleted_packages.sh files: ^((examples|tests)/.*/(MODULE.bazel|WORKSPACE|WORKSPACE.bzlmod|BUILD.bazel)|.bazelrc)$ pass_filenames: false + - id: sort-runtimes-manifest + name: Sort runtimes manifest + language: system + entry: ./python/private/tools/sort_manifest.py + files: ^python/private/runtimes_manifest\.txt$ + - id: sync-runtimes-manifest + name: Sync runtimes manifest workspace + language: system + entry: ./python/private/tools/sync_runtimes_manifest_workspace.py + files: ^python/private/runtimes_manifest\.txt$ + pass_filenames: false + - id: sync-downloader-configs + name: Sync downloader configs + language: system + entry: ./tools/private/sync_downloader_configs.py + files: downloader_config\.cfg$ + pass_filenames: false + - id: gazelle + name: Run Gazelle + language: system + entry: bazel run //tools/private/gazelle + files: (\.bzl|\.bazel|BUILD|WORKSPACE(\.bzlmod)?)$ + pass_filenames: false diff --git a/AGENTS.md b/AGENTS.md index 671b85c6bd..e6e1733c1d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,7 +5,17 @@ project. Act as an expert in Bazel, rules_python, Starlark, and Python. -DO NOT `git commit` or `git push`. +DO NOT `git commit` or `git push` unless given explicit permission. + +## RULES TO ALWAYS FOLLOW AND NEVER IGNORE + +ALWAYS FOLLOW THESE RULES. NEVER VIOLATE THEM. + +Ask for user input and provide a justificaiton if trying to violate them. + +* NEVER run `bazel clean --expunge`. +* Once a PR is created, do not amend or rebase. +* Do not add Bazel copyright to new or existing files. ## Style and conventions @@ -22,11 +32,83 @@ into the sentence, not verbatim. When adding `{versionadded}` or `{versionchanged}` sections, add them add the end of the documentation text. +### PR Updates + +Once a PR is created, create new commits and merges. Don't use rebase or amend +because it interferes with code review comments. + +### PR descriptions + +Follow the advice in `CONTRIBUTING.md` for PR descriptions. PR descriptions +become the commit message upon merge. + ### Starlark style For doc strings, using triple quoted strings when the doc string is more than three lines. Do not use a trailing backslack (`\`) for the opening triple-quote. +### Starlark Code + +Starlark does not support recursion. Use iterative algorithms instead. + +Starlark does not support `while` loops. Use `for` loop with an appropriately +sized iterable instead. + +#### Starlark testing + +For Starlark tests: + +* Use `rules_testing`, not `bazel_skylib`. +* See https://rules-testing.readthedocs.io/en/latest/analysis_tests.html for + examples on using rules_testing. +* See `tests/builders/builders_tests.bzl` for an example of using it in + this project. + +A test is defined in two parts: + * A setup function, e.g. `def _test_foo(name)`. This defines targets + and calls `analysis_test`. + * An implementation function, e.g. `def _test_foo_impl(env, target)`. This + contains asserts. + +Example: + +``` +# File: foo_tests.bzl + +load("@rules_testing//lib:analysis_test.bzl", "analysis_test") +load("@rules_testing//lib:test_suite.bzl", "test_suite") + +_tests = [] + +def _test_foo(name): + foo_library( + name = name + "_subject", + ) + analysis_test( + name = name, + impl = _test_foo_impl, + target = name + "_subject", + ) +_tests.append(_test_foo) + +def _test_foo_impl(env, target): + env.expect.that_whatever(target[SomeInfo].whatever).equals(expected) + +def foo_test_suite(name): + test_suite(name=name, tests=_tests) +``` + + +#### Repository rules + +The function argument `rctx` is a hint that the function is a repository rule, +or used by a repository rule. + +The function argument `mrctx` is a hint that the function can be used by a +repository rule or module extension. + +The `repository_ctx` API docs are at: https://bazel.build/rules/lib/builtins/repository_ctx + ### bzl_library targets for bzl source files * A `bzl_library` target should be defined for every `.bzl` file outside @@ -59,12 +141,24 @@ bzl_library( Tests are under the `tests/` directory. -When testing, add `--test_tag_filters=-integration-test`. +When testing, add `--config=fast-tests`. + +When building, add `--config=fast-tests`. -When building, add `--build_tag_filters=-integration-test`. +The `--config=fast-tests` flag avoids running expensive and slow tests can that +freeze the host machine or cause flakiness. ## Understanding the code base +This repository contains 3 Bazel bzlmod modules. + + * `sphinxdocs/` is for the `@sphinxdocs` module. + * `gazelle/` is for the `@rules_python_gazelle_plugin` module. + * All other code is part of `@rules_python`. + +`tests/support/` contains utility code and helpers for testing. + + `python/config_settings/BUILD.bazel` contains build flags that are part of the public API. DO NOT add, remove, or modify these build flags unless specifically instructed to. @@ -121,3 +215,24 @@ e.g. ``` load("//python/private:foo.bzl", "foo") # buildifier: disable=bzl-visibility ``` + +### CI Failure Inspection + +When inspecting CI failures, if the failure is due to a network error +downloading a repository, check if that rule set is mirrored on +mirror.bazel.build. If so, add it to the downloader config. + +### CI Flakiness and Monitoring + +CI is known to have flakey network issues. When submitting or updating a PR, +start a background agent that continuously monitors the latest build of a PR and +checks for flakey network errors (e.g., 504 gateway errors). + +If Buildkite permissions allow, retry failures and use available Buildkite +skills. If permissions do not allow, modify `.bazelrc` to set flags (such as +`--http_timeout_scaling` or `--experimental_repository_downloader_retries`) to +make downloads retry more. + +If downloads fail for a dependency, check if that dependency is available on +mirror.bazel.build and, if so, add it to downloader_config.cfg if it isn't +already there. diff --git a/BUILD.bazel b/BUILD.bazel index 5e85c27b3c..a25863708c 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -14,6 +14,40 @@ load("@bazel_skylib//:bzl_library.bzl", "bzl_library") +# Resolve rules_cc Starlark libraries to their correct targets +# gazelle:resolve starlark @rules_cc//cc/common:cc_info.bzl @rules_cc//cc/common:common +# gazelle:resolve starlark @rules_cc//cc/common:cc_common.bzl @rules_cc//cc/common:common + +# Resolve protobuf Starlark libraries +# gazelle:resolve starlark @com_google_protobuf//bazel:py_proto_library.bzl @com_google_protobuf//bazel:py_proto_library_bzl + +# Resolve bazel_tools repo rules to our internal wrapper to avoid transitive dependencies +# gazelle:resolve starlark @bazel_tools//tools/build_defs/repo:http.bzl //python/private:bazel_tools +# gazelle:resolve starlark @bazel_tools//tools/build_defs/repo:utils.bzl //python/private:bazel_tools + +# Prevent Gazelle from incorrectly stripping the .bzl suffix from the toml.bzl repo name +# gazelle:resolve starlark @toml.bzl//:toml.bzl @toml.bzl//:toml + +# Override Gazelle's incorrect default resolution for platforms host constraints Starlark library +# gazelle:resolve starlark @platforms//host:constraints.bzl @platforms//host:constraints_lib + +# Override Gazelle's incorrect default resolution for rules_cc Starlark libraries +# gazelle:resolve starlark @rules_cc//cc:cc_import.bzl @rules_cc//cc:core_rules +# gazelle:resolve starlark @rules_cc//cc:cc_library.bzl @rules_cc//cc:core_rules + +# Exclude directories that are separate packages/workspaces or only for testing/examples +# gazelle:exclude tests +# gazelle:exclude examples + +# Exclude internal development tools and dependencies that users don't need +# gazelle:exclude internal_dev_setup.bzl +# gazelle:exclude internal_dev_deps.bzl +# gazelle:exclude python/private/internal_dev_deps.bzl +# gazelle:exclude workspace_bazel9.bzl + +# Exclude legacy paths that are only kept for backwards compatibility +# gazelle:exclude python/private/common + package(default_visibility = ["//visibility:public"]) licenses(["notice"]) @@ -43,21 +77,15 @@ filegroup( "internal_dev_deps.bzl", "internal_dev_setup.bzl", "version.bzl", + "//command_line_option:distribution", "//python:distribution", "//tools:distribution", - "@rules_python_gazelle_plugin//:distribution", ], visibility = [ "//:__subpackages__", ], ) -bzl_library( - name = "version_bzl", - srcs = ["version.bzl"], - visibility = ["//:__subpackages__"], -) - # Reexport of all bzl files used to allow downstream rules to generate docs # without shipping with a dependency on Skylib filegroup( @@ -73,3 +101,12 @@ filegroup( ], visibility = ["//visibility:public"], ) + +# keep +bzl_library( + name = "version", + srcs = ["version.bzl"], + visibility = ["//:__subpackages__"], +) + +# No-op change to verify PR metadata checks diff --git a/BZLMOD_SUPPORT.md b/BZLMOD_SUPPORT.md index 73fde463b7..49010b838e 100644 --- a/BZLMOD_SUPPORT.md +++ b/BZLMOD_SUPPORT.md @@ -11,7 +11,7 @@ In general `bzlmod` has more features than `WORKSPACE` and users are encouraged ## Configuration -The releases page will give you the latest version number, and a basic example. The release page is located [here](/bazel-contrib/rules_python/releases). +The releases page will give you the latest version number, and a basic example. The release page is located [here](https://github.com/bazel-contrib/rules_python/releases). ## What is bzlmod? @@ -27,7 +27,7 @@ We have two examples that demonstrate how to configure `bzlmod`. The first example is in [examples/bzlmod](examples/bzlmod), and it demonstrates basic bzlmod configuration. A user does not use `local_path_override` stanza and would define the version in the `bazel_dep` line. -A second example, in [examples/bzlmod_build_file_generation](examples/bzlmod_build_file_generation) demonstrates the use of `bzlmod` to configure `gazelle` support for `rules_python`. +A second example, in [gazelle/examples/bzlmod_build_file_generation](gazelle/examples/bzlmod_build_file_generation) demonstrates the use of `bzlmod` to configure `gazelle` support for `rules_python`. ## Differences in behavior from WORKSPACE diff --git a/CHANGELOG.md b/CHANGELOG.md index 469e9d3612..17f176e5b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,46 +20,568 @@ A brief description of the categories of changes: * Particular sub-systems are identified using parentheses, e.g. `(bzlmod)` or `(docs)`. - +[1.9.0]: https://github.com/bazel-contrib/rules_python/releases/tag/1.9.0 -{#v0-0-0} -## Unreleased +{#v1-9-0-removed} +### Removed +* Nothing removed. + +{#v1-9-0-changed} +### Changed +* **DEPRECATED: implicit zipapp support** + * Implicit zipapp output of `py_binary`/`py_test` has been deprecated and + replaced by separate {obj}`py_zipapp_binary` and {obj}`py_zipapp_test` + rules. See + [#3567](https://github.com/bazel-contrib/rules_python/issues/3567) + for a detailed migration guide. +* (toolchains) stop exposing config settings in python toolchain alias repos. + Please consider depending on the flags defined in + `//python/config_setting/...` and the `@platforms` package instead. +* (binaries/tests) The `PYTHONBREAKPOINT` environment variable is automatically inherited +* (binaries/tests) The {obj}`stamp` attribute now transitions the Bazel builtin + {flag}`--stamp` flag. +* (pypi) Now the RECORD file patches will follow the quoted or unquoted filenames convention + in order to make `pytorch` and friends easier to patch. +* (wheel) `py_wheel` no longer expands the input depset during analysis, + improving analysis performance for targets with large dependency trees. +* (binaries/tests) (Windows) `--enable_runfiles=true` is the default for + `py_binary/py_test`. Prior behavior can be restored by adding + `@rules_python//command_line_option:enable_runfiles=false` or + `@rules_python//command_line_option:enable_runfiles=INHERIT` to the + `config_settings` attribute. NOTE: `enable_runfiles=true` will + soon become **required for Windows**. + +{#v1-9-0-fixed} +### Fixed +* (tests) No more coverage warnings are being printed if there are no sources. + ([#2762](https://github.com/bazel-contrib/rules_python/issues/2762)) +* (gazelle) Ancestor `conftest.py` files are added in addition to sibling `conftest.py`. + ([#3497](https://github.com/bazel-contrib/rules_python/issues/3497)) Note + that this behavior can be reverted to the pre-1.9.0 behavior by setting the new + `python_include_ancestor_conftest` directive to `false`. +* (binaries/tests) Stamped build data generated by Windows actions is readable + +{#v1-9-0-added} +### Added +* (binaries/tests) {obj}`--debugger`: allows specifying an extra dependency + to add to binaries/tests for custom debuggers. +* (binaries/tests) Build information is now included in binaries and tests. + Use the `bazel_binary_info` module to access it. The {flag}`--stamp` flag will + add {obj}`--workspace_status_command` information. +* (gazelle) A new directive `python_generate_pyi_srcs` has been added. When + `true`, a `py_*` target's `pyi_srcs` attribute will be set if any `.pyi` files + that are associated with the target's `srcs` are present. + ([#3354](https://github.com/bazel-contrib/rules_python/issues/3354)). +* (zipapp) {obj}`py_zipapp_binary` and {obj}`py_zipapp_test` rules added. These + will replace `--build_python_zip` and the zip output group of + `py_binary/py_test`. The zipapp rules support more functionality, correctness, + and have better build performance. +* (toolchains) Added {obj}`PyExecToolsInfo.exec_runtime` for more easily + getting an RBE-compatible runtime to use for build actions. +* (providers) {obj}`PyExecutableInfo` has several new fields to aid packaging + of binaries: {obj}`PyExecutableInfo.app_runfiles`, + {obj}`PyExecutableInfo.interpreter_args`, + {obj}`PyExecutableInfo.stage2_bootstrap`, and + {obj}`PyExecutableInfo.venv_python_exe`. +* (tools/wheelmaker.py) Added support for URL requirements according to PEP 508 + in Requires-Dist metadata. ([#3569](https://github.com/bazel-contrib/rules_python/pull/3569)) +* (gazelle) A new directive `python_include_ancestor_conftest` has been added. + When `false`, ancestor `conftest` targets are not automatically added to + {bzl:obj}`py_test` target dependencies. This `false` behavior is how things + were in `rules_python` before 1.9.0. The default is `true`, as the prior behavior + was technically incorrect. + ([#3596](https://github.com/bazel-contrib/rules_python/pull/3596)) + + +{#v1-8-5} +## [1.8.5] - 2026-02-22 + +[1.8.5]: https://github.com/bazel-contrib/rules_python/releases/tag/1.8.5 + +{#v1-8-5-fixed} +### Fixed +* (runfiles) Fixed `CurrentRepository()` raising `ValueError` on Windows. + ([#3579](https://github.com/bazel-contrib/rules_python/issues/3579)) +* (pypi) `pip_parse` no longer silently drops PEP 508 URL-based requirements + (`pkg @ https://...`) when `extract_url_srcs=False` (the default for + `pip_repository`). +* (pypi) Extras in requirement strings are now normalized per PEP 685, + fixing missing transitive dependencies when extras contain hyphens + (e.g., `sqlalchemy[postgresql-psycopg2binary]`). + ([#3587](https://github.com/bazel-contrib/rules_python/issues/3587)) + +{#v1-8-4} +## [1.8.4] - 2026-02-10 + +[1.8.4]: https://github.com/bazel-contrib/rules_python/releases/tag/1.8.4 + +{#v1-8-4-fixed} +### Fixed +* (pipstar): A corner case of evaluation of version specifiers (`"1.2" ~= "1.2.0"`) + has been fixed improving compatibility with the PEP440 standard. + Fixes [#3580](https://github.com/bazel-contrib/rules_python/issues/3580). +* (pipstar): We now add read permissions after extracting wheels for the cases + where the `whl` file is missing them. + Fixes [#3554](https://github.com/bazel-contrib/rules_python/issues/3554). + +{#v1-8-3} +## [1.8.3] - 2026-01-27 + +[1.8.3]: https://github.com/bazel-contrib/rules_python/releases/tag/1.8.3 + +{#v1-8-3-fixed} +### Fixed +* (pipstar) Fix whl extraction on Windows when bazelrc has XX flags. + Fixes [#3543](https://github.com/bazel-contrib/rules_python/issues/3543). + +{#v1-8-2} +## [1.8.2] - 2026-01-24 -[0.0.0]: https://github.com/bazel-contrib/rules_python/releases/tag/0.0.0 +[1.8.2]: https://github.com/bazel-contrib/rules_python/releases/tag/1.8.2 -{#v0-0-0-removed} +{#v1-8-2-fixed} +### Fixed +* (venvs) relax the C library filename check to make tensorflow work + Fixes [#3524](https://github.com/bazel-contrib/rules_python/issues/3529). + +{#v1-8-1} +## [1.8.1] - 2026-01-20 + +[1.8.1]: https://github.com/bazel-contrib/rules_python/releases/tag/1.8.1 + +{#v1-8-1-fixed} +### Fixed +* (pipstar) Extra resolution that refers back to the package being resolved works again. + Fixes [#3524](https://github.com/bazel-contrib/rules_python/issues/3524). + +{#v1-8-0} +## [1.8.0] - 2025-12-19 + +[1.8.0]: https://github.com/bazel-contrib/rules_python/releases/tag/1.8.0 + +{#v1-8-0-known-issues} +### Known Issues +* (gazelle) Windows support for the Gazelle plugin may be broken. See + [#3416](https://github.com/bazel-contrib/rules_python/issues/3416) for + details and possible workarounds. + +{#v1-8-0-removed} +### Removed +* (toolchain) Remove all of the python 3.8 toolchain support out of the box. Users need + to pass the `TOOL_VERSIONS` that include 3.8 toolchains or use the `bzlmod` APIs to add + them back. This means any hub `pip.parse` calls that target `3.8` will be ignored from + now on. ([#2704](https://github.com/bazel-contrib/rules_python/issues/2704)) + {bzl:obj}`python.single_version_override`, like: + + ```starlark + python = use_extension("@rules_python//python/extensions:python.bzl", "python") + + python.single_version_override( + python_version = "3.8.20", + sha256 = { + "aarch64-apple-darwin": "2ddfc04bdb3e240f30fb782fa1deec6323799d0e857e0b63fa299218658fd3d4", + "aarch64-unknown-linux-gnu": "9d8798f9e79e0fc0f36fcb95bfa28a1023407d51a8ea5944b4da711f1f75f1ed", + "x86_64-apple-darwin": "68d060cd373255d2ca5b8b3441363d5aa7cc45b0c11bbccf52b1717c2b5aa8bb", + "x86_64-pc-windows-msvc": "41b6709fec9c56419b7de1940d1f87fa62045aff81734480672dcb807eedc47e", + "x86_64-unknown-linux-gnu": "285e141c36f88b2e9357654c5f77d1f8fb29cc25132698fe35bb30d787f38e87", + }, + urls = ["https://github.com/astral-sh/python-build-standalone/releases/download/20241002/cpython-{python_version}+20241002-{platform}-{build}.tar.gz"], + ) + ``` +* (toolchain) Remove all of the python 3.9 toolchain versions except for the `3.9.25`. + This version has reached EOL and will no longer receive any security fixes, please update to + `3.10` or above. ([#2704](https://github.com/bazel-contrib/rules_python/issues/2704)) +* (toolchain) `ignore_root_user_error` has now been flipped to be always enabled and + the `chmod` of the python toolchain directories have been removed. From now on `rules_python` + always adds the `pyc` files to the glob excludes and in order to avoid any problems when using + the toolchains in the repository phase, ensure that you pass `-B` to the python interpreter. + ([#2016](https://github.com/bazel-contrib/rules_python/issues/2016)) + +{#v1-8-0-changed} +### Changed +* (toolchains) Use toolchains from the [20251031] release. +* (gazelle) Internally split modules mapping generation to be per-wheel for concurrency and caching. +* (pip) `pipstar` has been enabled for all `whl_library` instances where the whl + is passed through a label or downloaded using the bazel downloader + ([#2949](https://github.com/bazel-contrib/rules_python/issues/2949)). +* (pypi) `pipstar` flag default has been flipped to be on by default. + It can be disabled through `RULES_PYTHON_ENABLE_PIPSTAR=0` environment variable. + If you do need to disable it, please add a comment to + [#2949](https://github.com/bazel-contrib/rules_python/issues/2949). +* (gazelle deps) rules_go bumped from 0.55.1 to 0.59.0 +* (gazelle deps) gazelle bumped from 0.36.0 to 0.47.0 + +{#v1-8-0-fixed} +### Fixed +* (gazelle) Remove {obj}`py_binary` targets with invalid `srcs`. This includes files + that are not generated or regular files. + [#3046](https://github.com/bazel-contrib/rules_python/pull/3046) +* (runfiles) Fix incorrect Python runfiles path assumption - the existing + implementation assumes that it is always four levels below the runfiles + directory, leading to incorrect path checks + ([#3085](https://github.com/bazel-contrib/rules_python/issues/3085)). +* (toolchains) local toolchains now tell the `sys.abiflags` value of the + underlying runtime. +* (toolchains) various local toolchain fixes: add abi3 header targets, + fixes to linking, Windows DLL detection, and defines for free threaded + runtimes. +* (toolchains) The `python_headers` target is now compatible with + layering_check. +* (performance) 90% reduction in py_binary/py_test analysis phase cost. + ([#3381](https://github.com/bazel-contrib/rules_python/pull/3381)). +* (gazelle) Fix `gazelle_python_manifest.test` so that it accesses manifest files via `runfile` path handling rather than directly ([#3397](https://github.com/bazel-contrib/rules_python/issues/3397)). +* (core rules) For the system_python bootstrap, the runfiles root is added to + sys.path. +* (sphinxdocs) The sphinxdocs `.serve` target is now compatible with Bazel's `--symlink_prefix` + flag ([#3410](https://github.com/bazel-contrib/rules_python/issues/3410)). + +{#v1-8-0-added} +### Added +* (toolchains) `3.9.25` Python toolchain from [20251031] release. +* (toolchains) `3.13.10`, `3.14.1` Python toolchain from [20251202] release. +* (toolchains) `3.13.11`, `3.14.2`, `3.15.0a2` Python toolchains from [20251209] release. +* (pypi) API to tell `pip.parse` which platforms users care about. This is very useful to ensure + that when users do `bazel query` for their deps, they don't have to download all of the + dependencies for all of the available wheels. Torch wheels can be up of 1GB and it takes a lot + of time to download those, which is unnecessary if only the host platform builds are necessary + to be performed. This is mainly for backwards/forwards compatibility whilst rolling out + `RULES_PYTHON_ENABLE_PIPSTAR=1` by default. Users of `experimental_index_url` that perform + cross-builds should add {obj}`target_platforms` to their `pip.parse` invocations, which will + become mandatory if any cross-builds are required from the next release. +* (py_library) Attribute {obj}`namespace_package_files` added. It is a hint for + optimizing venv creation. + +[20251031]: https://github.com/astral-sh/python-build-standalone/releases/tag/20251031 +[20251202]: https://github.com/astral-sh/python-build-standalone/releases/tag/20251202 +[20251209]: https://github.com/astral-sh/python-build-standalone/releases/tag/20251209 + +{#v1-7-0} +## [1.7.0] - 2025-10-11 + +[1.7.0]: https://github.com/bazel-contrib/rules_python/releases/tag/1.7.0 + +{#v1-7-0-removed} ### Removed * (core rules) Support for Bazel's long deprecated "extra actions" has been removed ([#3215](https://github.com/bazel-contrib/rules_python/issues/3215)). -{#v0-0-0-changed} +{#v1-7-0-changed} ### Changed * (deps) bumped rules_cc dependency to `0.1.5`. * (bootstrap) For {obj}`--bootstrap_impl=system_python`, `PYTHONPATH` is no @@ -69,9 +591,31 @@ END_UNRELEASED_TEMPLATE * (bootstrap) For {obj}`--bootstrap_impl=system_python`, the sys.path order has changed from `[app paths, stdlib, runtime site-packages]` to `[stdlib, app paths, runtime site-packages]`. +* (pip) Publishing deps are no longer pulled via `experimental_index_url`. + ([#2937](https://github.com/bazel-contrib/rules_python/issues/2937)). +* (toolchains) `py_runtime` and `PyRuntimeInfo` reject Python 2 settings. + Setting `py_runtime.python_version = "PY2"` or non-None + `PyRuntimeInfo.py2_runtime` is an error. +* (pypi) `pipstar` flag has been implemented for `WORKSPACE` and can be flipped to be enabled using `RULES_PYTHON_ENABLE_PIPSTAR=1` environment variable. If you do, please + add a comment to + [#2949](https://github.com/bazel-contrib/rules_python/issues/2949) if you run into any + problems. + With this release we are deprecating {obj}`pip.parse.experimental_target_platforms` and + `pip_repository.experimental_target_platforms`. For users using `WORKSPACE` and + vendoring the `requirements.bzl` file, please re-vendor so that downstream is unaffected + when the APIs get removed. If you need to customize the way the dependencies get + evaluated, see [our docs](https://rules-python.readthedocs.io/en/latest/pypi/download.html#customizing-requires-dist-resolution) on customizing `Requires-Dist` resolution. +* (toolchains) Added Python versions 3.15.0a1, 3.14.0, 3.13.9, 3.12.12, 3.11.14, 3.10.19, and 3.9.24 + from the [20251014] release. +* (deps) (bzlmod) Upgraded to `bazel-skylib` version + [1.8.2](https://github.com/bazelbuild/bazel-skylib/releases/tag/1.8.2) -{#v0-0-0-fixed} +[20251014]: https://github.com/astral-sh/python-build-standalone/releases/tag/20251014 + +{#v1-7-0-fixed} ### Fixed +* (rules) The `PyInfo` constructor was setting the wrong value for + `has_py3_only_sources` - this is now fixed. * (bootstrap) The stage1 bootstrap script now correctly handles nested `RUNFILES_DIR` environments, fixing issues where a `py_binary` calls another `py_binary` ([#3187](https://github.com/bazel-contrib/rules_python/issues/3187)). @@ -79,11 +623,27 @@ END_UNRELEASED_TEMPLATE length errors due to too long environment variables. * (bootstrap) {obj}`--bootstrap_impl=script` now supports the `-S` interpreter setting. -* (venvs) {obj}`--vens_site_packages=yes` no longer errors when packages with +* (venvs) {obj}`--venvs_site_packages=yes` no longer errors when packages with overlapping files or directories are used together. ([#3204](https://github.com/bazel-contrib/rules_python/issues/3204)). - -{#v0-0-0-added} +* (venvs) {obj}`--venvs_site_packages=yes` works for packages that dynamically + link to shared libraries + ([#3228](https://github.com/bazel-contrib/rules_python/issues/3228)). +* (venvs) {obj}`--venvs_site_packages=yes` includes `pth` files at the root of the + site-packages folder + ([#3339](https://github.com/bazel-contrib/rules_python/issues/3339)). +* (uv) {obj}`//python/uv:lock.bzl%lock` now works with a local platform + runtime. +* (pypi) `linux_riscv64` is added to the platforms list in `_pip_repository_impl`, + which fixes [a build issue for tensorflow on riscv64](https://github.com/bazel-contrib/rules_python/discussions/2729). +* (toolchains) WORKSPACE builds now correctly register musl and freethreaded + variants. Setting {obj}`--py_linux_libc=musl` and `--py_freethreaded=yes` now + activate them, respectively. + ([#3262](https://github.com/bazel-contrib/rules_python/issues/3262)). +* (rules) {obj}`py_console_script_binary` is now compatible with symbolic macros + ([#3195](https://github.com/bazel-contrib/rules_python/pull/3195)). + +{#v1-7-0-added} ### Added * (runfiles) The Python runfiles library now supports Bazel's `--incompatible_compact_repo_mapping_manifest` flag. @@ -107,6 +667,10 @@ END_UNRELEASED_TEMPLATE {obj}`py_cc_toolchain.headers_abi3`, and {obj}`PyCcToolchainInfo.headers_abi3`. * {obj}`//python:features.bzl%features.headers_abi3` can be used to feature-detect the presense of the above. +* (toolchains) Local toolchains can use a label for the interpreter to use. +* (pypi) Support for environment marker handling and `experimental_index_url` handling for + Windows ARM64 for Python 3.11 and later + ([#2276](https://github.com/bazel-contrib/rules_python/issues/2276)). {#v1-6-3} ## [1.6.3] - 2025-09-21 @@ -120,7 +684,7 @@ END_UNRELEASED_TEMPLATE the right wheel when there are multiple wheels for the target platform (e.g. `musllinux_1_1_x86_64` and `musllinux_1_2_x86_64`). If the user wants to set the minimum version for the selection algorithm, use the - {attr}`pip.defaults.whl_platform_tags` attribute to configure that. If + {obj}`pip.default.whl_platform_tags` attribute to configure that. If `musllinux_*_x86_64` is specified, we will choose the lowest available wheel version. Fixes [#3250](https://github.com/bazel-contrib/rules_python/issues/3250). @@ -1942,4 +2506,4 @@ Breaking changes: * (pip) Create all_data_requirements alias * Expose Python C headers through the toolchain. -[0.24.0]: https://github.com/bazel-contrib/rules_python/releases/tag/0.24.0 +[0.24.0]: https://github.com/bazel-contrib/rules_python/releases/tag/0.24.0 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e1bd11b81d..73ab6d6220 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -103,7 +103,7 @@ information on using pull requests. [GitHub Help]: https://help.github.com/articles/about-pull-requests/ -### Commit messages +### Commit messages and PR descriptions Commit messages (upon merging) and PR messages should follow the [Conventional Commits](https://www.conventionalcommits.org/) style: @@ -139,16 +139,63 @@ Common `type`s: * `revert:` means a prior change is being reverted in some way. * `test:` means only tests are being added. +For the body, follow this guidance: + +* Briefly tells *why* the change is being made. This usually means + briefly describing how a bug manifests or what can't be accomplished + without the feature. +* Briefly gives an overview of *how* the code is changed. This is to + orient readers for the diff they're about to read and understand; it's + not a verbatim description of what changed. +* List unrelated or notable dev-only changes at the end. e.g. formatting an + old file, cleaning up testing, adding test support code, etc. + For the full details of types, see [Conventional Commits](https://www.conventionalcommits.org/). +#### PR description example + +``` +fix(pypi): handle files with .exe extensions + +Currently, if a file with `.exe` is seen, an error +occurs because validation assumes unix-only filenames. +This prevents using whls with pre-built .exe files in +their data payload. + +To fix, detect the target OS and use an OS-appropriate +validation function. + +* Also adds test helpers for detecting the current OS +``` + ### Documenting changes -Changes are documented in two places: CHANGELOG.md and API docs. +Changes are documented in two places: news entries and API docs. + +Instead of modifying `CHANGELOG.md` directly, you should create a news entry file in the `news/` directory. These files are automatically assembled into `CHANGELOG.md` at release time. + +#### Creating a news entry + +Create a `.md` file in the `news/` directory. The filename must follow the format `..md`: + +* ``: A unique identifier, typically the GitHub Pull Request number or Issue number (e.g., `1234`). +* ``: The category of the change, which must be one of: + * `added`: For new features or behavior added in a backwards-compatible manner. + * `changed`: For changes in existing behavior. + * `fixed`: For bug fixes. + * `removed`: For removed features or behavior. + +The content of the file should be a brief, human-friendly description of the change. Do not include a leading bullet point (e.g. `*` or `-`), as this is automatically added during assembly. If your change is specific to a subsystem, prefix it with the subsystem in parentheses, e.g., `(gazelle) Fixed handling of...`. + +Example: `news/1234.fixed.md` +```markdown +(gazelle) Fixed handling of auto-included `__init__.py` files when generating `py_binary` targets. +``` + +Do not edit `CHANGELOG.md` directly for unreleased changes. -CHANGELOG.md contains a brief, human friendly, description. This text is -intended for easy skimming so that, when people upgrade, they can quickly get a -sense of what's relevant to them. +#### API documentation API documentation are the doc strings for functions, fields, attributes, etc. When user-visible or notable behavior is added, changed, or removed, the diff --git a/MODULE.bazel b/MODULE.bazel index 6251ed4c3c..c4afa60640 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -5,9 +5,11 @@ module( ) bazel_dep(name = "bazel_features", version = "1.21.0") -bazel_dep(name = "bazel_skylib", version = "1.8.1") -bazel_dep(name = "rules_cc", version = "0.1.5") +bazel_dep(name = "bazel_skylib", version = "1.8.2") +bazel_dep(name = "package_metadata", version = "0.0.7") bazel_dep(name = "platforms", version = "0.0.11") +bazel_dep(name = "rules_cc", version = "0.2.17") +bazel_dep(name = "toml.bzl", version = "0.4.1") # Those are loaded only when using py_proto_library # Use py_proto_library directly from protobuf repository @@ -42,8 +44,12 @@ python = use_extension("//python/extensions:python.bzl", "python") # NOTE: This is not a stable version. It is provided for convenience, but will # change frequently to track the most recent Python version. # NOTE: The root module can override this. +# NOTE: There must be a corresponding `python.toolchain()` call for the version +# specified here. +python.defaults( + python_version = "3.11", +) python.toolchain( - is_default = True, python_version = "3.11", ) use_repo( @@ -60,120 +66,7 @@ register_toolchains("@pythons_hub//:all") # Install twine for our own runfiles wheel publishing and allow bzlmod users to use it. pip = use_extension("//python/extensions:pip.bzl", "pip") - -# NOTE @aignas 2025-07-06: we define these platforms to keep backwards compatibility with the -# current `experimental_index_url` implementation. Whilst we stabilize the API this list may be -# updated with a mention in the CHANGELOG. -[ - pip.default( - arch_name = cpu, - config_settings = [ - "@platforms//cpu:{}".format(cpu), - "@platforms//os:linux", - "//python/config_settings:_is_py_freethreaded_{}".format( - "yes" if freethreaded else "no", - ), - ], - env = {"platform_version": "0"}, - marker = "python_version >= '3.13'" if freethreaded else "", - os_name = "linux", - platform = "linux_{}{}".format(cpu, freethreaded), - whl_abi_tags = ["cp{major}{minor}t"] if freethreaded else [ - "abi3", - "cp{major}{minor}", - ], - whl_platform_tags = [ - "linux_{}".format(cpu), - "manylinux_*_{}".format(cpu), - ], - ) - for cpu in [ - "x86_64", - "aarch64", - ] - for freethreaded in [ - "", - "_freethreaded", - ] -] - -[ - pip.default( - arch_name = cpu, - config_settings = [ - "@platforms//cpu:{}".format(cpu), - "@platforms//os:osx", - "//python/config_settings:_is_py_freethreaded_{}".format( - "yes" if freethreaded else "no", - ), - ], - # We choose the oldest non-EOL version at the time when we release `rules_python`. - # See https://endoflife.date/macos - env = {"platform_version": "14.0"}, - marker = "python_version >= '3.13'" if freethreaded else "", - os_name = "osx", - platform = "osx_{}{}".format(cpu, freethreaded), - whl_abi_tags = ["cp{major}{minor}t"] if freethreaded else [ - "abi3", - "cp{major}{minor}", - ], - whl_platform_tags = [ - "macosx_*_{}".format(suffix) - for suffix in platform_tag_cpus - ], - ) - for cpu, platform_tag_cpus in { - "aarch64": [ - "universal2", - "arm64", - ], - "x86_64": [ - "universal2", - "x86_64", - ], - }.items() - for freethreaded in [ - "", - "_freethreaded", - ] -] - -[ - pip.default( - arch_name = cpu, - config_settings = [ - "@platforms//cpu:{}".format(cpu), - "@platforms//os:windows", - "//python/config_settings:_is_py_freethreaded_{}".format( - "yes" if freethreaded else "no", - ), - ], - env = {"platform_version": "0"}, - marker = "python_version >= '3.13'" if freethreaded else "", - os_name = "windows", - platform = "windows_{}{}".format(cpu, freethreaded), - whl_abi_tags = ["cp{major}{minor}t"] if freethreaded else [ - "abi3", - "cp{major}{minor}", - ], - whl_platform_tags = whl_platform_tags, - ) - for cpu, whl_platform_tags in { - "x86_64": ["win_amd64"], - }.items() - for freethreaded in [ - "", - "_freethreaded", - ] -] - pip.parse( - # NOTE @aignas 2024-10-26: We have an integration test that depends on us - # being able to build sdists for this hub, so explicitly set this to False. - # - # how do we test sdists? Maybe just worth adding a single sdist somewhere? - download_only = False, - experimental_index_url = "https://pypi.org/simple", hub_name = "rules_python_publish_deps", python_version = "3.11", requirements_by_platform = { @@ -184,24 +77,27 @@ pip.parse( ) use_repo(pip, "rules_python_publish_deps") -# Not a dev dependency to allow usage of //sphinxdocs code, which refers to stardoc repos. -bazel_dep(name = "stardoc", version = "0.7.2", repo_name = "io_bazel_stardoc") - # ===== DEV ONLY DEPS AND SETUP BELOW HERE ===== +bazel_dep(name = "sphinxdocs", version = "0.0.0", dev_dependency = True) +local_path_override( + module_name = "sphinxdocs", + path = "sphinxdocs", +) + bazel_dep(name = "rules_bazel_integration_test", version = "0.27.0", dev_dependency = True) bazel_dep(name = "rules_testing", version = "0.6.0", dev_dependency = True) bazel_dep(name = "rules_shell", version = "0.3.0", dev_dependency = True) bazel_dep(name = "rules_multirun", version = "0.9.0", dev_dependency = True) bazel_dep(name = "bazel_ci_rules", version = "1.0.0", dev_dependency = True) -bazel_dep(name = "rules_pkg", version = "1.0.1", dev_dependency = True) +bazel_dep(name = "rules_pkg", version = "1.2.0", dev_dependency = True) bazel_dep(name = "other", version = "0", dev_dependency = True) bazel_dep(name = "another_module", version = "0", dev_dependency = True) # Extra gazelle plugin deps so that WORKSPACE.bzlmod can continue including it for e2e tests. # We use `WORKSPACE.bzlmod` because it is impossible to have dev-only local overrides. -bazel_dep(name = "rules_go", version = "0.41.0", dev_dependency = True, repo_name = "io_bazel_rules_go") -bazel_dep(name = "rules_python_gazelle_plugin", version = "0", dev_dependency = True) +bazel_dep(name = "rules_go", version = "0.60.0", dev_dependency = True, repo_name = "io_bazel_rules_go") bazel_dep(name = "gazelle", version = "0.40.0", dev_dependency = True, repo_name = "bazel_gazelle") +bazel_dep(name = "bazel_skylib_gazelle_plugin", version = "1.8.2", dev_dependency = True) internal_dev_deps = use_extension( "//python/private:internal_dev_deps.bzl", @@ -213,9 +109,15 @@ use_repo( "buildkite_config", "implicit_namespace_ns_sub1", "implicit_namespace_ns_sub2", + "patch_whl_pkg", + "pkgutil_nspkg1", + "pkgutil_nspkg2", "rules_python_runtime_env_tc_info", "somepkg_with_build_files", + "whl_library_extras_direct_dep", "whl_with_build_files", + "whl_with_data1", + "whl_with_data2", ) dev_rules_python_config = use_extension( @@ -231,13 +133,6 @@ dev_rules_python_config.add_transition_setting( setting = "//tests/multi_pypi:external_deps_name", ) -# Add gazelle plugin so that we can run the gazelle example as an e2e integration -# test and include the distribution files. -local_path_override( - module_name = "rules_python_gazelle_plugin", - path = "gazelle", -) - local_path_override( module_name = "other", path = "tests/modules/other", @@ -282,11 +177,16 @@ dev_pip = use_extension( [ dev_pip.parse( download_only = True, - experimental_index_url = "https://pypi.org/simple", hub_name = "dev_pip", parallel_download = False, python_version = python_version, requirements_lock = "//docs:requirements.txt", + # Ensure that we are setting up the following platforms + target_platforms = [ + "{os}_{arch}", + "{os}_{arch}_freethreaded", + ], + uv_lock = "//docs:uv.lock", ) for python_version in [ "3.9", @@ -294,12 +194,12 @@ dev_pip = use_extension( "3.11", "3.12", "3.13", + "3.14", ] ] dev_pip.parse( download_only = True, - experimental_index_url = "https://pypi.org/simple", hub_name = "pypiserver", python_version = "3.11", requirements_lock = "//examples/wheel:requirements_server.txt", @@ -329,20 +229,18 @@ bazel_binaries.local( name = "self", path = "tests/integration/bazel_from_env", ) -bazel_binaries.download(version = "7.4.1") -bazel_binaries.download(version = "8.0.0") - -# For now, don't test with rolling, because that's Bazel 9, which is a ways -# away. -# bazel_binaries.download(version = "rolling") +bazel_binaries.download(version = "7.7.0") +bazel_binaries.download(version = "8.5.1") +bazel_binaries.download(version = "9.1.0") use_repo( bazel_binaries, "bazel_binaries", # These don't appear necessary, but are reported as direct dependencies # that should be use_repo()'d, so we add them as requested "bazel_binaries_bazelisk", - "build_bazel_bazel_7_4_1", - "build_bazel_bazel_8_0_0", + "build_bazel_bazel_7_7_0", + "build_bazel_bazel_8_5_1", + "build_bazel_bazel_9_1_0", # "build_bazel_bazel_rolling", "build_bazel_bazel_self", ) @@ -357,7 +255,7 @@ uv = use_extension("//python/uv:uv.bzl", "uv") uv.default( base_url = "https://github.com/astral-sh/uv/releases/download", manifest_filename = "dist-manifest.json", - version = "0.6.3", + version = "0.11.2", ) uv.default( compatible_with = [ @@ -373,13 +271,6 @@ uv.default( ], platform = "aarch64-unknown-linux-gnu", ) -uv.default( - compatible_with = [ - "@platforms//os:linux", - "@platforms//cpu:ppc", - ], - platform = "powerpc64-unknown-linux-gnu", -) uv.default( compatible_with = [ "@platforms//os:linux", @@ -432,5 +323,28 @@ uv_dev = use_extension( dev_dependency = True, ) uv_dev.configure( - version = "0.6.2", -) + version = "0.11.2", +) + +# Temporarily comment out these flag aliases because they break Bazel 9 +# when transitions are also used with a target. +# +# flag_alias( +# name = "build_python_zip", +# starlark_flag = "//python/config_settings:build_python_zip", +# ) + +# flag_alias( +# name = "incompatible_default_to_explicit_init_py", +# starlark_flag = "//python/config_settings:incompatible_default_to_explicit_init_py", +# ) + +# flag_alias( +# name = "python_path", +# starlark_flag = "//python/config_settings:python_path", +# ) + +# flag_alias( +# name = "experimental_python_import_all_repositories", +# starlark_flag = "//python/config_settings:experimental_python_import_all_repositories", +# ) diff --git a/README.md b/README.md index a7399fff9c..7d805ce315 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,9 @@ ## Overview This repository is the home of the core Python rules -- `py_library`, -`py_binary`, `py_test`, `py_proto_library`, and related symbols that provide the basis for Python -support in Bazel. It also contains package installation rules for integrating with PyPI and other indices. +`py_binary`, `py_test`, and related symbols that provide the basis for Python +support in Bazel. It also contains package installation rules for integrating +with PyPI and other indices. Documentation for rules_python is at and in the [Bazel Build Encyclopedia](https://docs.bazel.build/versions/master/be/python.html). @@ -17,7 +18,50 @@ The core rules are stable. Their implementation is subject to Bazel's [backward compatibility policy](https://docs.bazel.build/versions/master/backward-compatibility.html). This repository aims to follow [semantic versioning](https://semver.org). -The Bazel community maintains this repository. Neither Google nor the Bazel team provides support for the code. However, this repository is part of the test suite used to vet new Bazel releases. See [How to contribute](CONTRIBUTING.md) page for information on our development workflow. +The Bazel community maintains this repository. Neither Google nor the Bazel team +provides support for the code. However, this repository is part of the test +suite used to vet new Bazel releases. See [How to contribute](CONTRIBUTING.md) +page for information on our development workflow. + +## Design + +* Supported OSes - as per our supported platform policy, we strive for support + on all of the platforms that we have CI for. Some platforms do not have the + same backwards compatibility guarantees, but we hope the community can step in + where needed to make the support more robust. +* `requirements.txt` is how users have been defining dependencies for a long + time. We support this to support legacy usecases or package managers that we + don't support directly. Any additional information that we need will be + retrieved from the SimpleAPI during the `bzlmod` extension evaluation phase. + Then it will be written to the `MODULE.bazel.lock` file for future reuse. We + have plans to support `uv.lock` file directly. `uv` is recommended for + generating a fully locked `requirements.txt` file and we do provide a rule for + it. +* The `py_binary`, `py_test` rules should scale to large monorepos and we work + hard to minimize the work done during analysis and build phase. What is more, + the space requirements for should be minimal, so we strive to use symlinks + rather than extracting wheels at build time. This means that for different + configurations of the same build, we are not extracting the wheel multiple + times thus scaling better over the time. From `2.0` onwards we are creating a + virtual env for each target by creating an actual minimal virtual environment + using symlinks. We plan on creating the traditional `site-packages` layout in + the future by default. +* Support for standards - we strive to first implement any standards needed + within `rules_python` and this has resulted in a few PEPs supported within + pure starlark - PEP440, PEP509. + +Common misconceptions: +* `rules_python` has to keep backwards compatibility with `google3`. Whilst this + might have been true in the past, `rules_python` is an open source project and + any compatibility needs should come from the community - we have no + requirement to keep this compatibility and are allowed to make our decisions. + However, we do want to keep backwards compatibility as long as possible to not + upset users with never ending migrations. +* `rules_python` is not caching pip downloads. With 2.0, we use Bazel's + downloader by default and rely on bazel to provide the repository caching + mechanisms. This means that for simpler setups this should result in + transparent and scalable caching with the most recent bazel versions unless + there are issues in the bazel itself. ## Documentation diff --git a/RELEASING.md b/RELEASING.md index e4cf738f3d..2298b4c8a3 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -8,44 +8,41 @@ existing Bazel workspace to sanity check functionality. ## Releasing from HEAD -These are the steps for a regularly scheduled release from HEAD. +Releases are managed using a semi-automated process centered around a GitHub +Release Tracking Issue and automated workflows triggered by comments or issue edits. + +> [!NOTE] +> Comment-based commands must be posted by project maintainers (Owner, +> Member, or Collaborator) and must be on their own line (leading and trailing +> whitespace is ignored). ### Steps -1. Update the changelog and replace the version placeholders by running the - release tool. The next version number will by automatically determined - based on the presence of `VERSION_NEXT_*` placeholders and git tags. - - ```shell - bazel run //tools/private/release - ``` - -1. Send these changes for review and get them merged. -1. Create a branch for the new release, named `release/X.Y` - ``` - git branch --no-track release/X.Y upstream/main && git push upstream release/X.Y - ``` - -The next step is to create tags to trigger release workflow, **however** -we start by using release candidate tags (`X.Y.Z-rcN`) before tagging the -final release (`X.Y.Z`). - -1. Create release candidate tag and push. The first RC uses `N=0`. Increment - `N` for each RC. - ``` - git tag X.Y.0-rcN upstream/release/X.Y && git push upstream tag X.Y.0-rcN - ``` -2. Announce the RC release: see [Announcing Releases] -3. Wait a week for feedback. - * Follow [Patch release with cherry picks] to pull bug fixes into the - release branch. - * Repeat the RC tagging step, incrementing `N`. -4. Finally, tag the final release tag: - ```shell - git tag X.Y.0 upstream/release/X.Y && git push upstream tag X.Y.0 - ``` - -Release automation will create a GitHub release and BCR pull request. +1. **Prepare the Release**: Run the [Release: Prepare](https://github.com/bazel-contrib/rules_python/actions/workflows/release_prepare.yaml) + workflow manually. You can trigger it from the GitHub Actions UI or using + the GitHub CLI: + ```shell + gh workflow run release_prepare.yaml --repo bazel-contrib/rules_python + ``` + This will automatically determine the next version, create a release tracking + issue, and send a preparation PR. + +2. **Approve and Merge**: Approve and merge the PR. Once merged, a release + branch will be created automatically. + +3. **Add Backports (if needed)**: If there are backports, add them following + the [How to add backports](#how-to-add-backports) steps. + +4. **Create an RC**: Comment `/create-rc` on the tracking issue. This will + automatically process pending backports before creating the RC. If any + backport fails, the RC creation will abort. + +5. **Iterate**: Repeat steps 3 and 4 until backports and RCs are no longer + needed. + +6. **Finalize the Release**: Comment `/promote` on the tracking issue to + finalize the release. + ### Manually triggering the release workflow @@ -56,14 +53,14 @@ specific commit. To trigger the workflow, use the `gh workflow run` command: ```shell -gh workflow run release.yml --ref +gh workflow run release_publish.yaml --ref ``` By default, the workflow will publish the wheel to PyPI. To skip this step, you can set the `publish_to_pypi` input to `false`: ```shell -gh workflow run release.yml --ref -f publish_to_pypi=false +gh workflow run release_publish.yaml --ref -f publish_to_pypi=false ``` ### Determining Semantic Version @@ -72,11 +69,59 @@ gh workflow run release.yml --ref -f publish_to_pypi=false API changes and new features bump the minor, and those with only bug fixes and other minor changes bump the patch digit. -The release tool will automatically determine the next version number. To find -if there were any features added or incompatible changes made, review -[CHANGELOG.md](CHANGELOG.md) and the commit history. This can be done using -github by going to the url: -`https://github.com/bazel-contrib/rules_python/compare/...main`. +The release tool will automatically determine the next version number based on +the `VERSION_NEXT_*` placeholders in the codebase. To see what changes are +being accumulated for the next release, review the pending news entries in the +`news/` directory. + +## How to add backports + +To add backports to an active release, you can use one of the following +methods: + +### Method A: Comment on the PR + +Comment `/backport` on the PR you wish to backport. This will automatically +add the PR to the active release's backports checklist. Once the PR is merged, +the backports will be automatically processed. + +> [!NOTE] +> Commenting `/backport` on an open PR will block further release publishing +> (like creating RCs or promoting) until the PR is merged or manually set to +> status=ignore in the checklist. + +### Method B: Comment on the Tracking Issue + +Comment `/add-backports [ ...]` (space or comma separated) on +the tracking issue. The `` can be a PR number (optionally prefixed with +`#`) or a PR URL (strictly for the configured repository). This will +automatically add the PRs to the checklist and trigger processing. + +### Method C: Manual Checklist Update +1. Manually add checklist items under the `## Backports` section of the + Release Tracking Issue. The format must be: `- [ ] #` (e.g., + `- [ ] #1234`). +2. When ready, comment `/process-backports` on the tracking issue to trigger + processing. + +### Method D: Release Tool CLI +You can use the release tool to add backports from your local checkout: +```shell +bazel run //tools/private/release -- add-backports [ ...] +``` +The `` can be: +* A PR number (e.g., `124` or `#124`) +* A PR URL (e.g., `https://github.com/bazel-contrib/rules_python/pull/124` + or `https://github.com/bazel-contrib/rules_python/pull/124/files`) +* Only URLs for the configured repository are accepted. + +### Failure Behavior +If a backport fails to process (e.g., due to cherry-pick conflicts): +* The failed backport checklist item will remain unchecked with + `status=error-`. +* You must resolve the conflict manually: checkout the release branch, + cherry-pick the PR, resolve conflicts, push to remote, and manually check + the box on the tracking issue checklist with `status=done` metadata. ## Patch release with cherry picks @@ -96,8 +141,8 @@ The fix being included is commit `deadbeef`. If multiple commits need to be applied, repeat the `git cherry-pick` step for each. -Once the release branch is in the desired state, use `git tag` to tag it, as -done with a release from head. Release automation will do the rest. +Once the release branch is in the desired state, comment `/create-rc` on the +tracking issue to tag it, as done with a release from head. ### Announcing releases @@ -130,6 +175,14 @@ The two points of no return are: If release steps fail _prior_ to those steps, then its OK to change the tag. You may need to manually delete the GitHub release. +## Manual Editing of Tracking Issue + +You can manually edit the Release Tracking Issue to control the release flow. +The checklist items use metadata suffix: `| key=value key2=value2`. + +* **Retry Prepare Release**: Reset the task to `- [ ] Prepare Release | status=awaiting-preparation`. +* **Force Task Done**: Check the box `- [x]` and add appropriate metadata (e.g. `status=done`). + ## Secrets ### PyPI user rules-python diff --git a/WORKSPACE b/WORKSPACE index 077ddb5e68..5a31274b21 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -17,13 +17,10 @@ workspace(name = "rules_python") # Everything below this line is used only for developing rules_python. Users # should not copy it to their WORKSPACE. -# Necessary so that Bazel 9 recognizes this as rules_python and doesn't try -# to load the version Bazel itself uses by default. -# buildifier: disable=duplicated-name -local_repository( - name = "rules_python", - path = ".", -) +# Workaround for Bazel 9 duplicate name issue in Gazelle. +load("//:workspace_bazel9.bzl", "bazel_9_workaround") + +bazel_9_workaround() load("//:internal_dev_deps.bzl", "rules_python_internal_deps") diff --git a/command_line_option/BUILD.bazel b/command_line_option/BUILD.bazel new file mode 100644 index 0000000000..15c22f360b --- /dev/null +++ b/command_line_option/BUILD.bazel @@ -0,0 +1,30 @@ +# Special placeholders for Bazel builtin //command_line_option psuedo-targets +# +# These are special targets to use with `py_binary.config_settings` that are +# treated as aliases for `//command_line_option:XXX` psuedo-targets. They +# are not actual flags or have any value. + +package( + default_visibility = ["//visibility:public"], +) + +alias( + name = "build_runfile_links", + actual = "//python:none", +) + +alias( + name = "enable_runfiles", + actual = "//python:none", +) + +alias( + name = "extra_toolchains", + actual = "//python:none", +) + +filegroup( + name = "distribution", + srcs = glob(["**"]), + visibility = ["//:__subpackages__"], +) diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel index b6c48b0539..2e13da1296 100644 --- a/docs/BUILD.bazel +++ b/docs/BUILD.bazel @@ -13,14 +13,14 @@ # limitations under the License. load("@bazel_skylib//rules:build_test.bzl", "build_test") -load("@dev_pip//:requirements.bzl", "requirement") +load("@sphinxdocs//sphinxdocs:readthedocs.bzl", "readthedocs_install") +load("@sphinxdocs//sphinxdocs:sphinx.bzl", "sphinx_build_binary", "sphinx_docs") +load("@sphinxdocs//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library") +load("@sphinxdocs//sphinxdocs:sphinx_stardoc.bzl", "sphinx_stardoc", "sphinx_stardocs") +load("//python:defs.bzl", "py_binary") load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility -load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER") # buildifier: disable=bzl-visibility +load("//python/private:common_labels.bzl", "labels") # buildifier: disable=bzl-visibility load("//python/uv:lock.bzl", "lock") # buildifier: disable=bzl-visibility -load("//sphinxdocs:readthedocs.bzl", "readthedocs_install") -load("//sphinxdocs:sphinx.bzl", "sphinx_build_binary", "sphinx_docs") -load("//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library") -load("//sphinxdocs:sphinx_stardoc.bzl", "sphinx_stardoc", "sphinx_stardocs") package(default_visibility = ["//:__subpackages__"]) @@ -38,6 +38,27 @@ _TARGET_COMPATIBLE_WITH = select({ "//conditions:default": ["@platforms//:incompatible"], }) if BZLMOD_ENABLED else ["@platforms//:incompatible"] +py_binary( + name = "merge_changelog", + srcs = ["merge_changelog.py"], + target_compatible_with = _TARGET_COMPATIBLE_WITH, + deps = [ + "//tools/private/release:changelog_news", + ], +) + +genrule( + name = "merged_changelog", + srcs = [ + "//:CHANGELOG.md", + "//news:news_files", + ], + outs = ["changelog.md"], + cmd = "$(location :merge_changelog) --changelog $(location //:CHANGELOG.md) --news-dir news --output $@", + target_compatible_with = _TARGET_COMPATIBLE_WITH, + tools = [":merge_changelog"], +) + # See README.md for instructions. Short version: # * `bazel run //docs:docs.serve` in a separate terminal # * `ibazel build //docs:docs` to automatically rebuild docs @@ -61,9 +82,9 @@ sphinx_docs( "html", ], renamed_srcs = { - "//:CHANGELOG.md": "changelog.md", "//:CONTRIBUTING.md": "contributing.md", - "//sphinxdocs/inventories:bazel_inventory": "bazel_inventory.inv", + ":merged_changelog": "changelog.md", + "@sphinxdocs//sphinxdocs/inventories:bazel_inventory": "bazel_inventory.inv", }, sphinx = ":sphinx-build", strip_prefix = package_name() + "/", @@ -73,7 +94,7 @@ sphinx_docs( ":bzl_api_docs", ":py_api_srcs", ":py_runtime_pair", - "//sphinxdocs/docs:docs_lib", + "@sphinxdocs//docs:docs_lib", ], ) @@ -85,55 +106,60 @@ build_test( sphinx_stardocs( name = "bzl_api_docs", srcs = [ - "//python:defs_bzl", - "//python:features_bzl", - "//python:packaging_bzl", - "//python:pip_bzl", - "//python:py_binary_bzl", - "//python:py_cc_link_params_info_bzl", - "//python:py_exec_tools_info_bzl", - "//python:py_exec_tools_toolchain_bzl", - "//python:py_executable_info_bzl", - "//python:py_library_bzl", - "//python:py_runtime_bzl", - "//python:py_runtime_info_bzl", - "//python:py_test_bzl", - "//python:repositories_bzl", - "//python/api:api_bzl", - "//python/api:attr_builders_bzl", - "//python/api:executables_bzl", - "//python/api:libraries_bzl", - "//python/api:rule_builders_bzl", - "//python/cc:py_cc_toolchain_bzl", - "//python/cc:py_cc_toolchain_info_bzl", - "//python/entry_points:py_console_script_binary_bzl", - "//python/local_toolchains:repos_bzl", - "//python/private:attr_builders_bzl", - "//python/private:builders_util_bzl", - "//python/private:py_binary_rule_bzl", - "//python/private:py_cc_toolchain_rule_bzl", - "//python/private:py_info_bzl", - "//python/private:py_library_rule_bzl", - "//python/private:py_runtime_rule_bzl", - "//python/private:py_test_rule_bzl", - "//python/private:rule_builders_bzl", - "//python/private/api:py_common_api_bzl", - "//python/private/pypi:config_settings_bzl", - "//python/private/pypi:env_marker_info_bzl", - "//python/private/pypi:pkg_aliases_bzl", - "//python/private/pypi:whl_config_setting_bzl", - "//python/private/pypi:whl_library_bzl", - "//python/uv:lock_bzl", - "//python/uv:uv_bzl", - "//python/uv:uv_toolchain_bzl", - "//python/uv:uv_toolchain_info_bzl", + "//python:defs", + "//python:features", + "//python:packaging", + "//python:pip", + "//python:proto", + "//python:py_binary", + "//python:py_cc_link_params_info", + "//python:py_exec_tools_info", + "//python:py_exec_tools_toolchain", + "//python:py_executable_info", + "//python:py_info", + "//python:py_library", + "//python:py_runtime", + "//python:py_runtime_info", + "//python:py_test", + "//python:repositories", + "//python/api", + "//python/api:attr_builders", + "//python/api:executables", + "//python/api:libraries", + "//python/api:rule_builders", + "//python/cc:py_cc_toolchain", + "//python/cc:py_cc_toolchain_info", + "//python/entry_points:py_console_script_binary", + "//python/extensions:config", + "//python/extensions:python", + "//python/local_toolchains:repos", + "//python/private:attr_builders", + "//python/private:builders", + "//python/private:builders_util", + "//python/private:py_binary_rule", + "//python/private:py_cc_toolchain_rule", + "//python/private:py_info", + "//python/private:py_library_rule", + "//python/private:py_runtime_rule", + "//python/private:py_test_rule", + "//python/private:rule_builders", + "//python/private/api:py_common_api", + "//python/private/pypi:config_settings", + "//python/private/pypi:env_marker_info", + "//python/private/pypi:pkg_aliases", + "//python/private/pypi:whl_config_setting", + "//python/private/pypi:whl_library", + "//python/private/zipapp:py_zipapp_rule", + "//python/uv", + "//python/uv:lock", + "//python/uv:uv_toolchain", + "//python/uv:uv_toolchain_info", + "//python/zipapp:py_zipapp_binary", + "//python/zipapp:py_zipapp_test", ] + ([ - # Bazel 6 + Stardoc isn't able to parse something about the python bzlmod extension - "//python/extensions:python_bzl", - ] if IS_BAZEL_7_OR_HIGHER else []) + ([ # This depends on @pythons_hub, which is only created under bzlmod, - "//python/extensions:pip_bzl", - ] if IS_BAZEL_7_OR_HIGHER and BZLMOD_ENABLED else []), + "//python/extensions:pip", + ] if BZLMOD_ENABLED else []), prefix = "api/rules_python/", tags = ["docs"], target_compatible_with = _TARGET_COMPATIBLE_WITH, @@ -141,7 +167,7 @@ sphinx_stardocs( sphinx_stardoc( name = "py_runtime_pair", - src = "//python/private:py_runtime_pair_rule_bzl", + src = "//python/private:py_runtime_pair_rule", prefix = "api/rules_python/", tags = ["docs"], target_compatible_with = _TARGET_COMPATIBLE_WITH, @@ -163,16 +189,22 @@ readthedocs_install( sphinx_build_binary( name = "sphinx-build", + config_settings = { + labels.BOOTSTRAP_IMPL: "script", + labels.VENVS_SITE_PACKAGES: "yes", + labels.PY_FREETHREADED: "yes", + labels.PYTHON_VERSION: "3.14", + }, target_compatible_with = _TARGET_COMPATIBLE_WITH, deps = [ - requirement("sphinx"), - requirement("sphinx_rtd_theme"), - requirement("myst_parser"), - requirement("readthedocs_sphinx_ext"), - requirement("typing_extensions"), - requirement("sphinx_autodoc2"), - requirement("sphinx_reredirects"), - "//sphinxdocs/src/sphinx_bzl", + "@dev_pip//myst_parser", + "@dev_pip//readthedocs_sphinx_ext", + "@dev_pip//sphinx", + "@dev_pip//sphinx_autodoc2", + "@dev_pip//sphinx_reredirects", + "@dev_pip//sphinx_rtd_theme", + "@dev_pip//typing_extensions", + "@sphinxdocs//sphinxdocs/src/sphinx_bzl", ], ) @@ -191,3 +223,12 @@ lock( python_version = "3.9", visibility = ["//:__subpackages__"], ) + +# Run bazel run //docs:uv_lock.update +lock( + name = "uv_lock", + srcs = ["pyproject.toml"], + out = "uv.lock", + python_version = "3.9", + visibility = ["//:__subpackages__"], +) diff --git a/docs/_includes/py_console_script_binary.md b/docs/_includes/py_console_script_binary.md index cae9f9f2f5..c9686f2eee 100644 --- a/docs/_includes/py_console_script_binary.md +++ b/docs/_includes/py_console_script_binary.md @@ -12,7 +12,8 @@ py_console_script_binary( ) ``` -#### Specifying extra dependencies +:::{rubric} Specifying extra dependencies +::: You can also specify extra dependencies and the exact script name you want to call. This is useful for tools like `flake8`, `pylint`, and `pytest`, which have plugin discovery methods and discover @@ -36,7 +37,8 @@ py_console_script_binary( ) ``` -#### Using a specific Python version +:::{rubric} Using a specific Python version +::: A specific Python version can be forced by passing the desired Python version, e.g. to force Python 3.9: ```starlark @@ -49,7 +51,8 @@ py_console_script_binary( ) ``` -#### Adding a Shebang Line +:::{rubric} Adding a Shebang Line +::: You can specify a shebang line for the generated binary. This is useful for Unix-like systems where the shebang line determines which interpreter is used to execute @@ -69,7 +72,8 @@ Note that to execute via the shebang line, you need to ensure the specified Python interpreter is available in the environment. -#### Using a specific Python Version directly from a Toolchain +:::{rubric} Using a specific Python Version directly from a Toolchain +::: :::{deprecated} 1.1.0 The toolchain-specific `py_binary` and `py_test` symbols are aliases to the regular rules. For example, `load("@python_versions//3.11:defs.bzl", "py_binary")` and `load("@python_versions//3.11:defs.bzl", "py_test")` are deprecated. diff --git a/docs/api/rules_python/command_line_option/index.md b/docs/api/rules_python/command_line_option/index.md new file mode 100644 index 0000000000..02d29206f5 --- /dev/null +++ b/docs/api/rules_python/command_line_option/index.md @@ -0,0 +1,76 @@ +:::{default-domain} bzl +::: +:::{bzl:currentfile} //command_line_option:BUILD.bazel +::: + +# //command_line_option + +This package provides special targets that correspond to the Bazel-builtin +`//command_line_option` psuedo-targets. These can be used with the {obj}`config_settings` +attribute on Python rules to transition specific command line flags for a target. + +:::{note} +These targets are not actual `alias()` targets, Starlark flags, nor are they the +actual builtin command line flags. They are regular targets that the +`config_settings` transition logic specially recognizes and handles as if they +were the builtin `//command_line_option` psuedo-targets. +::: + +While this package only provides a subset of builtin Bazel flags, additional +ones can be introduced by: + +* Define your own `@foo//command_line_flag:` target. It **must** be + in a top-level `command_line_flag` directory. +* Use {obj}`config.add_transition_setting` to make the rules transition on + the corresponding `//command_line_option:` builtin Bazel psuedo-target. + +:::{seealso} +The `config_settings` attribute documentation on: +* {obj}`py_binary.config_settings` +* {obj}`py_test.config_settings` +::: + +## build_runfile_links + +:::{bzl:target} build_runfile_links + +Special target for the Bazel-builtin `//command_line_option:build_runfile_links` flag. + +See the [Bazel documentation for --build_runfile_links](https://bazel.build/reference/command-line-reference#flag--build_runfile_links). + +The special value `INHERIT` can be specified to use the existing flag value. +::: + +## enable_runfiles + +:::{bzl:target} enable_runfiles + +Special target for the Bazel-builtin `//command_line_option:enable_runfiles` flag. + +See the [Bazel documentation for --enable_runfiles](https://bazel.build/reference/command-line-reference#flag--enable_runfiles). + +The special value `INHERIT` can be specified to use the existing flag value. +::: + +## extra_toolchains + +:::{bzl:target} extra_toolchains + +Special target for the Bazel-builtin `//command_line_option:extra_toolchains` +flag. + +See the [Bazel documentation for --extra_toolchains](https://bazel.build/reference/command-line-reference#flag--extra_toolchains). + +The special value `INHERIT` can be specified to use the existing flag value. + +:::{note} +Unlike the normal Bazel built-in flag, which accepts a list of labels, this +pseudo-flag must be specified as a single, comma-separated string when set +using the `config_settings` attribute. For example: + +```python +"//command_line_option:extra_toolchains": "//my/tc1,//my/tc2" +``` +::: +::: + diff --git a/docs/api/rules_python/python/cc/index.md b/docs/api/rules_python/python/cc/index.md index 2f4e3ae171..98d68dacd5 100644 --- a/docs/api/rules_python/python/cc/index.md +++ b/docs/api/rules_python/python/cc/index.md @@ -35,7 +35,7 @@ This target provides: * `CcInfo`: The C++ information about the Python ABI3 headers. -:::{versionadded} VERSION_NEXT_FEATURE +:::{versionadded} 1.7.0 The {obj}`features.headers_abi3` attribute can be used to detect if this target is available or not. ::: diff --git a/docs/api/rules_python/python/config_settings/index.md b/docs/api/rules_python/python/config_settings/index.md index 989ebf1128..78f8937a4d 100644 --- a/docs/api/rules_python/python/config_settings/index.md +++ b/docs/api/rules_python/python/config_settings/index.md @@ -24,6 +24,86 @@ This is a transition flag and will be removed in a subsequent release. :::: ::: +::::{bzl:flag} build_python_zip +Controls if a `py_binary/py_test` output is a self-executable zipapp. + +When enabled, the output of `py_binary` or `py_test` targets will be a +self-executable zipapp. + +:::{note} +This affects _all_ `py_binary` and `py_test` targets in the build, not +only the target(s) specified on the command line. +::: + +Values: +* `true` +* `false` + +This flag replaces the Bazel builtin `--build_python_zip` flag. + +:::{versionadded} 1.7.0 +::: +:::: + +::::{bzl:flag} debugger +A target for providing a custom debugger dependency. + +This flag is roughly equivalent to putting a target in `deps`. It allows +injecting a dependency into executables (`py_binary`, `py_test`) without having +to modify their deps. The expectation is it points to a target that provides an +alternative debugger (pudb, winpdb, debugpy, etc). + +* Must provide {obj}`PyInfo`. +* This dependency is only used for the target config, i.e. build tools don't + have it added. + +:::{note} +Setting this flag adds the debugger dependency, but doesn't automatically set +`PYTHONBREAKPOINT` to change `breakpoint()` behavior. +::: + +:::{versionadded} 1.9.0 +::: +:::: + +::::{bzl:flag} experimental_python_import_all_repositories +Controls whether repository directories are added to the import path. + +When enabled, the top-level directories in the runfiles root directory (which +are presumbed to be repository directories) are added to the Python import +search path. + +It's recommended to set this to **`false`** to avoid external dependencies +unexpectedly interferring with import searching. + +Values; +* `true` (default) +* `false` + +This flag replaces the Bazel builtin +`--experimental_python_import_all_repositories` flag. + +:::{versionadded} 1.7.0 +::: +:::: + +::::{bzl:flag} python_path +A fallback path to use for Python for particular legacy Windows-specific code paths. + +Deprecated, do not use. This flag is largely a no-op and was replaced by +toolchains. It only remains for some legacy Windows code-paths that will +be removed. + +This flag replaces the Bazel builtin `--python_path` flag. + +:::{deprecated} 1.7.0 +Use toolchains instead. +::: + +:::{versionadded} 1.7.0 +::: +:::: + :::{bzl:flag} python_version Determines the default hermetic Python toolchain version. This can be set to one of the values that `rules_python` maintains. @@ -33,6 +113,29 @@ one of the values that `rules_python` maintains. Parses the value of the `python_version` and transforms it into a `X.Y` value. ::: +::::{bzl:flag} incompatible_default_to_explicit_init_py +Controls if missing `__init__.py` files are generated or not. + +If false, `py_binary` and `py_test` will, for every `*.py` and `*.so` file, +create `__init__.py` files for the containing directory, and all parent +directories, that do not already have an `__init__.py` file. If true, this +behavior is disabled. + +It's recommended to disable this behavior to avoid surprising import effects +from directories being importable when they otherwise wouldn't be, and for +how it can interfere with implicit namespace packages. + +Values: +* `true`: do not generate missing `__init__.py` files +* `false` (default): generate missing `__init__.py` files + +This flag replaces the Bazel builtin +`--incompatible_default_to_explicit_init_py` flag. + +:::{versionadded} 1.7.0 +::: +:::: + :::{bzl:target} is_python_* config_settings to match Python versions @@ -66,7 +169,7 @@ If you need to match a version that isn't present, then you have two options: ) ``` -2. Use {obj}`python.single_override` to re-introduce the desired version so +2. Use {obj}`python.single_version_override` to re-introduce the desired version so that the corresponding `//python/config_setting:is_python_XXX` target is generated. ::: @@ -139,6 +242,44 @@ The `auto` value The `omit_if_generated_source` value was removed :::: +::::{bzl:flag} validate_test_main +Determines if `py_test` runs a build-time validation that its main module +actually runs tests. + +A common `py_test` pitfall is to define test classes or functions but forget +to add code that runs them (for example, assuming `py_test` automatically +invokes `unittest` or `pytest`). When that happens, the test does nothing and +silently passes. + +When enabled, a validation action statically analyzes the main module and +fails the build if it defines test classes or functions but its top-level body +is "inert" -- i.e. it only contains definitions, imports, assignments, and +docstrings, with nothing that actually runs tests (such as an +`if __name__ == "__main__":` guard that invokes a test runner). + +A module that defines no classes or functions at all (for example, one that +only imports other modules) is always allowed, since it isn't the +"defined some tests but forgot to run them" case this check targets. + +This is only applicable to `py_test` targets that have a `main` source file; +targets using `main_module` are not checked. + +Values: + +* `auto`: (default) Automatically decide the effective value; the current + behavior is `disabled`. +* `enabled`: Run the validation action. +* `disabled`: Don't run the validation action. + +:::{note} +Enabling this requires the exec tools toolchain (with an exec interpreter) to +be registered, which is the case for the default hermetic toolchains. +::: + +:::{versionadded} VERSION_NEXT_FEATURE +::: +:::: + ::::{bzl:flag} py_linux_libc Set what libc is used for the target platform. This will affect which whl binaries will be pulled and what toolchain will be auto-detected. Currently `rules_python` only supplies toolchains compatible with `glibc`. @@ -171,48 +312,6 @@ the values used when environment markers are resolved at build time. ::: :::: -::::{bzl:flag} pip_whl -Set what distributions are used in the `pip` integration. - -Values: -* `auto`: Prefer `whl` distributions if they are compatible with a target - platform, but fallback to `sdist`. This is the default. -* `only`: Only use `whl` distributions and error out if it is not available. -* `no`: Only use `sdist` distributions. The wheels will be built non-hermetically in the `whl_library` repository rule. -:::{versionadded} 0.33.0 -::: -:::: - -::::{bzl:flag} pip_whl_osx_arch -Set what wheel types we should prefer when building on the OSX platform. - -Values: -* `arch`: Prefer architecture specific wheels. -* `universal`: Prefer universal wheels that usually are bigger and contain binaries for both, Intel and ARM architectures in the same wheel. -:::{versionadded} 0.33.0 -::: -:::: - -::::{bzl:flag} pip_whl_glibc_version -Set the minimum `glibc` version that the `py_binary` using `whl` distributions from a PyPI index should support. - -Values: -* `""`: Select the lowest available version of each wheel giving you the maximum compatibility. This is the default. -* `X.Y`: The string representation of a `glibc` version. The allowed values depend on the `requirements.txt` lock file contents. -:::{versionadded} 0.33.0 -::: -:::: - -::::{bzl:flag} pip_whl_muslc_version -Set the minimum `muslc` version that the `py_binary` using `whl` distributions from a PyPI index should support. - -Values: -* `""`: Select the lowest available version of each wheel giving you the maximum compatibility. This is the default. -* `X.Y`: The string representation of a `muslc` version. The allowed values depend on the `requirements.txt` lock file contents. -:::{versionadded} 0.33.0 -::: -:::: - ::::{bzl:flag} pip_whl_osx_version Set the minimum `osx` version that the `py_binary` using `whl` distributions from a PyPI index should support. @@ -289,6 +388,22 @@ Values: :::: +::::{bzl:flag} venv +Determines which PyPI repository hub is used when resolving package dependencies. + +This flag is transitioned on automatically by executable targets (`py_binary`, `py_test`) +to select the appropriate concrete PyPI hub (e.g., when fallback or disjoint packages exist across multiple hubs). + +Values: +* `auto`: (default) Resolves dependencies using the fallback or first available hub. +* ``: Explicitly forces resolution of packages from the + specified concrete PyPI hub (corresponding to a + {obj}`pip.parse.hub_name` value). + +:::{versionadded} 2.2.0 +::: +:::: + ::::{bzl:flag} venvs_use_declare_symlink Determines if relative symlinks are created using `declare_symlink()` at build @@ -311,3 +426,16 @@ is created. :::{versionadded} 1.2.0 ::: :::: + + +## Removed Flags + +:::{versionremoved} 2.1.0 +The following flags were removed: + +* `pip_whl` +* `pip_whl_osx_arch` +* `pip_whl_glibc_version` +* `pip_whl_muslc_version` +::: + diff --git a/docs/conf.py b/docs/conf.py index 47ab378cfb..17b3c17106 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -7,7 +7,7 @@ copyright = "2023, The Bazel Authors" author = "Bazel" -# NOTE: These are overriden by -D flags via --//sphinxdocs:extra_defines +# NOTE: These are overriden by -D flags via --@sphinxdocs//sphinxdocs:extra_defines version = "0.0.0" release = version @@ -95,35 +95,49 @@ "pypi-dependencies.html": "pypi/index.html", } -# Adapted from the template code: -# https://github.com/readthedocs/readthedocs.org/blob/main/readthedocs/doc_builder/templates/doc_builder/conf.py.tmpl -if os.environ.get("READTHEDOCS") == "True": - # Must come first because it can interfere with other extensions, according - # to the original conf.py template comments - extensions.insert(0, "readthedocs_ext.readthedocs") - exclude_patterns = ["_includes/*"] templates_path = ["_templates"] primary_domain = None # The default is 'py', which we don't make much use of nitpicky = True +# Ignore nitpicks for missing cross-references to external objects. +# These are typically objects that aren't documented or aren't easily linked +# via intersphinx mapping, so we suppress warnings for them to keep the build clean. nitpick_ignore_regex = [ - # External xrefs aren't setup: ignore missing xref warnings - # External xrefs to sphinx isn't setup: ignore missing xref warnings - ("py:.*", "(sphinx|docutils|ast|enum|collections|typing_extensions).*"), + ("py:class", r"docutils\..*"), + ("py:obj", r"sphinx\.util\.docutils\..*"), + ("py:obj", r"sphinx\.util\.docfields\..*"), + ("py:class", r"sphinx\.util\.typing\..*"), + ("py:class", r"sphinx_bzl\.bzl\..*"), + ("py:class", r"typing_extensions\.TypeAlias"), + ("bzl:obj", r":current_py_cc_headers_abi3"), + ("bzl:obj", r":python"), + ("bzl:type", r"T"), + ("bzl:type", r"input_value"), + ("bzl:type", r"DepsetBuilder"), + ("bzl:type", r"RunfilesBuilder"), + ("bzl:type", r"BuiltinPyInfo"), + ("bzl:type", r".*SentinelInfo"), + ("bzl:type", r".*SphinxDocsLibraryInfo"), + ("bzl:type", r".*_SphinxRunInfo"), ] # --- Intersphinx configuration intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), + "sphinx": ("https://www.sphinx-doc.org/en/master", None), "bazel": ("https://bazel.build/", "bazel_inventory.inv"), } # --- Extlinks configuration extlinks = { - "gh-issue": (f"https://github.com/bazel-contrib/rules_python/issues/%s", "#%s issue"), - "gh-path": (f"https://github.com/bazel-contrib/rules_python/tree/main/%s", "%s"), - "gh-pr": (f"https://github.com/bazel-contrib/rules_python/pulls/%s", "#%s PR"), + "gh-issue": ( + "https://github.com/bazel-contrib/rules_python/issues/%s", + "#%s issue", + ), + "gh-path": ("https://github.com/bazel-contrib/rules_python/tree/main/%s", "%s"), + "gh-pr": ("https://github.com/bazel-contrib/rules_python/pull/%s", "#%s PR"), } # --- MyST configuration diff --git a/docs/coverage.md b/docs/coverage.md index 3c7d9e0cfc..41c95a1b75 100644 --- a/docs/coverage.md +++ b/docs/coverage.md @@ -37,6 +37,13 @@ first in the import path. If you find yourself in this situation, then you'll need to manually configure coverage (see below). ::: +:::{note} +The bundled `coverage` wheel set covers CPython 3.9 through 3.14 (with +freethreaded variants for 3.13+). For Python versions outside that range, +`configure_coverage_tool = True` is a silent no-op and `bazel coverage` will +produce empty lcov data; manually configure coverage (see below) instead. +::: + ## Manually configuring coverage To manually configure coverage support, you'll need to set the diff --git a/docs/devguide.md b/docs/devguide.md index e7870b5733..86a9d5b542 100644 --- a/docs/devguide.md +++ b/docs/devguide.md @@ -105,7 +105,7 @@ integration test. ``` bazel run //tools/private/update_deps:update_coverage_deps # for example: - # bazel run //tools/private/update_deps:update_coverage_deps 7.6.1 + # bazel run //tools/private/update_deps:update_coverage_deps 7.10.7 ``` ## Updating tool dependencies @@ -117,6 +117,7 @@ to have everything self-documented, we have a special target, of the requirement-updating scripts in sequence in one go. This can be done once per release as we prepare for releases. +(creating-backport-prs)= ## Creating Backport PRs The steps to create a backport PR are: diff --git a/docs/environment-variables.md b/docs/environment-variables.md index 4913e329e4..3eb62221c0 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -11,7 +11,7 @@ would be: python -Xaaa /path/to/file.py ``` -This feature is likely to be useful for the integration of debuggers. For example, +This feature is useful for the integration of debuggers. For example, it would be possible to configure `RULES_PYTHON_ADDITIONAL_INTERPRETER_ARGS` to be set to `/path/to/debugger.py --port 12344 --file`, resulting in the command executed being: @@ -22,12 +22,14 @@ python /path/to/debugger.py --port 12345 --file /path/to/file.py :::{seealso} The {bzl:obj}`interpreter_args` attribute. + +The guide on {any}`How to integrate a debugger` ::: :::{versionadded} 1.3.0 ::: -:::{versionchanged} VERSION_NEXT_FEATURE -Support added for {obj}`--bootstrap_impl=system_python`. +:::{versionchanged} 1.7.0 +Support added for {bzl:flag}`--bootstrap_impl=system_python`. ::: :::: @@ -50,29 +52,6 @@ When `1`, `rules_python` will warn users about deprecated functionality that wil be removed in a subsequent major `rules_python` version. Defaults to `0` if unset. ::: -::::{envvar} RULES_PYTHON_ENABLE_PYSTAR - -When `1`, the `rules_python` Starlark implementation of the core rules is used -instead of the Bazel-builtin rules. Note that this requires Bazel 7+. Defaults -to `1`. - -:::{versionadded} 0.26.0 -Defaults to `0` if unspecified. -::: -:::{versionchanged} 0.40.0 -The default became `1` if unspecified -::: -:::: - -::::{envvar} RULES_PYTHON_ENABLE_PIPSTAR - -When `1`, the `rules_python` Starlark implementation of the PyPI/pip integration is used -instead of the legacy Python scripts. - -:::{versionadded} 1.5.0 -::: -:::: - ::::{envvar} RULES_PYTHON_EXTRACT_ROOT Directory to use as the root for creating files necessary for bootstrapping so @@ -111,6 +90,47 @@ Valid values: * Other non-empty values mean to use isolated mode. ::: +:::{envvar} RULES_PYTHON_PYCACHE_DIR + +Determines the directory that runtime-generated pyc cache files will +be stored in. + +This directory may be reused between invocations, depending on the sandboxing +configuration. Setting it to `/dev/null` will, in effect, disable runtime +pyc caching. By setting e.g. +`--sandbox_add_mount_pair=/tmp/rules_python_pycache`, it's possible for pyc +caching to persist across invocations. + +**Behavior specific to downloaded runtimes:** +First `RULES_PYTHON_PYCACHE_DIR` is checked. If set, it is used as-is for +the root pycache directory. + +Otherwise, the following environment variables are checked in the following +order. Their values will have `rules_python_pycache` appended to them to form +the root pycache directory: +1. `XDG_CACHE_HOME`. +2. `TMP` (non-Windows) or `TEMP` (Windows). +3. The common platform-specific temporary directory (`/tmp` (non-Windows) or + `C:\Temp` (Windows)). + +If such a diretory cannot be found, or created, then `/dev/null` will be used, +which will effectively disable pyc caching. + +::: + +:::{envvar} RULES_PYTHON_PYPI_HUB_RESERVED + +When `1`, any PyPI hub named `"pypi"` will be renamed to `_pypi` +to prevent name collisions with the unified `@pypi` proxy repository, and +a warning is printed indicating that the renaming occurred. If not set (defaulting +to `0`), a warning is printed advising to rename the hub, and the collision +is not resolved. + +:::{versionadded} 2.2.0 +::: + +::: + :::{envvar} RULES_PYTHON_REPO_DEBUG When `1`, repository rules will print debug information about what they're @@ -141,3 +161,14 @@ os, arch values are the same as the ones mentioned in the When `1`, debug information about coverage behavior is printed to stderr. ::: + +## Removed Environment Variables + +:::{versionremoved} 2.1.0 +The following environment variables were removed: + +* `RULES_PYTHON_ENABLE_PYSTAR`: Used to enable the Starlark implementation of + core rules. +* `RULES_PYTHON_ENABLE_PIPSTAR`: Used to enable the Starlark implementation of + PyPI integration. +::: diff --git a/docs/getting-started.md b/docs/getting-started.md index d81d72f590..6494b37c51 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -11,6 +11,8 @@ For more detailed information about configuring `rules_python`, see: * [Configuring third-party dependencies (pip/PyPI)](./pypi/index) * [API docs](api/index) +A [screen recording](https://www.youtube.com/watch?v=Xtuh-WipOnk) on configuring rules_python for beginners is also available. + ## Including dependencies The first step to using `rules_python` is to add the dependency to diff --git a/docs/howto/common-deps-with-multiple-pypi-versions.md b/docs/howto/common-deps-with-multiple-pypi-versions.md index 3b933d22f4..f6887e3667 100644 --- a/docs/howto/common-deps-with-multiple-pypi-versions.md +++ b/docs/howto/common-deps-with-multiple-pypi-versions.md @@ -1,54 +1,100 @@ (common-deps-with-multiple-pypi-versions)= # How to use a common set of dependencies with multiple PyPI versions -In this guide, we show how to handle a situation common to monorepos -that extensively share code: How does a common library refer to the correct -`@pypi_` hub when binaries may have their own requirements (and thus -PyPI hub name)? Stated as code, this situation: +In this guide, we show how to handle a situation common to monorepos that +extensively share code: How do multiple binaries utilize distinct requirements +files while pulling from shared internal libraries, without requiring +manually-maintained `select()` logic inside dependency targets? -```bzl +Stated as code, consider this example: +```bzl +# When building bin_alpha, requests and more_itertools should resolve +# from requirements_alpha.txt py_binary( - name = "bin_alpha", - deps = ["@pypi_alpha//requests", ":common"], + name = "bin_alpha", + deps = [ + "@pypi//requests", + ":common", + ], ) + +# When building bin_beta, requests and more_itertools should resolve +# from requirements_beta.txt py_binary( - name = "bin_beta", - deps = ["@pypi_beta//requests", ":common"], + name = "bin_beta", + deps = [ + "@pypi//requests", + ":common", + ], ) +# Transitive dependencies like more_itertools are requested here, but +# must automatically match whichever dependency track is active for the binary. py_library( - name = "common", - deps = ["@pypi_???//more_itertools"] # <-- Which @pypi repo? + name = "common", + deps = ["@pypi//more_itertools"], ) ``` -## Using flags to pick a hub +## Defining dependency tracks via custom platforms -The basic trick to make `:common` pick the appropriate `@pypi_` is to use -`select()` to choose one based on build flags. To help this process, `py_binary` -et al allow forcing particular build flags to be used, and custom flags can be -registered to allow `py_binary` et al to set them. +The solution involves defining custom "platforms" mapped to separate +dependency tracks inside `MODULE.bazel`. Using custom platforms via +{obj}`pip.default` and associating requirements files to them through the +`requirements_by_platform` attribute on {obj}`pip.parse` instructs +`rules_python` to generate `select()` logic behind a unified hub. -In this example, we create a custom string flag named `//:pypi_hub`, -register it to allow using it with `py_binary` directly, then use `select()` -to pick different dependencies. +Binaries configure their execution requirements by forcing flag transition +attributes using custom build setting flags. -```bzl +In this example, we define custom string flag named `//:pypi_hub`, setup +distinct custom platforms for `"alpha"` and `"beta"` profiles, and register +associated requirements lock files grouped inside the `@pypi` hub. + +```starlark # File: MODULE.bazel +rules_python_config = use_extension( + "@rules_python//python/extensions:config.bzl", + "config", +) rules_python_config.add_transition_setting( setting = "//:pypi_hub", ) -# File: BUILD.bazel +pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") -```bzl +pip.default( + platform = "alpha", + config_settings = ["@//:is_pypi_alpha"], +) + +pip.default( + platform = "beta", + config_settings = ["@//:is_pypi_beta"], +) + +pip.parse( + hub_name = "pypi", + python_version = "3.14", + requirements_by_platform = { + "//:requirements_alpha.txt": "alpha", + "//:requirements_beta.txt": "beta", + }, +) + +use_repo(pip, "pypi") +``` + +```starlark +# File: BUILD.bazel load("@bazel_skylib//rules:common_settings.bzl", "string_flag") string_flag( name = "pypi_hub", + build_setting_default = "none", ) config_setting( @@ -58,7 +104,7 @@ config_setting( config_setting( name = "is_pypi_beta", - flag_values = {"//:pypi_hub": "beta"} + flag_values = {"//:pypi_hub": "beta"}, ) py_binary( @@ -67,26 +113,32 @@ py_binary( config_settings = { "//:pypi_hub": "alpha", }, - deps = ["@pypi_alpha//requests", ":common"], + deps = [ + "@pypi//requests", + ":common", + ], ) + py_binary( name = "bin_beta", srcs = ["bin_beta.py"], config_settings = { "//:pypi_hub": "beta", }, - deps = ["@pypi_beta//requests", ":common"], + deps = [ + "@pypi//requests", + ":common", + ], ) + py_library( name = "common", - deps = select({ - ":is_pypi_alpha": ["@pypi_alpha//more_itertools"], - ":is_pypi_beta": ["@pypi_beta//more_itertools"], - }), + deps = ["@pypi//more_itertools"], ) ``` -When `bin_alpha` and `bin_beta` are built, they will have the `pypi_hub` -flag force to their respective value. When `:common` is evaluated, it sees -the flag value of the binary that is consuming it, and the `select()` resolves -appropriately. +When building `bin_alpha` or `bin_beta`, they set `//:pypi_hub` via target +transitions. The generated aliased dependencies inside the `@pypi` hub will +evaluate that Bazel configuration, automatically delivering corresponding +Python wheels from targeted lock files. + diff --git a/docs/howto/debuggers.md b/docs/howto/debuggers.md new file mode 100644 index 0000000000..199a366675 --- /dev/null +++ b/docs/howto/debuggers.md @@ -0,0 +1,206 @@ +:::{default-domain} bzl +::: + +# How to integrate a debugger + +This guide explains how to integrate a debugger +with your Python applications built with `rules_python`. + +There are two ways available: the {obj}`--debugger` flag, and the {any}`RULES_PYTHON_ADDITIONAL_INTERPRETER_ARGS` environment variable. + +## {obj}`--debugger` flag + +### Basic Usage + +The {obj}`--debugger` flag allows you to inject an extra dependency into `py_test` +and `py_binary` targets so that they have a custom debugger available at +runtime. The flag is roughly equivalent to manually adding it to `deps` of +the target under test. + +To use the debugger, you typically provide the `--debugger` flag to your `bazel run` command. + +Example command line: + +```bash +bazel run --@rules_python//python/config_settings:debugger=@pypi//pudb \ + //path/to:my_python_binary +``` + +This will launch the Python program with the `@pypi//pudb` dependency added. + +The exact behavior (e.g., waiting for attachment, breaking at the first line) +depends on the specific debugger and its configuration. + +:::{note} +The specified target must be in the requirements.txt file used with +`pip.parse()` to make it available to Bazel. +::: + +### Python `PYTHONBREAKPOINT` Environment Variable + +For more fine-grained control over debugging, especially for programmatic breakpoints, +you can leverage the Python built-in `breakpoint()` function and the +`PYTHONBREAKPOINT` environment variable. + +The `breakpoint()` built-in function, available since Python 3.7, +can be called anywhere in your code to invoke a debugger. The `PYTHONBREAKPOINT` +environment variable can be set to specify which debugger to use. + +For example, to use `pdb` (the Python Debugger) when `breakpoint()` is called: + +```bash +PYTHONBREAKPOINT=pudb.set_trace bazel run \ + --@rules_python//python/config_settings:debugger=@pypi//pudb \ + //path/to:my_python_binary +``` + +For more details on `PYTHONBREAKPOINT`, refer to the [Python documentation](https://docs.python.org/3/library/functions.html#breakpoint). + +### Setting a default debugger + +By adding settings to your user or project `.bazelrc` files, you can have +these settings automatically added to your bazel invocations. e.g. + +``` +common --@rules_python//python/config_settings:debugger=@pypi//pudb +common --test_env=PYTHONBREAKPOINT=pudb.set_trace +``` + +Note that `--test_env` isn't strictly necessary. The `py_test` and `py_binary` +rules will respect the `PYTHONBREAKPOINT` environment variable in your shell. + +## debugpy (e.g. vscode) + +You can integrate `debugpy` (i.e. the debugger used in vscode or PyCharm) by using a launcher script. This method leverages {any}`RULES_PYTHON_ADDITIONAL_INTERPRETER_ARGS` to inject the debugger into the Bazel-managed Python process. + +For the remainder of this document, we assume you are using vscode. + +![VS Code debugpy demo](https://raw.githubusercontent.com/shayanhoshyari/issue-reports/refs/heads/main/rules_python/vscode_debugger/docs/demo.gif) + + +1. **Create a launcher script**: Save the following Python script as `.vscode/debugpy/launch.py` (or another location, adjusting `launch.json` accordingly). This script bridges VS Code's debugger with Bazel. + +
+ launch.py + + ```python + """ + Launcher script for VS Code (debugpy). + + This script is not managed by Bazel; it is invoked by VS Code's launch.json to + wrap the Bazel command, injecting the debugger into the runtime environment. + """ + + import argparse + import os + import shlex + import subprocess + import sys + from typing import cast + + def main() -> None: + parser = argparse.ArgumentParser(description="Launch bazel debugpy with test or run.") + parser.add_argument("mode", choices=["test", "run"], help="Choose whether to run a bazel test or run.") + parser.add_argument("args", help="The bazel target to test or run (e.g., //foo:bar) and any additional args") + args = parser.parse_args() + + # Import debugpy, provided by VS Code + try: + # debugpy._vendored is needed for force_pydevd to perform path manipulation. + import debugpy._vendored # type: ignore[import-not-found] + + # pydev_monkey patches os and subprocess functions to handle new launched processes. + from _pydev_bundle import pydev_monkey # type: ignore[import-not-found] + except ImportError as exc: + print(f"Error: This script must be run via VS Code's debug adapter. Details: {exc}") + sys.exit(-1) + + # Prepare arguments for the monkey-patched process. + # is_exec=False ensures we don't replace the current process immediately. + patched_args = cast(list[str], pydev_monkey.patch_args(["python", "dummy.py"], is_exec=False)) + pydev_monkey.send_process_created_message() + + # Extract the injected arguments (skipping the dummy python executable and script). + # These args invoke the pydevd entrypoint which connects back to the debugger. + rules_python_interpreter_args = " ".join(patched_args[1:-1]) + + bzl_args = shlex.split(args.args) + if not bzl_args: + print("Error: At least one argument (the target) is required.") + sys.exit(-1) + + cmd = [ + "bazel", + args.mode, + # Propagate environment variables to the test/run environment. + "--test_env=PYDEVD_RESOLVE_SYMLINKS", + "--test_env=RULES_PYTHON_ADDITIONAL_INTERPRETER_ARGS", + "--test_env=IDE_PROJECT_ROOTS", + bzl_args[0], + ] + + if bzl_args[1:]: + if args.mode == "run": + # Append extra arguments for 'run' mode. + cmd.append("--") + cmd.extend(bzl_args[1:]) + elif args.mode == "test": + # Append extra arguments for 'test' mode. + cmd.extend([f"--test_arg={arg}" for arg in bzl_args[1:]]) + + env = { + **os.environ.copy(), + # Inject the debugger arguments into the rules_python toolchain. + "RULES_PYTHON_ADDITIONAL_INTERPRETER_ARGS": rules_python_interpreter_args, + # Ensure breakpoints hit the original source files, not Bazel's symlinks. + "PYDEVD_RESOLVE_SYMLINKS": "1", + } + + # Execute Bazel. + result = subprocess.run(cmd, env=env, check=False) + sys.exit(result.returncode) + + if __name__ == "__main__": + main() + ``` +
+ +2. **Configure `launch.json`**: Add the following configurations to your `.vscode/launch.json`. This tells VS Code to use the launcher script. + +
+ launch.json + + ```json + { + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Bazel py run", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/.vscode/debugpy/launch.py", + "args": ["run", "${input:BazelArgs}"], + "console": "integratedTerminal" + }, + { + "name": "Python: Bazel py test", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/.vscode/debugpy/launch.py", + "args": ["test", "${input:BazelArgs}"], + "console": "integratedTerminal" + } + ], + "inputs": [ + { + "id": "BazelArgs", + "type": "promptString", + "description": "Bazel target and arguments (e.g., //foo:bar --my-arg)" + } + ] + } + ``` +
+ + Note: If you find `justMyCode` behavior is incompatible with Bazel's symlinks (causing breakpoints to be missed), you can set `"justMyCode": false` in `launch.json` and use the `IDE_PROJECT_ROOTS` environment variable (set to `"${workspaceFolder}"`) to explicitly map your workspace. + diff --git a/docs/howto/multi-platform-pypi-deps.md b/docs/howto/multi-platform-pypi-deps.md index 6cc7f842ea..f329ccdf9e 100644 --- a/docs/howto/multi-platform-pypi-deps.md +++ b/docs/howto/multi-platform-pypi-deps.md @@ -48,7 +48,7 @@ Additional dimensions should be appended and separated with an underscore (e.g. `linux_x86_64_musl_cuda12.9_numpy2`). The platform name should not include the Python version. That is handled by -`pip.parse.python_version` separately. +{attr}`pip.parse.python_version` separately. :::{note} The term _platform_ here has nothing to do with Bazel's `platform()` rule. @@ -56,7 +56,7 @@ The term _platform_ here has nothing to do with Bazel's `platform()` rule. #### Defining custom settings -Because {obj}`pip.parse.config_settings` is a list of arbitrary `config_setting` +Because {attr}`pip.default.config_settings` is a list of arbitrary `config_setting` targets, you can define your own flags or implement custom config matching logic. This allows you to model settings that aren't inherently part of rules_python. @@ -85,7 +85,7 @@ contains commonly used settings for OS and CPU: * `@platforms//cpu:aarch64` Note that these are the raw flag names. In order to use them with `pip.default`, -you must use {obj}`config_setting()` to match a particular value for them. +you must use {obj}`config_setting` to match a particular value for them. ### Associating Requirements to Platforms @@ -163,16 +163,16 @@ pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") # A custom platform for CUDA on glibc linux pip.default( platform = "linux_x86_64_cuda12.9", - os = "linux", - cpu = "x86_64", + arch_name = "x86_64", + os_name = "linux", config_settings = ["@//:is_cuda_12_9"], ) # A custom platform for musl on linux pip.default( platform = "linux_aarch64_musl", - os = "linux", - cpu = "aarch64", + os_name = "linux", + arch_name = "aarch64", config_settings = ["@//:is_musl"], ) diff --git a/docs/index.md b/docs/index.md index 7f03681b76..0204f38ec7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,7 +5,7 @@ :::{topic} Core rules The core Python rules -- `py_library`, `py_binary`, `py_test`, -`py_proto_library`, and related symbols that provide the basis for Python +and related symbols that provide the basis for Python support in Bazel. When using Bazel 6 (or earlier), the core rules are bundled into the Bazel binary, and the symbols diff --git a/docs/merge_changelog.py b/docs/merge_changelog.py new file mode 100644 index 0000000000..47af98b2a2 --- /dev/null +++ b/docs/merge_changelog.py @@ -0,0 +1,38 @@ +"""A tool to merge news entries into CHANGELOG.md for documentation preview.""" + +import argparse +import pathlib + +from tools.private.release import changelog_news + + +def main(): + parser = argparse.ArgumentParser(description="Merge news entries into CHANGELOG.md") + parser.add_argument( + "--changelog", type=pathlib.Path, required=True, help="Path to CHANGELOG.md" + ) + parser.add_argument( + "--news-dir", type=pathlib.Path, required=True, help="Path to news directory" + ) + parser.add_argument( + "--output", + type=pathlib.Path, + required=True, + help="Path to output merged changelog", + ) + args = parser.parse_args() + + # Call the public merge_new_into_changelog function + # Using deterministic date "0000-00-00" and version "0.0.0" + changelog_news.merge_new_into_changelog( + changelog_path=args.changelog, + output_path=args.output, + news_dir=args.news_dir, + version="unreleased", + release_date="0000-00-00", + delete_news=False, + ) + + +if __name__ == "__main__": + main() diff --git a/docs/pypi/download-workspace.md b/docs/pypi/download-workspace.md index 5dfb0f257a..4912f71103 100644 --- a/docs/pypi/download-workspace.md +++ b/docs/pypi/download-workspace.md @@ -41,7 +41,6 @@ re-executed to pick up a non-hermetic change to your environment (e.g., updating your system `python` interpreter), you can force it to re-execute by running `bazel sync --only [pip_parse name]`. -(per-os-arch-requirements)= ## Requirements for a specific OS/Architecture In some cases, you may need to use different requirements files for different OS and architecture combinations. diff --git a/docs/pypi/download.md b/docs/pypi/download.md index 7f4e205d84..e819ed0791 100644 --- a/docs/pypi/download.md +++ b/docs/pypi/download.md @@ -50,6 +50,114 @@ You can use the pip extension multiple times. This configuration will create multiple external repos that have no relation to one another and may result in downloading the same wheels numerous times. +(unified-pypi-hub)= +## Unified `@pypi` Hub for Multi-Hub Configurations + +:::{versionadded} 2.2.0 +Unified `@pypi` hub repository for Bzlmod multi-hub configurations. +::: + +When you call the `pip` extension multiple times with different `hub_name` +attributes, `rules_python` automatically generates a unified `@pypi` hub +repository (unless one of your concrete hubs is explicitly named `"pypi"`). + +This unified `@pypi` repository acts as a dynamic proxy that routes package +dependencies to the active concrete hub at build time. This is especially +useful in monorepos where shared library targets need to depend on PyPI +packages without knowing which specific hub or requirements lock file the +consuming binary is using. + +#### Reserved `"pypi"` Hub Name + +The hub name `"pypi"` is **reserved** for the automatically generated unified +hub repository. Defining a concrete hub named `"pypi"` will cause a collision. + +For details on how this collision is handled and resolved via environment +variables, see the {envvar}`RULES_PYTHON_PYPI_HUB_RESERVED` documentation. + +#### Configuring the Unified Hub + +To configure the unified hub, define your concrete hubs as usual, and +optionally designate a default hub using the `pip.default` tag's +`default_hub` attribute: + +```starlark +pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") + +# Define concrete hub 'pypi_a' +pip.parse( + hub_name = "pypi_a", + python_version = "3.11", + requirements_lock = "//:requirements_a.txt", +) + +# Define concrete hub 'pypi_b' +pip.parse( + hub_name = "pypi_b", + python_version = "3.11", + requirements_lock = "//:requirements_b.txt", +) + +# Designate 'pypi_b' as the default hub for the unified '@pypi' repository +pip.default(default_hub = "pypi_b") + +# Import the unified hub repository +use_repo(pip, "pypi") +``` + +#### Dynamic Routing at Build Time + +By default, the unified `@pypi` repository will resolve packages from the +designated `default_hub`. You can dynamically switch the active hub for a build +using the `--@rules_python//python/config_settings:venv` command-line flag +or via target transitions: + +```bash +# Build using packages from 'pypi_a' +bazel build --@rules_python//python/config_settings:venv=pypi_a //my:binary +``` + +Shared library targets can simply depend on the unified hub (e.g., +`@pypi//numpy`), and the dependency will automatically resolve to the correct +wheel version from the active hub during the build. + +### Declaring Abstract Dependencies (pip.dep) + +:::{versionadded} 2.2.0 +Declaring abstract PyPI dependencies via `pip.dep` tags. +::: + +Sometimes a shared library target or a ruleset needs to depend on a PyPI +package (e.g., `@pypi//numpy`), but does not want to force a specific package +version or a concrete `requirements.txt` lock file on its consumers. + +Instead of calling `pip.parse()`, the module can declare its dependency using +the `pip.dep` tag: + +```starlark +pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") + +# Declare an abstract dependency on 'numpy' and specify extra targets that +# are expected to be available in the package. +pip.dep( + name = "numpy", + extra_targets = ["extra-alias"], +) +``` + +This ensures that the target structure `@pypi//numpy` (and +`@pypi//numpy:extra-alias`) exists in the unified `@pypi` hub repository, so the +declaring module can compile and analyze successfully without needing any local +requirements file. + +The actual concrete implementation and version of the package must be provided +by a downstream module calling `pip.parse`. + +If a downstream module attempts to build a target that depends on an abstract +dependency, but has not provided a concrete implementation for it via any +`pip.parse` call, the build will fail at execution time. + + As with any repository rule or extension, if you would like to ensure that `pip_parse` is re-executed to pick up a non-hermetic change to your environment (e.g., updating your system `python` interpreter), you can force it to re-execute by running `bazel sync --only [pip_parse @@ -104,15 +212,29 @@ the years, people started needing support for building containers, and usually, fetching dependencies for a particular target platform that may be different from the host platform. -Multi-platform support for cross-building the wheels can be done in two ways: -1. using {attr}`experimental_index_url` for the {bzl:obj}`pip.parse` bzlmod tag class -2. using the {attr}`pip.parse.download_only` setting. +Multi-platform support for cross-building the wheels can be done by +using {attr}`target_platforms` for the {bzl:obj}`pip.parse` bzlmod tag class :::{warning} This will not work for sdists with C extensions, but pure Python sdists may still work using the first approach. ::: +By default, `rules_python` selects the host `{os}_{arch}` platform from its `MODULE.bazel` +file. This means that `rules_python` by default does not provide cross-platform building support +because some packages have very large wheels and users should be able to use `bazel query` with +minimal overhead. As a result, users should configure their `pip.parse` +calls and select which platforms they want to target via the +{attr}`pip.parse.target_platforms` attribute: +```starlark + # Example of enabling free threaded and non-freethreaded switching on the host platform: + target_platforms = ["{os}_{arch}", "{os}_{arch}_freethreaded"], + + # As another example, to enable building for `linux_x86_64` containers and the host platform: + # target_platforms = ["{os}_{arch}", "linux_x86_64"], +) +``` + ### Using `download_only` attribute Let's say you have two requirements files: @@ -168,11 +290,6 @@ available on the PyPI index that you use. ### Customizing `Requires-Dist` resolution -:::{note} -Currently this is disabled by default, but you can turn it on using -{envvar}`RULES_PYTHON_ENABLE_PIPSTAR` environment variable. -::: - In order to understand what dependencies to pull for a particular package, `rules_python` parses the `whl` file [`METADATA`][metadata]. Packages can express dependencies via `Requires-Dist`, and they can add conditions using @@ -197,16 +314,6 @@ additional keys, which become available during dependency evaluation. (bazel-downloader)= ### Bazel downloader and multi-platform wheel hub repository. -:::{warning} -This is currently still experimental, and whilst it has been proven to work in quite a few -environments, the APIs are still being finalized, and there may be changes to the APIs for this -feature without much notice. - -The issues that you can subscribe to for updates are: -* {gh-issue}`260` -* {gh-issue}`1357` -::: - The {obj}`pip` extension supports pulling information from `PyPI` (or a compatible mirror), and it will ensure that the [bazel downloader][bazel_downloader] is used for downloading the wheels. @@ -218,14 +325,10 @@ This provides the following benefits: * Allow using transitions and targeting free-threaded and musl platforms more easily. * Avoids `pip` for wheel fetching and results in much faster dependency fetching. -To enable the feature specify {attr}`pip.parse.experimental_index_url` as shown in -the {gh-path}`examples/bzlmod/MODULE.bazel` example. - -Similar to [uv](https://docs.astral.sh/uv/configuration/indexes/), one can override the -index that is used for a single package. By default, we first search in the index specified by -{attr}`pip.parse.experimental_index_url`, then we iterate through the -{attr}`pip.parse.experimental_extra_index_urls` unless there are overrides specified via -{attr}`pip.parse.experimental_index_url_overrides`. +Similar to [uv](https://docs.astral.sh/uv/configuration/indexes/), one can override the index that +is used for a single package. By default, we first search in the indexes specified by +`--extra-index-url`, then we fall back to the `--index-url` setting unless there are overrides +specified via {attr}`pip.parse.experimental_index_url_overrides`. When using this feature during the `pip` extension evaluation you will see the accessed indexes similar to below: ```console @@ -249,16 +352,58 @@ that by parsing the `whl` filename based on [PEP600], [PEP656] standards. This allows the user to configure the behaviour by using the following publicly available flags: * {obj}`--@rules_python//python/config_settings:py_linux_libc` for selecting the Linux libc variant. -* {obj}`--@rules_python//python/config_settings:pip_whl` for selecting `whl` distribution preference. -* {obj}`--@rules_python//python/config_settings:pip_whl_osx_arch` for selecting MacOS wheel preference. -* {obj}`--@rules_python//python/config_settings:pip_whl_glibc_version` for selecting the GLIBC version compatibility. -* {obj}`--@rules_python//python/config_settings:pip_whl_muslc_version` for selecting the musl version compatibility. -* {obj}`--@rules_python//python/config_settings:pip_whl_osx_version` for selecting MacOS version compatibility. [bazel_downloader]: https://bazel.build/rules/lib/builtins/repository_ctx#download [pep600]: https://peps.python.org/pep-0600/ [pep656]: https://peps.python.org/pep-0656/ +## Internal dependencies and private repositories + +The `rules_python` Bazel module downloads Python interpreters and +dependencies as part of its functionality. These artifacts are fetched +using Bazel's internal HTTP downloader, not using the `pip` tool. + +If you are in a network-restricted environment and must use internal +registries, you can configure the Bazel downloader to redirect all of +these downloads to a different registry. + +Example of a `bazel_downloader.cfg`: +```cfg +all_blocked_message See internal.mirror.lan/registry/ for more information +allow s3.amazon.com + +# Rewrite everything to files.pythonhosted to the internal mirror with two +# capture groups: the first group matches the host and is appended first, +# the second matches the entire path and is appended second +rewrite (files.pythonhosted.org)/(.*) internal.mirror.lan/python/$1/$2 +rewrite (pypi.python.org)/(.*) internal.mirror.lan/python/$1/$2 + +# Allow the internal mirror and block everything else +allow internal.mirror.lan +block * +``` + +Use the config file with `--experimental_downloader_config=bazel_downloader.cfg`. + +### How the config is parsed: + +* Uses Java regular expressions +* Matching is performed only on host and path components of the URL, not the scheme +* Directives are applied in the following order: `rewrite, allow, block` +* Back references are numbered starting from `$1` +* Expressions must match the entire string being tested, not just find a substring. + +If your patterns don't seem to match or rewrite: + +* Begin with simple patterns to ensure they match as expected. +* Be cautious when using `block` statements to avoid unintentionally blocking necessary downloads. Add `block` statements incrementally and test thoroughly after each change. + +### References: + +* [Configuring Bazel's Downloader](https://blog.aspect.build/configuring-bazels-downloader) +* [URLRewriterConfig.java Source Code](https://github.com/bazelbuild/bazel/blob/master/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/UrlRewriterConfig.java) +* [Issue 3519](https://github.com/bazel-contrib/rules_python/issues/3519) + (credential-helper)= ## Credential Helper @@ -297,6 +442,7 @@ into whatever HTTP(S) request it performs against `example.com`. See the [Credential Helper Spec][cred-helper-spec] for more details. + [rfc7617]: https://datatracker.ietf.org/doc/html/rfc7617 [cred-helper-design]: https://github.com/bazelbuild/proposals/blob/main/designs/2022-06-07-bazel-credential-helpers.md [cred-helper-spec]: https://github.com/EngFlow/credential-helper-spec/blob/main/spec.md diff --git a/docs/pypi/index.md b/docs/pypi/index.md index 17928898c5..2d17dd5054 100644 --- a/docs/pypi/index.md +++ b/docs/pypi/index.md @@ -1,6 +1,7 @@ :::{default-domain} bzl ::: +(pypi-dependencies)= # Using PyPI Using PyPI packages (aka "pip install") involves the following main steps: diff --git a/docs/pypi/lock.md b/docs/pypi/lock.md index b5d8ec24f7..5c9f0646dc 100644 --- a/docs/pypi/lock.md +++ b/docs/pypi/lock.md @@ -68,8 +68,55 @@ compile_pip_requirements( ### uv pip compile (bzlmod only) -We also have experimental setup for the `uv pip compile` way of generating lock files. +We also have an experimental setup for the `uv pip compile` way of generating lock files. This is well tested with the public PyPI index, but you may hit some rough edges with private mirrors. -For more documentation see {obj}`lock` documentation. +#### Example usage + +```starlark +load("@rules_python//python/uv:lock.bzl", "lock") + +lock( + name = "requirements", + srcs = ["pyproject.toml", "requirements.in"], + out = "requirements_lock.txt", +) +``` + +#### `[tool.uv]` settings from pyproject.toml + +When a `pyproject.toml` file is among the {attr}`lock.srcs`, the +{obj}`lock` rule auto-detects the project directory and passes +`--project ` to `uv pip compile`. This causes `uv` to read +`[tool.uv]` settings from that `pyproject.toml`, such as +`no-build-isolation`, `exclude-dependencies`, and workspace members. + +If multiple `pyproject.toml` files are in {attr}`lock.srcs`, the one +with the shortest directory path is selected (this heuristic works for +typical uv workspace layouts where the root configuration is at the +shortest path). + +If the auto-detection picks the wrong project directory, use the +`project` parameter to override: + +```starlark +lock( + name = "requirements", + srcs = ["pyproject.toml", "requirements.in"], + out = "requirements_lock.txt", + project = "subproject", +) +``` + +:::{warning} +**Known limitations of auto-detection** + +1. **Workspace heuristic** — the shortest-path selection may incorrectly assume the upper-most + workspace `pyproject.toml` is the correct one. For monorepos with multiple independent + sub-projects, you must set `project` explicitly for each {obj}`lock` target. +1. **No test target** — unlike {obj}`compile_pip_requirements`, no test target is auto-created; see + the {obj}`lock` docs for how to add one manually using `diff_test` from `bazel_skylib`. +::: + +For more documentation see {obj}`lock`. diff --git a/docs/pyproject.toml b/docs/pyproject.toml index 9a089df59c..f0ca3928ff 100644 --- a/docs/pyproject.toml +++ b/docs/pyproject.toml @@ -13,5 +13,8 @@ dependencies = [ "absl-py", "typing-extensions", "sphinx-reredirects", - "pefile" + "pefile", + "pyelftools", + "macholib", + "markupsafe", ] diff --git a/docs/readthedocs_build.sh b/docs/readthedocs_build.sh index ec5390bfc7..cd60792a3f 100755 --- a/docs/readthedocs_build.sh +++ b/docs/readthedocs_build.sh @@ -5,16 +5,16 @@ set -eou pipefail declare -a extra_env while IFS='=' read -r -d '' name value; do if [[ "$name" == READTHEDOCS* ]]; then - extra_env+=("--//sphinxdocs:extra_env=$name=$value") + extra_env+=("--@sphinxdocs//sphinxdocs:extra_env=$name=$value") fi done < <(env -0) # In order to get the build number, we extract it from the host name -extra_env+=("--//sphinxdocs:extra_env=HOSTNAME=$HOSTNAME") +extra_env+=("--@sphinxdocs//sphinxdocs:extra_env=HOSTNAME=$HOSTNAME") set -x bazel run \ --config=rtd \ - "--//sphinxdocs:extra_defines=version=$READTHEDOCS_VERSION" \ + "--@sphinxdocs//sphinxdocs:extra_defines=version=$READTHEDOCS_VERSION" \ "${extra_env[@]}" \ //docs:readthedocs_install diff --git a/docs/requirements.txt b/docs/requirements.txt index c05554aeeb..468cd38c0d 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -2,10 +2,14 @@ # bazel run //docs:requirements.update --index-url https://pypi.org/simple -absl-py==2.3.1 \ +absl-py==2.3.1 ; python_full_version < '3.10' \ --hash=sha256:a97820526f7fbfd2ec1bce83f3f25e3a14840dac0d8e02a0b71cd75db3f77fc9 \ --hash=sha256:eeecf07f0c2a93ace0772c92e596ace6d3d3996c042b2128459aaae2a76de11d # via rules-python-docs (docs/pyproject.toml) +absl-py==2.4.0 ; python_full_version >= '3.10' \ + --hash=sha256:88476fd881ca8aab94ffa78b7b6c632a782ab3ba1cd19c9bd423abc4fb4cd28d \ + --hash=sha256:8c6af82722b35cf71e0f4d1d47dcaebfff286e27110a99fc359349b247dfb5d4 + # via rules-python-docs (docs/pyproject.toml) alabaster==0.7.16 ; python_full_version < '3.10' \ --hash=sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65 \ --hash=sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92 @@ -14,121 +18,186 @@ alabaster==1.0.0 ; python_full_version >= '3.10' \ --hash=sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e \ --hash=sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b # via sphinx +altgraph==0.17.5 \ + --hash=sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7 \ + --hash=sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597 + # via macholib astroid==3.3.11 \ --hash=sha256:1e5a5011af2920c7c67a53f65d536d65bfa7116feeaf2354d8b94f29573bb0ce \ --hash=sha256:54c760ae8322ece1abd213057c4b5bba7c49818853fc901ef09719a60dbf9dec # via sphinx-autodoc2 -babel==2.17.0 \ - --hash=sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d \ - --hash=sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2 +babel==2.18.0 \ + --hash=sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d \ + --hash=sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35 # via sphinx -certifi==2025.8.3 \ - --hash=sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407 \ - --hash=sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5 +certifi==2026.6.17 \ + --hash=sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432 \ + --hash=sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db # via requests -charset-normalizer==3.4.3 \ - --hash=sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91 \ - --hash=sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0 \ - --hash=sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154 \ - --hash=sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601 \ - --hash=sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884 \ - --hash=sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07 \ - --hash=sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c \ - --hash=sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64 \ - --hash=sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe \ - --hash=sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f \ - --hash=sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432 \ - --hash=sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc \ - --hash=sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa \ - --hash=sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9 \ - --hash=sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae \ - --hash=sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19 \ - --hash=sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d \ - --hash=sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e \ - --hash=sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4 \ - --hash=sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7 \ - --hash=sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312 \ - --hash=sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92 \ - --hash=sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31 \ - --hash=sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c \ - --hash=sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f \ - --hash=sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99 \ - --hash=sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b \ - --hash=sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15 \ - --hash=sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392 \ - --hash=sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f \ - --hash=sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8 \ - --hash=sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491 \ - --hash=sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0 \ - --hash=sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc \ - --hash=sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0 \ - --hash=sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f \ - --hash=sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a \ - --hash=sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40 \ - --hash=sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927 \ - --hash=sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849 \ - --hash=sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce \ - --hash=sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14 \ - --hash=sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05 \ - --hash=sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c \ - --hash=sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c \ - --hash=sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a \ - --hash=sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc \ - --hash=sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34 \ - --hash=sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9 \ - --hash=sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096 \ - --hash=sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14 \ - --hash=sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30 \ - --hash=sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b \ - --hash=sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b \ - --hash=sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942 \ - --hash=sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db \ - --hash=sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5 \ - --hash=sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b \ - --hash=sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce \ - --hash=sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669 \ - --hash=sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0 \ - --hash=sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018 \ - --hash=sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93 \ - --hash=sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe \ - --hash=sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049 \ - --hash=sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a \ - --hash=sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef \ - --hash=sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2 \ - --hash=sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca \ - --hash=sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16 \ - --hash=sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f \ - --hash=sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb \ - --hash=sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1 \ - --hash=sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557 \ - --hash=sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37 \ - --hash=sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7 \ - --hash=sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72 \ - --hash=sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c \ - --hash=sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9 +charset-normalizer==3.4.7 \ + --hash=sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc \ + --hash=sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c \ + --hash=sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67 \ + --hash=sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4 \ + --hash=sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0 \ + --hash=sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c \ + --hash=sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5 \ + --hash=sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444 \ + --hash=sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153 \ + --hash=sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9 \ + --hash=sha256:16d971e29578a5e97d7117866d15889a4a07befe0e87e703ed63cd90cb348c01 \ + --hash=sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217 \ + --hash=sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b \ + --hash=sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c \ + --hash=sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a \ + --hash=sha256:1dc8b0ea451d6e69735094606991f32867807881400f808a106ee1d963c46a83 \ + --hash=sha256:1efde3cae86c8c273f1eb3b287be7d8499420cf2fe7585c41d370d3e790054a5 \ + --hash=sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7 \ + --hash=sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb \ + --hash=sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c \ + --hash=sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1 \ + --hash=sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42 \ + --hash=sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab \ + --hash=sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df \ + --hash=sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e \ + --hash=sha256:320ade88cfb846b8cd6b4ddf5ee9e80ee0c1f52401f2456b84ae1ae6a1a5f207 \ + --hash=sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18 \ + --hash=sha256:36836d6ff945a00b88ba1e4572d721e60b5b8c98c155d465f56ad19d68f23734 \ + --hash=sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38 \ + --hash=sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110 \ + --hash=sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18 \ + --hash=sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44 \ + --hash=sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d \ + --hash=sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48 \ + --hash=sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e \ + --hash=sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5 \ + --hash=sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d \ + --hash=sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53 \ + --hash=sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790 \ + --hash=sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c \ + --hash=sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b \ + --hash=sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116 \ + --hash=sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d \ + --hash=sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10 \ + --hash=sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6 \ + --hash=sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2 \ + --hash=sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776 \ + --hash=sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a \ + --hash=sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265 \ + --hash=sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008 \ + --hash=sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943 \ + --hash=sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374 \ + --hash=sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246 \ + --hash=sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e \ + --hash=sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5 \ + --hash=sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616 \ + --hash=sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15 \ + --hash=sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41 \ + --hash=sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960 \ + --hash=sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752 \ + --hash=sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e \ + --hash=sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72 \ + --hash=sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7 \ + --hash=sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8 \ + --hash=sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b \ + --hash=sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4 \ + --hash=sha256:82b271f5137d07749f7bf32f70b17ab6eaabedd297e75dce75081a24f76eb545 \ + --hash=sha256:84c018e49c3bf790f9c2771c45e9313a08c2c2a6342b162cd650258b57817706 \ + --hash=sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366 \ + --hash=sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb \ + --hash=sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a \ + --hash=sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e \ + --hash=sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00 \ + --hash=sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f \ + --hash=sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a \ + --hash=sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1 \ + --hash=sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66 \ + --hash=sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356 \ + --hash=sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319 \ + --hash=sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4 \ + --hash=sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad \ + --hash=sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d \ + --hash=sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5 \ + --hash=sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7 \ + --hash=sha256:aef65cd602a6d0e0ff6f9930fcb1c8fec60dd2cfcb6facaf4bdb0e5873042db0 \ + --hash=sha256:af21eb4409a119e365397b2adbaca4c9ccab56543a65d5dbd9f920d6ac29f686 \ + --hash=sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34 \ + --hash=sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49 \ + --hash=sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c \ + --hash=sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1 \ + --hash=sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e \ + --hash=sha256:bd9b23791fe793e4968dba0c447e12f78e425c59fc0e3b97f6450f4781f3ee60 \ + --hash=sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0 \ + --hash=sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274 \ + --hash=sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d \ + --hash=sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0 \ + --hash=sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae \ + --hash=sha256:c593052c465475e64bbfe5dbd81680f64a67fdc752c56d7a0ae205dc8aeefe0f \ + --hash=sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d \ + --hash=sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe \ + --hash=sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3 \ + --hash=sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393 \ + --hash=sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1 \ + --hash=sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af \ + --hash=sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44 \ + --hash=sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00 \ + --hash=sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c \ + --hash=sha256:dca4bbc466a95ba9c0234ef56d7dd9509f63da22274589ebd4ed7f1f4d4c54e3 \ + --hash=sha256:dd915403e231e6b1809fe9b6d9fc55cf8fb5e02765ac625d9cd623342a7905d7 \ + --hash=sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd \ + --hash=sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e \ + --hash=sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b \ + --hash=sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8 \ + --hash=sha256:e5f4d355f0a2b1a31bc3edec6795b46324349c9cb25eed068049e4f472fb4259 \ + --hash=sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859 \ + --hash=sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46 \ + --hash=sha256:e80c8378d8f3d83cd3164da1ad2df9e37a666cdde7b1cb2298ed0b558064be30 \ + --hash=sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b \ + --hash=sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46 \ + --hash=sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24 \ + --hash=sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a \ + --hash=sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24 \ + --hash=sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc \ + --hash=sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215 \ + --hash=sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063 \ + --hash=sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832 \ + --hash=sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6 \ + --hash=sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79 \ + --hash=sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464 # via requests colorama==0.4.6 ; sys_platform == 'win32' \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 # via sphinx -docutils==0.22.2 \ - --hash=sha256:9fdb771707c8784c8f2728b67cb2c691305933d68137ef95a75db5f4dfbc213d \ - --hash=sha256:b0e98d679283fc3bb0ead8a5da7f501baa632654e7056e9c5846842213d674d8 +docutils==0.21.2 ; python_full_version < '3.11' \ + --hash=sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f \ + --hash=sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2 # via # myst-parser # sphinx # sphinx-rtd-theme -idna==3.10 \ - --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ - --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 +docutils==0.22.4 ; python_full_version >= '3.11' \ + --hash=sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968 \ + --hash=sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de + # via + # myst-parser + # sphinx + # sphinx-rtd-theme +idna==3.18 \ + --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ + --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 # via requests -imagesize==1.4.1 \ - --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \ - --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a +imagesize==1.5.0 ; python_full_version < '3.10' \ + --hash=sha256:32677681b3f434c2cb496f00e89c5a291247b35b1f527589909e008057da5899 \ + --hash=sha256:8bfc5363a7f2133a89f0098451e0bcb1cd71aba4dc02bbcecb39d99d40e1b94f + # via sphinx +imagesize==2.0.0 ; python_full_version >= '3.10' \ + --hash=sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96 \ + --hash=sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3 # via sphinx -importlib-metadata==8.7.0 ; python_full_version < '3.10' \ - --hash=sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000 \ - --hash=sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd +importlib-metadata==8.7.1 ; python_full_version < '3.10' \ + --hash=sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb \ + --hash=sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151 # via sphinx jinja2==3.1.6 \ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ @@ -137,82 +206,122 @@ jinja2==3.1.6 \ # myst-parser # readthedocs-sphinx-ext # sphinx -markdown-it-py==3.0.0 \ +macholib==1.16.4 \ + --hash=sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea \ + --hash=sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362 + # via rules-python-docs (docs/pyproject.toml) +markdown-it-py==3.0.0 ; python_full_version < '3.11' \ --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb # via # mdit-py-plugins # myst-parser -markupsafe==3.0.2 \ - --hash=sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4 \ - --hash=sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30 \ - --hash=sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0 \ - --hash=sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9 \ - --hash=sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396 \ - --hash=sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13 \ - --hash=sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028 \ - --hash=sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca \ - --hash=sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557 \ - --hash=sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832 \ - --hash=sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0 \ - --hash=sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b \ - --hash=sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579 \ - --hash=sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a \ - --hash=sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c \ - --hash=sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff \ - --hash=sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c \ - --hash=sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22 \ - --hash=sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094 \ - --hash=sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb \ - --hash=sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e \ - --hash=sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5 \ - --hash=sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a \ - --hash=sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d \ - --hash=sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a \ - --hash=sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b \ - --hash=sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8 \ - --hash=sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225 \ - --hash=sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c \ - --hash=sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144 \ - --hash=sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f \ - --hash=sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87 \ - --hash=sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d \ - --hash=sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93 \ - --hash=sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf \ - --hash=sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158 \ - --hash=sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84 \ - --hash=sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb \ - --hash=sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48 \ - --hash=sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171 \ - --hash=sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c \ - --hash=sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6 \ - --hash=sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd \ - --hash=sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d \ - --hash=sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1 \ - --hash=sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d \ - --hash=sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca \ - --hash=sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a \ - --hash=sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29 \ - --hash=sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe \ - --hash=sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798 \ - --hash=sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c \ - --hash=sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8 \ - --hash=sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f \ - --hash=sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f \ - --hash=sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a \ - --hash=sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178 \ - --hash=sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0 \ - --hash=sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79 \ - --hash=sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430 \ - --hash=sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50 - # via jinja2 +markdown-it-py==4.2.0 ; python_full_version >= '3.11' \ + --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ + --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a + # via + # mdit-py-plugins + # myst-parser +markupsafe==3.0.3 \ + --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ + --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ + --hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \ + --hash=sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19 \ + --hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \ + --hash=sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c \ + --hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \ + --hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \ + --hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \ + --hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \ + --hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \ + --hash=sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26 \ + --hash=sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1 \ + --hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \ + --hash=sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218 \ + --hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \ + --hash=sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695 \ + --hash=sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad \ + --hash=sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73 \ + --hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \ + --hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \ + --hash=sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa \ + --hash=sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559 \ + --hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \ + --hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \ + --hash=sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758 \ + --hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \ + --hash=sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8 \ + --hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \ + --hash=sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c \ + --hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \ + --hash=sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a \ + --hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \ + --hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \ + --hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \ + --hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \ + --hash=sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2 \ + --hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \ + --hash=sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354 \ + --hash=sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50 \ + --hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \ + --hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \ + --hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \ + --hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \ + --hash=sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115 \ + --hash=sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e \ + --hash=sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485 \ + --hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \ + --hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \ + --hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \ + --hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \ + --hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \ + --hash=sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b \ + --hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \ + --hash=sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 \ + --hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \ + --hash=sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d \ + --hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \ + --hash=sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287 \ + --hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \ + --hash=sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f \ + --hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \ + --hash=sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed \ + --hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \ + --hash=sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c \ + --hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \ + --hash=sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8 \ + --hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \ + --hash=sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6 \ + --hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \ + --hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \ + --hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d \ + --hash=sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01 \ + --hash=sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7 \ + --hash=sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419 \ + --hash=sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795 \ + --hash=sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1 \ + --hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \ + --hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \ + --hash=sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42 \ + --hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \ + --hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \ + --hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \ + --hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \ + --hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \ + --hash=sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591 \ + --hash=sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc \ + --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ + --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 + # via + # rules-python-docs (docs/pyproject.toml) + # jinja2 mdit-py-plugins==0.4.2 ; python_full_version < '3.10' \ --hash=sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636 \ --hash=sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5 # via myst-parser -mdit-py-plugins==0.5.0 ; python_full_version >= '3.10' \ - --hash=sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f \ - --hash=sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6 +mdit-py-plugins==0.6.1 ; python_full_version >= '3.10' \ + --hash=sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d \ + --hash=sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0 # via myst-parser mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ @@ -222,13 +331,17 @@ myst-parser==3.0.1 ; python_full_version < '3.10' \ --hash=sha256:6457aaa33a5d474aca678b8ead9b3dc298e89c68e67012e73146ea6fd54babf1 \ --hash=sha256:88f0cb406cb363b077d176b51c476f62d60604d68a8dcdf4832e080441301a87 # via rules-python-docs (docs/pyproject.toml) -myst-parser==4.0.1 ; python_full_version >= '3.10' \ +myst-parser==4.0.1 ; python_full_version == '3.10.*' \ --hash=sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4 \ --hash=sha256:9134e88959ec3b5780aedf8a99680ea242869d012e8821db3126d427edc9c95d # via rules-python-docs (docs/pyproject.toml) -packaging==25.0 \ - --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ - --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f +myst-parser==5.1.0 ; python_full_version >= '3.11' \ + --hash=sha256:9c91c52b3cdb4d94a6506e4fab4e2f296c7623a0da0dcbe6de1565c3dad67a8a \ + --hash=sha256:ab69322dc6719dcc7f296479dbb70181b66df6ed315064f92dbc85c0e1bf2f02 + # via rules-python-docs (docs/pyproject.toml) +packaging==26.2 \ + --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ + --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 # via # readthedocs-sphinx-ext # sphinx @@ -236,82 +349,116 @@ pefile==2024.8.26 \ --hash=sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632 \ --hash=sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f # via rules-python-docs (docs/pyproject.toml) -pygments==2.19.2 \ - --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ - --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b +pyelftools==0.32 ; python_full_version < '3.10' \ + --hash=sha256:013df952a006db5e138b1edf6d8a68ecc50630adbd0d83a2d41e7f846163d738 \ + --hash=sha256:6de90ee7b8263e740c8715a925382d4099b354f29ac48ea40d840cf7aa14ace5 + # via rules-python-docs (docs/pyproject.toml) +pyelftools==0.33 ; python_full_version >= '3.10' \ + --hash=sha256:660d82dcbeb8e83d1702bd97f223f761625da06111c0cc988eac6b8ab0c1b61f \ + --hash=sha256:f215ad5f47d3f1373a21496a6c9e0707c622840d0622f23ff7ce08678b020036 + # via rules-python-docs (docs/pyproject.toml) +pygments==2.20.0 \ + --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ + --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 # via sphinx -pyyaml==6.0.2 \ - --hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \ - --hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \ - --hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \ - --hash=sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e \ - --hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \ - --hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \ - --hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \ - --hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \ - --hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \ - --hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \ - --hash=sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a \ - --hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \ - --hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \ - --hash=sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8 \ - --hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \ - --hash=sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19 \ - --hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \ - --hash=sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a \ - --hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \ - --hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \ - --hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \ - --hash=sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631 \ - --hash=sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d \ - --hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \ - --hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \ - --hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \ - --hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \ - --hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \ - --hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \ - --hash=sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706 \ - --hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \ - --hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \ - --hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \ - --hash=sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083 \ - --hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \ - --hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \ - --hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \ - --hash=sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f \ - --hash=sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725 \ - --hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \ - --hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \ - --hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \ - --hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \ - --hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \ - --hash=sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5 \ - --hash=sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d \ - --hash=sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290 \ - --hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \ - --hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \ - --hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \ - --hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \ - --hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \ - --hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4 +pyyaml==6.0.3 \ + --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ + --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ + --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ + --hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \ + --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \ + --hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \ + --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \ + --hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \ + --hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \ + --hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \ + --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \ + --hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \ + --hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \ + --hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \ + --hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \ + --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \ + --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \ + --hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \ + --hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \ + --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \ + --hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \ + --hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \ + --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \ + --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \ + --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \ + --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \ + --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \ + --hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \ + --hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \ + --hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \ + --hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \ + --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \ + --hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \ + --hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \ + --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \ + --hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \ + --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \ + --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \ + --hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \ + --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \ + --hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \ + --hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \ + --hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \ + --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \ + --hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \ + --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \ + --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \ + --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \ + --hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \ + --hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \ + --hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \ + --hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \ + --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \ + --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \ + --hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \ + --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \ + --hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \ + --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \ + --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \ + --hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \ + --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \ + --hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \ + --hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \ + --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \ + --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \ + --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ + --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \ + --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \ + --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \ + --hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \ + --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ + --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ + --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 # via myst-parser readthedocs-sphinx-ext==2.2.5 \ --hash=sha256:ee5fd5b99db9f0c180b2396cbce528aa36671951b9526bb0272dbfce5517bd27 \ --hash=sha256:f8c56184ea011c972dd45a90122568587cc85b0127bc9cf064d17c68bc809daa # via rules-python-docs (docs/pyproject.toml) -requests==2.32.5 \ +requests==2.32.5 ; python_full_version < '3.10' \ --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf # via # readthedocs-sphinx-ext # sphinx -roman-numerals-py==3.1.0 ; python_full_version >= '3.11' \ - --hash=sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c \ - --hash=sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d +requests==2.34.2 ; python_full_version >= '3.10' \ + --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ + --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed + # via + # readthedocs-sphinx-ext + # sphinx +roman-numerals==4.1.0 ; python_full_version >= '3.11' \ + --hash=sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2 \ + --hash=sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7 # via sphinx -snowballstemmer==3.0.1 \ - --hash=sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064 \ - --hash=sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895 +snowballstemmer==3.1.1 \ + --hash=sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752 \ + --hash=sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260 # via sphinx sphinx==7.4.7 ; python_full_version < '3.10' \ --hash=sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe \ @@ -331,9 +478,18 @@ sphinx==8.1.3 ; python_full_version == '3.10.*' \ # sphinx-reredirects # sphinx-rtd-theme # sphinxcontrib-jquery -sphinx==8.2.3 ; python_full_version >= '3.11' \ - --hash=sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348 \ - --hash=sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3 +sphinx==9.0.4 ; python_full_version == '3.11.*' \ + --hash=sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3 \ + --hash=sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb + # via + # rules-python-docs (docs/pyproject.toml) + # myst-parser + # sphinx-reredirects + # sphinx-rtd-theme + # sphinxcontrib-jquery +sphinx==9.1.0 ; python_full_version >= '3.12' \ + --hash=sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb \ + --hash=sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978 # via # rules-python-docs (docs/pyproject.toml) # myst-parser @@ -348,13 +504,13 @@ sphinx-reredirects==0.1.6 ; python_full_version < '3.11' \ --hash=sha256:c491cba545f67be9697508727818d8626626366245ae64456fe29f37e9bbea64 \ --hash=sha256:efd50c766fbc5bf40cd5148e10c00f2c00d143027de5c5e48beece93cc40eeea # via rules-python-docs (docs/pyproject.toml) -sphinx-reredirects==1.0.0 ; python_full_version >= '3.11' \ - --hash=sha256:1d0102710a8f633c6c885f940f440f7195ada675c1739976f0135790747dea06 \ - --hash=sha256:7c9bada9f1330489fcf4c7297a2d6da2a49ca4877d3f42d1388ae1de1019bf5c +sphinx-reredirects==1.1.0 ; python_full_version >= '3.11' \ + --hash=sha256:4b5692273c72cd2d4d917f4c6f87d5919e4d6114a752d4be033f7f5f6310efd9 \ + --hash=sha256:fb9b195335ab14b43f8273287d0c7eeb637ba6c56c66581c11b47202f6718b29 # via rules-python-docs (docs/pyproject.toml) -sphinx-rtd-theme==3.0.2 \ - --hash=sha256:422ccc750c3a3a311de4ae327e82affdaf59eb695ba4936538552f3b00f4ee13 \ - --hash=sha256:b7457bc25dda723b20b086a670b9953c859eab60a2a03ee8eb2bb23e176e5f85 +sphinx-rtd-theme==3.1.0 \ + --hash=sha256:1785824ae8e6632060490f67cf3a72d404a85d2d9fc26bce3619944de5682b89 \ + --hash=sha256:b44276f2c276e909239a4f6c955aa667aaafeb78597923b1c60babc76db78e4c # via rules-python-docs (docs/pyproject.toml) sphinxcontrib-applehelp==2.0.0 \ --hash=sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1 \ @@ -384,39 +540,54 @@ sphinxcontrib-serializinghtml==2.0.0 \ --hash=sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331 \ --hash=sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d # via sphinx -tomli==2.2.1 ; python_full_version < '3.11' \ - --hash=sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6 \ - --hash=sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd \ - --hash=sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c \ - --hash=sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b \ - --hash=sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8 \ - --hash=sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6 \ - --hash=sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77 \ - --hash=sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff \ - --hash=sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea \ - --hash=sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192 \ - --hash=sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249 \ - --hash=sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee \ - --hash=sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4 \ - --hash=sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98 \ - --hash=sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8 \ - --hash=sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4 \ - --hash=sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281 \ - --hash=sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744 \ - --hash=sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69 \ - --hash=sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13 \ - --hash=sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140 \ - --hash=sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e \ - --hash=sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e \ - --hash=sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc \ - --hash=sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff \ - --hash=sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec \ - --hash=sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2 \ - --hash=sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222 \ - --hash=sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106 \ - --hash=sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272 \ - --hash=sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a \ - --hash=sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7 +tomli==2.4.1 ; python_full_version < '3.11' \ + --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ + --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ + --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \ + --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \ + --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \ + --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \ + --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \ + --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \ + --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \ + --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \ + --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \ + --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \ + --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \ + --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \ + --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \ + --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \ + --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \ + --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \ + --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \ + --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \ + --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \ + --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \ + --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \ + --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \ + --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \ + --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \ + --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \ + --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \ + --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \ + --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \ + --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \ + --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \ + --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \ + --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \ + --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \ + --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \ + --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \ + --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \ + --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \ + --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \ + --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \ + --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \ + --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \ + --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \ + --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \ + --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \ + --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049 # via # sphinx # sphinx-autodoc2 @@ -427,11 +598,15 @@ typing-extensions==4.15.0 \ # rules-python-docs (docs/pyproject.toml) # astroid # sphinx-autodoc2 -urllib3==2.5.0 \ - --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ - --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc +urllib3==2.6.3 ; python_full_version < '3.10' \ + --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ + --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 + # via requests +urllib3==2.7.0 ; python_full_version >= '3.10' \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 # via requests -zipp==3.23.0 ; python_full_version < '3.10' \ - --hash=sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e \ - --hash=sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166 +zipp==3.23.1 ; python_full_version < '3.10' \ + --hash=sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc \ + --hash=sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110 # via importlib-metadata diff --git a/docs/support.md b/docs/support.md index 8728540804..088466d1bb 100644 --- a/docs/support.md +++ b/docs/support.md @@ -1,9 +1,10 @@ # Support Policy -The Bazel community maintains this repository. Neither Google nor the Bazel team -provides support for the code. However, this repository is part of the test -suite used to vet new Bazel releases. See the -page for information on our development workflow. +This is a community maintained project run by volunteers. What that means in +practice is: + +* Responses to issues and PRs depend on available time and energy. +* If you care, contribute (see ) and get involved. ## Supported rules_python Versions @@ -25,10 +26,9 @@ patch 1.4, version 1.5 must be patched first. Backports can be requested by [creating an issue with the patch release template][patch-release-issue] or by sending a pull request performing the backport. -See the dev guide for [how to create a backport PR][backport-pr]. +See the dev guide for [how to create a backport PR](creating-backport-prs). [patch-release-issue]: https://github.com/bazelbuild/rules_python/issues/new?template=patch_release_request.md -[backport-pr]: devguide.html#creating-backport-prs ## Supported Bazel Versions @@ -61,8 +61,9 @@ are Linux, Mac, and Windows. In order to better describe different support levels, the following acts as a rough guideline for different platform tiers: -* Tier 0 - The platforms that our CI runs on: `linux_x86_64`, `osx_x86_64`, `RBE linux_x86_64`. -* Tier 1 - The platforms that are similar enough to what the CI runs on: `linux_aarch64`, `osx_arm64`. +* Tier 0 - The platforms that our CI runs on: `linux_x86_64`, `osx_arm64`, `RBE linux_x86_64`. +* Tier 1 - The platforms that are similar enough to what the CI runs on: `linux_aarch64`, + `osx_x86_64`. What is more, `windows_x86_64` is in this list, as we run tests in CI, but developing for Windows is more challenging, and features may come later to this platform. diff --git a/docs/toolchains.md b/docs/toolchains.md index 52e619a120..98534aee63 100644 --- a/docs/toolchains.md +++ b/docs/toolchains.md @@ -242,6 +242,9 @@ existing attributes: {attr}`python.single_version_platform_override.coverage_tool`. * Adding additional Python versions via {bzl:obj}`python.single_version_override` or {bzl:obj}`python.single_version_platform_override`. +* Adding additional Python versions dynamically from a manifest file or URL + via {attr}`python.override.add_runtime_manifest_files` or + {attr}`python.override.add_runtime_manifest_urls`. ### Registering custom runtimes @@ -310,6 +313,73 @@ Added support for custom platform names, `target_compatible_with`, and `target_settings` with `single_version_platform_override`. ::: +### Registering runtimes from a manifest + +If you want to register multiple custom runtimes or versions at once, you can +use a python-build-standalone manifest file. This is useful if you want to +adopt new versions that are not yet built into `rules_python` without having +to manually define each one using `single_version_platform_override`. + +To do this, specify the `add_runtime_manifest_files` or +`add_runtime_manifest_urls` (and `runtime_manifest_sha`) attributes in +`python.override` in your `MODULE.bazel`. + +In the example below, we register all runtimes available in a specific local +or remote PBS release manifest: + +```starlark +# File: MODULE.bazel +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.override( + add_runtime_manifest_files = [ + "@//:SHA256SUMS", + ], + add_runtime_manifest_urls = [ + "https://github.com/astral-sh/python-build-standalone/releases/download/20260414/SHA256SUMS", + ], + base_url = "https://example.com/downloads", + runtime_manifest_sha = "ce18fdfd47c66830a40ea9b9e314a14b1636bbfd684501bc5ca1fc6d55a7933f", +) +``` + +#### Manifest file format + +The manifest must be a plain text file where each line contains the SHA256 hash +and the location of a runtime archive, separated by whitespace: + +``` + +``` + +The `` can be either: +- A relative filename (e.g., + `cpython-3.10.20+20260414-x86_64-unknown-linux-gnu-install_only.tar.zst`). + In this case, the download URL is constructed by appending the filename to + the `base_url` attribute (if using `add_runtime_manifest_files`) or to the + parent directory of each URL in `add_runtime_manifest_urls` (treating them + as mirrors). +- An absolute URL (e.g., + `https://example.com/downloads/cpython-3.10.20+20260414-x86_64-unknown-linux-gnu-install_only.tar.zst`). + In this case, the URL is used directly to download the archive. + +In both cases, the filename or the last path segment of the URL must follow +the standard python-build-standalone naming convention. `rules_python` parses +this name to extract runtime metadata (such as Python version, target +architecture, operating system, and libc). + +Notes: +- `rules_python` will read or download the manifest, parse it, and + automatically register toolchains for all valid Python runtimes found in it + that match supported platforms. +- Only runtimes matching known platforms in `rules_python` will be registered. + +:::{versionadded} 2.1.0 +Added support for registering runtimes from a manifest using +`add_runtime_manifest_files`, `add_runtime_manifest_urls`, and +`runtime_manifest_sha` in `python.override`. +::: + + ### Using defined toolchains from WORKSPACE It is possible to use toolchains defined in `MODULE.bazel` in `WORKSPACE`. For example, @@ -460,6 +530,10 @@ local_runtime_toolchains_repo( register_toolchains("@local_toolchains//:all", dev_dependency = True) ``` +In the example above, `interpreter_path` is used to find Python via `PATH` +lookups. Alternatively, {obj}`interpreter_target` can be set, which can +refer to a Python in an arbitrary Bazel repository. + :::{important} Be sure to set `dev_dependency = True`. Using a local toolchain only makes sense for the root module. @@ -551,14 +625,14 @@ Python is used to run a program but also makes it easy to use a Python version that isn't compatible with build-time assumptions. ``` -register_toolchains("@rules_python//python/runtime_env_toolchains:all") +`register_toolchains`("@rules_python//python/runtime_env_toolchains:all") ``` Note that this toolchain has no constraints, i.e. it will match any platform, Python version, etc. :::{seealso} -[Local toolchain], which creates a more full featured toolchain from a +[Local `toolchain`], which creates a more full featured toolchain from a locally installed Python. ::: @@ -842,3 +916,30 @@ The [`//python/bin:repl` target](repl) provides an environment identical to what `py_binary` provides. That means it handles things like the [`PYTHONSAFEPATH`](https://docs.python.org/3/using/cmdline.html#envvar-PYTHONSAFEPATH) environment variable automatically. The `//python/bin:python` target will not. + +## Consuming Python C headers and libraries + +The following targets expose the headers and libraries from the +currently selected Python C toolchain: + +- {obj}`@rules_python//python/cc:current_py_cc_headers` +- {obj}`@rules_python//python/cc:current_py_cc_headers_abi3` +- {obj}`@rules_python//python/cc:current_py_cc_libs` + +These targets behave similarly to a `cc_library`, but instead of defining +their own sources, they forward providers from the underlying toolchain- +selected `cc_library`. + +A Python C toolchain must be registered for these targets to work. +Under bzlmod, a toolchain is registered automatically. In non-bzlmod +setups, users must ensure that a toolchain is explicitly registered. + +Users should depend on these targets instead of legacy alias targets +when embedding Python or building C extensions, as this ensures +compatibility across different toolchain configurations. + + +:::{seealso} +The _How to get Python headres for C extensions_ how-to guide, and the +{obj}`//python/cc:BUILD.bazel` package API documentation. +::: diff --git a/docs/uv.lock b/docs/uv.lock new file mode 100644 index 0000000000..72e8f307a9 --- /dev/null +++ b/docs/uv.lock @@ -0,0 +1,1163 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", + "python_full_version < '3.10'", +] + +[[package]] +name = "absl-py" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/10/2a/c93173ffa1b39c1d0395b7e842bbdc62e556ca9d8d3b5572926f3e4ca752/absl_py-2.3.1.tar.gz", hash = "sha256:a97820526f7fbfd2ec1bce83f3f25e3a14840dac0d8e02a0b71cd75db3f77fc9", size = 116588, upload-time = "2025-07-03T09:31:44.05Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/aa/ba0014cc4659328dc818a28827be78e6d97312ab0cb98105a770924dc11e/absl_py-2.3.1-py3-none-any.whl", hash = "sha256:eeecf07f0c2a93ace0772c92e596ace6d3d3996c042b2128459aaae2a76de11d", size = 135811, upload-time = "2025-07-03T09:31:42.253Z" }, +] + +[[package]] +name = "absl-py" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/64/c7/8de93764ad66968d19329a7e0c147a2bb3c7054c554d4a119111b8f9440f/absl_py-2.4.0.tar.gz", hash = "sha256:8c6af82722b35cf71e0f4d1d47dcaebfff286e27110a99fc359349b247dfb5d4", size = 116543, upload-time = "2026-01-28T10:17:05.322Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl", hash = "sha256:88476fd881ca8aab94ffa78b7b6c632a782ab3ba1cd19c9bd423abc4fb4cd28d", size = 135750, upload-time = "2026-01-28T10:17:04.19Z" }, +] + +[[package]] +name = "alabaster" +version = "0.7.16" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", size = 23776, upload-time = "2024-01-10T00:56:10.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511, upload-time = "2024-01-10T00:56:08.388Z" }, +] + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, +] + +[[package]] +name = "altgraph" +version = "0.17.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/f8/97fdf103f38fed6792a1601dbc16cc8aac56e7459a9fff08c812d8ae177a/altgraph-0.17.5.tar.gz", hash = "sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7", size = 48428, upload-time = "2025-11-21T20:35:50.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/ba/000a1996d4308bc65120167c21241a3b205464a2e0b58deda26ae8ac21d1/altgraph-0.17.5-py2.py3-none-any.whl", hash = "sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597", size = 21228, upload-time = "2025-11-21T20:35:49.444Z" }, +] + +[[package]] +name = "astroid" +version = "3.3.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/74/dfb75f9ccd592bbedb175d4a32fc643cf569d7c218508bfbd6ea7ef9c091/astroid-3.3.11.tar.gz", hash = "sha256:1e5a5011af2920c7c67a53f65d536d65bfa7116feeaf2354d8b94f29573bb0ce", size = 400439, upload-time = "2025-07-13T18:04:23.177Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/0f/3b8fdc946b4d9cc8cc1e8af42c4e409468c84441b933d037e101b3d72d86/astroid-3.3.11-py3-none-any.whl", hash = "sha256:54c760ae8322ece1abd213057c4b5bba7c49818853fc901ef09719a60dbf9dec", size = 275612, upload-time = "2025-07-13T18:04:21.07Z" }, +] + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "certifi" +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, + { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, + { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, + { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, + { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, + { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, + { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, + { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, + { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, + { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, + { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/ef725f8eb19b5a261b30f78efa9252ef9d017985cb499102f6f49834cd12/charset_normalizer-3.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217", size = 299121, upload-time = "2026-04-02T09:28:14.372Z" }, + { url = "https://files.pythonhosted.org/packages/a3/22/2f12878fbc680fbbb52386cd39a379801f62eaca74fc8b323381325f0f04/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5", size = 200612, upload-time = "2026-04-02T09:28:16.162Z" }, + { url = "https://files.pythonhosted.org/packages/bc/b6/10c84e789126ca97d4a7228863a30481e786980a8b8cfcbf4f30658ca63c/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9", size = 221041, upload-time = "2026-04-02T09:28:17.554Z" }, + { url = "https://files.pythonhosted.org/packages/21/7b/c414866a138400b2e81973d006da7f694cfeaf895ef07d2cba9a8743841a/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a", size = 216323, upload-time = "2026-04-02T09:28:18.863Z" }, + { url = "https://files.pythonhosted.org/packages/2e/92/bdcf94997e06b223d826df3abed45a5ad6e17f609b7df9d25cd23b5bde30/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc", size = 208419, upload-time = "2026-04-02T09:28:20.332Z" }, + { url = "https://files.pythonhosted.org/packages/1a/64/3f9142293c88b1b10e199649ed1330f070c2a68e305335a5819fa7f25fa7/charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00", size = 195016, upload-time = "2026-04-02T09:28:21.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d1/d8a6b7dd5c5636b76ce0d080bc57d8e56c7bbd6bc2ac941529a35e41d84a/charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776", size = 206115, upload-time = "2026-04-02T09:28:23.259Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8c/60ebe912379627d023eb96995b40bc50308729f210f43d66109ca0a7bbd2/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319", size = 204022, upload-time = "2026-04-02T09:28:24.779Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2a/41816ceda78a551cbfdfbeab6f3891152b0e3f758ce6580c2c18c829f774/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24", size = 195914, upload-time = "2026-04-02T09:28:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9b/7c7f4b7f11525fcbdfba752455314ac60646bae91cdd671d531c1f7a97c6/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42", size = 222159, upload-time = "2026-04-02T09:28:27.504Z" }, + { url = "https://files.pythonhosted.org/packages/9f/57/301682e7469bdbfa2ce219a804f0668b2266ab8520570d85d3b3ef483ea3/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4", size = 206154, upload-time = "2026-04-02T09:28:28.848Z" }, + { url = "https://files.pythonhosted.org/packages/20/ec/90339ff5cdc598b265748c1f231c7d7fbd9123a92cee10f757e0b1448de4/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67", size = 217423, upload-time = "2026-04-02T09:28:30.248Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e7/a7a6147f8e3375676309cf584b25c72a3bab784ea4085b0011fa07b23aeb/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274", size = 210604, upload-time = "2026-04-02T09:28:31.736Z" }, + { url = "https://files.pythonhosted.org/packages/1a/62/d9340c7a79c393e57807d7fb6c57e82060687891f81b74d3201958b919c1/charset_normalizer-3.4.7-cp39-cp39-win32.whl", hash = "sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366", size = 144631, upload-time = "2026-04-02T09:28:33.158Z" }, + { url = "https://files.pythonhosted.org/packages/21/e7/92901117e2ddc8facfe8235a3ecd4eb482185b2ad5d5b6606b37c1afea06/charset_normalizer-3.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444", size = 154710, upload-time = "2026-04-02T09:28:34.557Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4f/e1fb138201ad9a32499dd9a98aa4a5a5441fbf7f56b52b619a54b7ee8777/charset_normalizer-3.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c", size = 143716, upload-time = "2026-04-02T09:28:35.908Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, +] + +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + +[[package]] +name = "idna" +version = "3.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, +] + +[[package]] +name = "imagesize" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/59/4b0dd64676aa6fb4986a755790cb6fc558559cf0084effad516820208ec3/imagesize-1.5.0.tar.gz", hash = "sha256:8bfc5363a7f2133a89f0098451e0bcb1cd71aba4dc02bbcecb39d99d40e1b94f", size = 1281127, upload-time = "2026-03-03T01:59:54.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/b1/a0662b03103c66cf77101a187f396ea91167cd9b7d5d3a2e465ad2c7ee9b/imagesize-1.5.0-py2.py3-none-any.whl", hash = "sha256:32677681b3f434c2cb496f00e89c5a291247b35b1f527589909e008057da5899", size = 5763, upload-time = "2026-03-03T01:59:52.343Z" }, +] + +[[package]] +name = "imagesize" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "macholib" +version = "1.16.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "altgraph" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/2f/97589876ea967487978071c9042518d28b958d87b17dceb7cdc1d881f963/macholib-1.16.4.tar.gz", hash = "sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362", size = 59427, upload-time = "2025-11-22T08:28:38.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/d1/a9f36f8ecdf0fb7c9b1e78c8d7af12b8c8754e74851ac7b94a8305540fc7/macholib-1.16.4-py2.py3-none-any.whl", hash = "sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea", size = 38117, upload-time = "2025-11-22T08:28:36.939Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", + "python_full_version < '3.10'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, + { url = "https://files.pythonhosted.org/packages/56/23/0d8c13a44bde9154821586520840643467aee574d8ce79a17da539ee7fed/markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26", size = 11623, upload-time = "2025-09-27T18:37:29.296Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/07a2cb9a8045d5f3f0890a8c3bc0859d7a47bfd9a560b563899bec7b72ed/markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc", size = 12049, upload-time = "2025-09-27T18:37:30.234Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e4/6be85eb81503f8e11b61c0b6369b6e077dcf0a74adbd9ebf6b349937b4e9/markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c", size = 21923, upload-time = "2025-09-27T18:37:31.177Z" }, + { url = "https://files.pythonhosted.org/packages/6f/bc/4dc914ead3fe6ddaef035341fee0fc956949bbd27335b611829292b89ee2/markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42", size = 20543, upload-time = "2025-09-27T18:37:32.168Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/5fe81fbcfba4aef4093d5f856e5c774ec2057946052d18d168219b7bd9f9/markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b", size = 20585, upload-time = "2025-09-27T18:37:33.166Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f6/e0e5a3d3ae9c4020f696cd055f940ef86b64fe88de26f3a0308b9d3d048c/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758", size = 21387, upload-time = "2025-09-27T18:37:34.185Z" }, + { url = "https://files.pythonhosted.org/packages/c8/25/651753ef4dea08ea790f4fbb65146a9a44a014986996ca40102e237aa49a/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2", size = 20133, upload-time = "2025-09-27T18:37:35.138Z" }, + { url = "https://files.pythonhosted.org/packages/dc/0a/c3cf2b4fef5f0426e8a6d7fce3cb966a17817c568ce59d76b92a233fdbec/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d", size = 20588, upload-time = "2025-09-27T18:37:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/cd/1b/a7782984844bd519ad4ffdbebbba2671ec5d0ebbeac34736c15fb86399e8/markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7", size = 14566, upload-time = "2025-09-27T18:37:37.09Z" }, + { url = "https://files.pythonhosted.org/packages/18/1f/8d9c20e1c9440e215a44be5ab64359e207fcb4f675543f1cf9a2a7f648d0/markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e", size = 15053, upload-time = "2025-09-27T18:37:38.054Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d3/fe08482b5cd995033556d45041a4f4e76e7f0521112a9c9991d40d39825f/markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8", size = 13928, upload-time = "2025-09-27T18:37:39.037Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/03/a2ecab526543b152300717cf232bb4bb8605b6edb946c845016fa9c9c9fd/mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5", size = 43542, upload-time = "2024-09-09T20:27:49.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/f7/7782a043553ee469c1ff49cfa1cdace2d6bf99a1f333cf38676b3ddf30da/mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636", size = 55316, upload-time = "2024-09-09T20:27:48.397Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "markdown-it-py", version = "4.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "myst-parser" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "jinja2", marker = "python_full_version < '3.10'" }, + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "mdit-py-plugins", version = "0.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pyyaml", marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/64/e2f13dac02f599980798c01156393b781aec983b52a6e4057ee58f07c43a/myst_parser-3.0.1.tar.gz", hash = "sha256:88f0cb406cb363b077d176b51c476f62d60604d68a8dcdf4832e080441301a87", size = 92392, upload-time = "2024-04-28T20:22:42.116Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/de/21aa8394f16add8f7427f0a1326ccd2b3a2a8a3245c9252bc5ac034c6155/myst_parser-3.0.1-py3-none-any.whl", hash = "sha256:6457aaa33a5d474aca678b8ead9b3dc298e89c68e67012e73146ea6fd54babf1", size = 83163, upload-time = "2024-04-28T20:22:39.985Z" }, +] + +[[package]] +name = "myst-parser" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "jinja2", marker = "python_full_version == '3.10.*'" }, + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "mdit-py-plugins", version = "0.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "pyyaml", marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/a5/9626ba4f73555b3735ad86247a8077d4603aa8628537687c839ab08bfe44/myst_parser-4.0.1.tar.gz", hash = "sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4", size = 93985, upload-time = "2025-02-12T10:53:03.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl", hash = "sha256:9134e88959ec3b5780aedf8a99680ea242869d012e8821db3126d427edc9c95d", size = 84579, upload-time = "2025-02-12T10:53:02.078Z" }, +] + +[[package]] +name = "myst-parser" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "jinja2", marker = "python_full_version >= '3.11'" }, + { name = "markdown-it-py", version = "4.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "mdit-py-plugins", version = "0.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pyyaml", marker = "python_full_version >= '3.11'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/dc/603751677fff302f34396e206b610f556a59d7fe58b9a2145f54e96b48e8/myst_parser-5.1.0.tar.gz", hash = "sha256:ab69322dc6719dcc7f296479dbb70181b66df6ed315064f92dbc85c0e1bf2f02", size = 101182, upload-time = "2026-05-13T09:38:19.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/dc/f3dfb7488b770f3f67e6545085bf2abea5172e88f57b8ad25ef860ca704c/myst_parser-5.1.0-py3-none-any.whl", hash = "sha256:9c91c52b3cdb4d94a6506e4fab4e2f296c7623a0da0dcbe6de1565c3dad67a8a", size = 85817, upload-time = "2026-05-13T09:38:17.904Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pefile" +version = "2024.8.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/4f/2750f7f6f025a1507cd3b7218691671eecfd0bbebebe8b39aa0fe1d360b8/pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632", size = 76008, upload-time = "2024-08-26T20:58:38.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766, upload-time = "2024-08-26T21:01:02.632Z" }, +] + +[[package]] +name = "pyelftools" +version = "0.32" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/ab/33968940b2deb3d92f5b146bc6d4009a5f95d1d06c148ea2f9ee965071af/pyelftools-0.32.tar.gz", hash = "sha256:6de90ee7b8263e740c8715a925382d4099b354f29ac48ea40d840cf7aa14ace5", size = 15047199, upload-time = "2025-02-19T14:20:05.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/43/700932c4f0638c3421177144a2e86448c0d75dbaee2c7936bda3f9fd0878/pyelftools-0.32-py3-none-any.whl", hash = "sha256:013df952a006db5e138b1edf6d8a68ecc50630adbd0d83a2d41e7f846163d738", size = 188525, upload-time = "2025-02-19T14:19:59.919Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, + { url = "https://files.pythonhosted.org/packages/9f/62/67fc8e68a75f738c9200422bf65693fb79a4cd0dc5b23310e5202e978090/pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", size = 184450, upload-time = "2025-09-25T21:33:00.618Z" }, + { url = "https://files.pythonhosted.org/packages/ae/92/861f152ce87c452b11b9d0977952259aa7df792d71c1053365cc7b09cc08/pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", size = 174319, upload-time = "2025-09-25T21:33:02.086Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cd/f0cfc8c74f8a030017a2b9c771b7f47e5dd702c3e28e5b2071374bda2948/pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", size = 737631, upload-time = "2025-09-25T21:33:03.25Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b2/18f2bd28cd2055a79a46c9b0895c0b3d987ce40ee471cecf58a1a0199805/pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", size = 836795, upload-time = "2025-09-25T21:33:05.014Z" }, + { url = "https://files.pythonhosted.org/packages/73/b9/793686b2d54b531203c160ef12bec60228a0109c79bae6c1277961026770/pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", size = 750767, upload-time = "2025-09-25T21:33:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/a9/86/a137b39a611def2ed78b0e66ce2fe13ee701a07c07aebe55c340ed2a050e/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", size = 727982, upload-time = "2025-09-25T21:33:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/dd/62/71c27c94f457cf4418ef8ccc71735324c549f7e3ea9d34aba50874563561/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", size = 755677, upload-time = "2025-09-25T21:33:09.876Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/6f5e0d58bd924fb0d06c3a6bad00effbdae2de5adb5cda5648006ffbd8d3/pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", size = 142592, upload-time = "2025-09-25T21:33:10.983Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" }, +] + +[[package]] +name = "readthedocs-sphinx-ext" +version = "2.2.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "packaging" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "requests", version = "2.34.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e8/ce/38130d8dec600bf5413eb89a3413dd38f204c7c728c4947e12ff8cb793b7/readthedocs-sphinx-ext-2.2.5.tar.gz", hash = "sha256:ee5fd5b99db9f0c180b2396cbce528aa36671951b9526bb0272dbfce5517bd27", size = 12303, upload-time = "2023-12-19T10:00:49.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/71/c89e7709a0d4f93af1848e9855112299a820b470d84f917b4dd5998bdd07/readthedocs_sphinx_ext-2.2.5-py2.py3-none-any.whl", hash = "sha256:f8c56184ea011c972dd45a90122568587cc85b0127bc9cf064d17c68bc809daa", size = 11332, upload-time = "2023-12-19T10:00:43.972Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version < '3.10'" }, + { name = "charset-normalizer", marker = "python_full_version < '3.10'" }, + { name = "idna", marker = "python_full_version < '3.10'" }, + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version >= '3.10'" }, + { name = "charset-normalizer", marker = "python_full_version >= '3.10'" }, + { name = "idna", marker = "python_full_version >= '3.10'" }, + { name = "urllib3", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "roman-numerals" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, +] + +[[package]] +name = "rules-python-docs" +version = "0.0.0" +source = { virtual = "." } +dependencies = [ + { name = "absl-py", version = "2.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "absl-py", version = "2.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "macholib" }, + { name = "markupsafe" }, + { name = "myst-parser", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "myst-parser", version = "4.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "myst-parser", version = "5.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pefile" }, + { name = "pyelftools" }, + { name = "readthedocs-sphinx-ext" }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "sphinx-autodoc2" }, + { name = "sphinx-reredirects", version = "0.1.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx-reredirects", version = "1.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx-rtd-theme" }, + { name = "typing-extensions" }, +] + +[package.metadata] +requires-dist = [ + { name = "absl-py" }, + { name = "macholib" }, + { name = "markupsafe" }, + { name = "myst-parser" }, + { name = "pefile" }, + { name = "pyelftools" }, + { name = "readthedocs-sphinx-ext" }, + { name = "sphinx" }, + { name = "sphinx-autodoc2" }, + { name = "sphinx-reredirects" }, + { name = "sphinx-rtd-theme", specifier = ">=2.0" }, + { name = "typing-extensions" }, +] + +[[package]] +name = "snowballstemmer" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, +] + +[[package]] +name = "sphinx" +version = "7.4.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "alabaster", version = "0.7.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "babel", marker = "python_full_version < '3.10'" }, + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "imagesize", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jinja2", marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pygments", marker = "python_full_version < '3.10'" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "snowballstemmer", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/be/50e50cb4f2eff47df05673d361095cafd95521d2a22521b920c67a372dcb/sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe", size = 8067911, upload-time = "2024-07-20T14:46:56.059Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/ef/153f6803c5d5f8917dbb7f7fcf6d34a871ede3296fa89c2c703f5f8a6c8e/sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239", size = 3401624, upload-time = "2024-07-20T14:46:52.142Z" }, +] + +[[package]] +name = "sphinx" +version = "8.1.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "babel", marker = "python_full_version == '3.10.*'" }, + { name = "colorama", marker = "python_full_version == '3.10.*' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "imagesize", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "jinja2", marker = "python_full_version == '3.10.*'" }, + { name = "packaging", marker = "python_full_version == '3.10.*'" }, + { name = "pygments", marker = "python_full_version == '3.10.*'" }, + { name = "requests", version = "2.34.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "snowballstemmer", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.10.*'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125, upload-time = "2024-10-13T20:27:10.448Z" }, +] + +[[package]] +name = "sphinx" +version = "9.0.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "babel", marker = "python_full_version == '3.11.*'" }, + { name = "colorama", marker = "python_full_version == '3.11.*' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "imagesize", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "jinja2", marker = "python_full_version == '3.11.*'" }, + { name = "packaging", marker = "python_full_version == '3.11.*'" }, + { name = "pygments", marker = "python_full_version == '3.11.*'" }, + { name = "requests", version = "2.34.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "roman-numerals", marker = "python_full_version == '3.11.*'" }, + { name = "snowballstemmer", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.11.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/3f/4bbd76424c393caead2e1eb89777f575dee5c8653e2d4b6afd7a564f5974/sphinx-9.0.4-py3-none-any.whl", hash = "sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb", size = 3917713, upload-time = "2025-12-04T07:45:24.944Z" }, +] + +[[package]] +name = "sphinx" +version = "9.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", +] +dependencies = [ + { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "babel", marker = "python_full_version >= '3.12'" }, + { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "imagesize", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "jinja2", marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.12'" }, + { name = "pygments", marker = "python_full_version >= '3.12'" }, + { name = "requests", version = "2.34.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, + { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, +] + +[[package]] +name = "sphinx-autodoc2" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astroid" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/5f/5350046d1aa1a56b063ae08b9ad871025335c9d55fe2372896ea48711da9/sphinx_autodoc2-0.5.0.tar.gz", hash = "sha256:7d76044aa81d6af74447080182b6868c7eb066874edc835e8ddf810735b6565a", size = 115077, upload-time = "2023-11-27T07:27:51.407Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/e6/48d47961bbdae755ba9c17dfc65d89356312c67668dcb36c87cfadfa1964/sphinx_autodoc2-0.5.0-py3-none-any.whl", hash = "sha256:e867013b1512f9d6d7e6f6799f8b537d6884462acd118ef361f3f619a60b5c9e", size = 43385, upload-time = "2023-11-27T07:27:49.929Z" }, +] + +[[package]] +name = "sphinx-reredirects" +version = "0.1.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", + "python_full_version < '3.10'", +] +dependencies = [ + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/6b/bcca2785de4071f604a722444d4d7ba8a9d40de3c14ad52fce93e6d92694/sphinx_reredirects-0.1.6.tar.gz", hash = "sha256:c491cba545f67be9697508727818d8626626366245ae64456fe29f37e9bbea64", size = 7080, upload-time = "2025-03-22T10:52:30.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/6f/0b3625be30a1a50f9e4c2cb2ec147b08f15ed0e9f8444efcf274b751300b/sphinx_reredirects-0.1.6-py3-none-any.whl", hash = "sha256:efd50c766fbc5bf40cd5148e10c00f2c00d143027de5c5e48beece93cc40eeea", size = 5675, upload-time = "2025-03-22T10:52:29.113Z" }, +] + +[[package]] +name = "sphinx-reredirects" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/8d/0e39fe2740d7d71417edf9a6424aa80ca2c27c17fc21282cdc39f90d5a40/sphinx_reredirects-1.1.0.tar.gz", hash = "sha256:fb9b195335ab14b43f8273287d0c7eeb637ba6c56c66581c11b47202f6718b29", size = 614624, upload-time = "2025-12-22T08:28:02.792Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/81/b5dd07067f3daac6d23687ec737b2d593740671ebcd145830c8f92d381c5/sphinx_reredirects-1.1.0-py3-none-any.whl", hash = "sha256:4b5692273c72cd2d4d917f4c6f87d5919e4d6114a752d4be033f7f5f6310efd9", size = 6351, upload-time = "2025-12-22T08:27:59.724Z" }, +] + +[[package]] +name = "sphinx-rtd-theme" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-jquery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/68/a1bfbf38c0f7bccc9b10bbf76b94606f64acb1552ae394f0b8285bfaea25/sphinx_rtd_theme-3.1.0.tar.gz", hash = "sha256:b44276f2c276e909239a4f6c955aa667aaafeb78597923b1c60babc76db78e4c", size = 7620915, upload-time = "2026-01-12T16:03:31.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/c7/b5c8015d823bfda1a346adb2c634a2101d50bb75d421eb6dcb31acd25ebc/sphinx_rtd_theme-3.1.0-py2.py3-none-any.whl", hash = "sha256:1785824ae8e6632060490f67cf3a72d404a85d2d9fc26bce3619944de5682b89", size = 7655617, upload-time = "2026-01-12T16:03:28.101Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jquery" +version = "4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/f3/aa67467e051df70a6330fe7770894b3e4f09436dea6881ae0b4f3d87cad8/sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", size = 122331, upload-time = "2023-03-14T15:01:01.944Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae", size = 121104, upload-time = "2023-03-14T15:01:00.356Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", size = 25965, upload-time = "2026-04-13T23:21:46.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", size = 10378, upload-time = "2026-04-13T23:21:45.386Z" }, +] diff --git a/downloader_config.cfg b/downloader_config.cfg new file mode 100644 index 0000000000..3fa6264eda --- /dev/null +++ b/downloader_config.cfg @@ -0,0 +1,21 @@ +# Try GitHub first (primary) +rewrite ^github\.com/bazel-contrib/bazel_features/(.*) github.com/bazel-contrib/bazel_features/$1 +rewrite ^github\.com/bazel-contrib/rules_go/(.*) github.com/bazel-contrib/rules_go/$1 +rewrite ^github\.com/bazelbuild/bazel-skylib/(.*) github.com/bazelbuild/bazel-skylib/$1 +rewrite ^github\.com/bazelbuild/platforms/(.*) github.com/bazelbuild/platforms/$1 +rewrite ^github\.com/bazelbuild/rules_kotlin/(.*) github.com/bazelbuild/rules_kotlin/$1 +rewrite ^github\.com/bazelbuild/rules_shell/(.*) github.com/bazelbuild/rules_shell/$1 +rewrite ^github\.com/bazelbuild/rules_java/(.*) github.com/bazelbuild/rules_java/$1 +rewrite ^github\.com/bazelbuild/stardoc/(.*) github.com/bazelbuild/stardoc/$1 + + +# Fall back to mirror (secondary) +# Tracking upstream BCR mirror addition: https://github.com/bazelbuild/platforms/issues/139 +rewrite ^github\.com/bazel-contrib/bazel_features/(.*) mirror.bazel.build/github.com/bazel-contrib/bazel_features/$1 +rewrite ^github\.com/bazel-contrib/rules_go/(.*) mirror.bazel.build/github.com/bazel-contrib/rules_go/$1 +rewrite ^github\.com/bazelbuild/bazel-skylib/(.*) mirror.bazel.build/github.com/bazelbuild/bazel-skylib/$1 +rewrite ^github\.com/bazelbuild/platforms/(.*) mirror.bazel.build/github.com/bazelbuild/platforms/$1 +rewrite ^github\.com/bazelbuild/rules_kotlin/(.*) mirror.bazel.build/github.com/bazelbuild/rules_kotlin/$1 +rewrite ^github\.com/bazelbuild/rules_shell/(.*) mirror.bazel.build/github.com/bazelbuild/rules_shell/$1 +rewrite ^github\.com/bazelbuild/rules_java/(.*) mirror.bazel.build/github.com/bazelbuild/rules_java/$1 +rewrite ^github\.com/bazelbuild/stardoc/(.*) mirror.bazel.build/github.com/bazelbuild/stardoc/$1 diff --git a/examples/BUILD.bazel b/examples/BUILD.bazel index d2fddc44c5..a08e5e64ae 100644 --- a/examples/BUILD.bazel +++ b/examples/BUILD.bazel @@ -26,5 +26,30 @@ lock( "--universal", "--python-version=3.9", ], - python_version = "3.9.19", + python_version = "3.9", +) + +lock( + name = "bzlmod_requirements_3_11", + srcs = ["bzlmod/requirements.in"], + out = "bzlmod/requirements_lock_3_11.txt", + args = [ + "--emit-index-url", + "--universal", + "--python-version=3.11", + ], + python_version = "3.11", +) + +lock( + name = "bzlmod_requirements_3_11_windows", + srcs = ["bzlmod/requirements.in"], + out = "bzlmod/requirements_windows_3_11.txt", + args = [ + "--emit-index-url", + "--python-platform", + "windows", + "--python-version=3.11", + ], + python_version = "3.11", ) diff --git a/examples/build_file_generation/.bazelrc b/examples/build_file_generation/.bazelrc index 306954d7be..f1ae44fac8 100644 --- a/examples/build_file_generation/.bazelrc +++ b/examples/build_file_generation/.bazelrc @@ -3,7 +3,7 @@ test --test_output=errors --enable_runfiles # Windows requires these for multi-python support: build --enable_runfiles -# The bzlmod version of this example is in examples/bzlmod_build_file_generation +# The bzlmod version of this example is in gazelle/examples/bzlmod_build_file_generation # Once WORKSPACE support is dropped, this example can be entirely deleted. common --noenable_bzlmod common --enable_workspace diff --git a/examples/build_file_generation/WORKSPACE b/examples/build_file_generation/WORKSPACE index 27f6ec071c..27d0d13b7c 100644 --- a/examples/build_file_generation/WORKSPACE +++ b/examples/build_file_generation/WORKSPACE @@ -7,54 +7,7 @@ workspace(name = "build_file_generation_example") # file. When the symbol is loaded you can use the rule. load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") -###################################################################### -# We need rules_go and bazel_gazelle, to build the gazelle plugin from source. -# Setup instructions for this section are at -# https://github.com/bazelbuild/bazel-gazelle#running-gazelle-with-bazel -# You may need to update the version of the rule, which is listed in the above -# documentation. -###################################################################### - -# Define an http_archive rule that will download the below ruleset, -# test the sha, and extract the ruleset to you local bazel cache. - -http_archive( - name = "io_bazel_rules_go", - sha256 = "9d72f7b8904128afb98d46bbef82ad7223ec9ff3718d419afb355fddd9f9484a", - urls = [ - "https://mirror.bazel.build/github.com/bazel-contrib/rules_go/releases/download/v0.55.1/rules_go-v0.55.1.zip", - "https://github.com/bazel-contrib/rules_go/releases/download/v0.55.1/rules_go-v0.55.1.zip", - ], -) - -# Download the bazel_gazelle ruleset. -http_archive( - name = "bazel_gazelle", - sha256 = "75df288c4b31c81eb50f51e2e14f4763cb7548daae126817247064637fd9ea62", - urls = [ - "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.36.0/bazel-gazelle-v0.36.0.tar.gz", - "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.36.0/bazel-gazelle-v0.36.0.tar.gz", - ], -) - -# Load rules_go ruleset and expose the toolchain and dep rules. -load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies") -load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies") - -# go_rules_dependencies is a function that registers external dependencies -# needed by the Go rules. -# See: https://github.com/bazelbuild/rules_go/blob/master/go/dependencies.rst#go_rules_dependencies -go_rules_dependencies() - -# go_rules_dependencies is a function that registers external dependencies -# needed by the Go rules. -# See: https://github.com/bazelbuild/rules_go/blob/master/go/dependencies.rst#go_rules_dependencies -go_register_toolchains(version = "1.21.13") - -# The following call configured the gazelle dependencies, Go environment and Go SDK. -gazelle_dependencies() - -# Remaining setup is for rules_python. +# Setup rules_python. # DON'T COPY_PASTE THIS. # Our example uses `local_repository` to point to the HEAD version of rules_python. @@ -124,6 +77,53 @@ load("@pip//:requirements.bzl", "install_deps") # Initialize repositories for all packages in requirements_lock.txt. install_deps() +###################################################################### +# We need rules_go and bazel_gazelle, to build the gazelle plugin from source. +# Setup instructions for this section are at +# https://github.com/bazelbuild/bazel-gazelle#running-gazelle-with-bazel +# You may need to update the version of the rule, which is listed in the above +# documentation. +###################################################################### + +# Define an http_archive rule that will download the below ruleset, +# test the sha, and extract the ruleset to you local bazel cache. + +http_archive( + name = "io_bazel_rules_go", + sha256 = "9d72f7b8904128afb98d46bbef82ad7223ec9ff3718d419afb355fddd9f9484a", + urls = [ + "https://mirror.bazel.build/github.com/bazel-contrib/rules_go/releases/download/v0.55.1/rules_go-v0.55.1.zip", + "https://github.com/bazel-contrib/rules_go/releases/download/v0.55.1/rules_go-v0.55.1.zip", + ], +) + +# Download the bazel_gazelle ruleset. +http_archive( + name = "bazel_gazelle", + sha256 = "75df288c4b31c81eb50f51e2e14f4763cb7548daae126817247064637fd9ea62", + urls = [ + "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.36.0/bazel-gazelle-v0.36.0.tar.gz", + "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.36.0/bazel-gazelle-v0.36.0.tar.gz", + ], +) + +# Load rules_go ruleset and expose the toolchain and dep rules. +load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies") +load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies") + +# go_rules_dependencies is a function that registers external dependencies +# needed by the Go rules. +# See: https://github.com/bazelbuild/rules_go/blob/master/go/dependencies.rst#go_rules_dependencies +go_rules_dependencies() + +# go_rules_dependencies is a function that registers external dependencies +# needed by the Go rules. +# See: https://github.com/bazelbuild/rules_go/blob/master/go/dependencies.rst#go_rules_dependencies +go_register_toolchains(version = "1.21.13") + +# The following call configured the gazelle dependencies, Go environment and Go SDK. +gazelle_dependencies() + # The rules_python gazelle extension has some third-party go dependencies # which we need to fetch in order to compile it. load("@rules_python_gazelle_plugin//:deps.bzl", _py_gazelle_deps = "gazelle_deps") diff --git a/examples/bzlmod/.bazelignore b/examples/bzlmod/.bazelignore index 536ded93a6..a59e740c96 100644 --- a/examples/bzlmod/.bazelignore +++ b/examples/bzlmod/.bazelignore @@ -1,3 +1,2 @@ other_module -py_proto_library/foo_external vendor diff --git a/examples/bzlmod/.bazelrc b/examples/bzlmod/.bazelrc index ca83047ccc..28a44a7523 100644 --- a/examples/bzlmod/.bazelrc +++ b/examples/bzlmod/.bazelrc @@ -1,5 +1,21 @@ +# Starting with Bazel 8, Windows requires this flag in order +# for symlinks to work properly (namely, so that sh_test with +# py_binary as a data dependency gets symlinks that are executable) +startup --windows_enable_symlinks + common --enable_bzlmod common --lockfile_mode=update +# This adds an implicit --config= +# See docs for osname values +# https://bazel.build/reference/command-line-reference#common_options-flag--enable_platform_specific_config +common --enable_platform_specific_config + +common:windows --cxxopt=/std:c++17 +common:windows --host_cxxopt=/std:c++17 +common:linux --cxxopt=-std=c++17 +common:linux --host_cxxopt=-std=c++17 +common:macos --cxxopt=-std=c++17 +common:macos --host_cxxopt=-std=c++17 coverage --java_runtime_version=remotejdk_11 diff --git a/examples/bzlmod/.bazelversion b/examples/bzlmod/.bazelversion index 35907cd9ca..512e4c889e 100644 --- a/examples/bzlmod/.bazelversion +++ b/examples/bzlmod/.bazelversion @@ -1 +1 @@ -7.x +9.x diff --git a/examples/bzlmod/MODULE.bazel b/examples/bzlmod/MODULE.bazel index 95e1090f53..106f25134e 100644 --- a/examples/bzlmod/MODULE.bazel +++ b/examples/bzlmod/MODULE.bazel @@ -12,18 +12,15 @@ local_path_override( path = "../..", ) -# (py_proto_library specific) Add the protobuf library for well-known types (e.g. `Any`, `Timestamp`, etc) -bazel_dep(name = "protobuf", version = "27.0", repo_name = "com_google_protobuf") - # Only needed to make rules_python's CI happy. rules_java 8.3.0+ is needed so # that --java_runtime_version=remotejdk_11 works with Bazel 8. -bazel_dep(name = "rules_java", version = "8.3.1") +bazel_dep(name = "rules_java", version = "8.16.1") # Only needed to make rules_python's CI happy. A test verifies that # MODULE.bazel.lock is cross-platform friendly, and there are transitive # dependencies on rules_rust, so we need rules_rust 0.54.1+ where such issues # were fixed. -bazel_dep(name = "rules_rust", version = "0.54.1") +bazel_dep(name = "rules_rust", version = "0.67.0") # We next initialize the python toolchain using the extension. # You can set different Python versions in this block. @@ -55,7 +52,6 @@ python.override( # require versions not listed here. # available_python_versions = [ # "3.10.9", - # "3.9.18", # "3.9.19", # # The following is used by the `other_module` and we need to include it here # # as well. @@ -65,7 +61,7 @@ python.override( # instead of rules_python's defaulting to the latest available version, # controls what full version is used when `3.x` is requested. minor_mapping = { - "3.9": "3.9.19", + "3.9": "3.9.25", }, ) @@ -169,10 +165,13 @@ pip.default( env = { "platform_version": "0", }, + # Windows ARM64 support has been added only on 3.11 and above, hence, constrain + # the availability of the platform for those python versions. + marker = "python_version >= '3.11'", os_name = "windows", platform = "windows_aarch64", whl_abi_tags = [], # default to all ABIs - whl_platform_tags = ["win_amd64"], + whl_platform_tags = ["win_arm64"], ) # To fetch pip dependencies, use pip.parse. We can pass in various options, @@ -184,18 +183,6 @@ pip.default( pip.parse( # We can use `envsubst in the above envsubst = ["PIP_INDEX_URL"], - # Use the bazel downloader to query the simple API for downloading the sources - # Note, that we can use envsubst for this value. - experimental_index_url = "${PIP_INDEX_URL:-https://pypi.org/simple}", - # One can also select a particular index for a particular package. - # This ensures that the setup is resistant against confusion attacks. - # experimental_index_url_overrides = { - # "my_package": "https://different-index-url.com", - # }, - # Or you can specify extra indexes like with `pip`: - # experimental_extra_index_urls = [ - # "https://different-index-url.com", - # ], experimental_requirement_cycles = { "sphinx": [ "sphinx", @@ -206,17 +193,19 @@ pip.parse( "sphinxcontrib-serializinghtml", ], }, - # You can use one of the values below to specify the target platform - # to generate the dependency graph for. - experimental_target_platforms = [ - # Specifying the target platforms explicitly - "cp39_linux_x86_64", - "cp39_linux_*", - "cp39_*", - ], extra_hub_aliases = { "wheel": ["generated_file"], }, + extra_pip_args = [ + # Use the bazel downloader to query the simple API for downloading the sources + # Note, that we can use envsubst for this value. + # One can also select a particular index for a particular package. + # This ensures that the setup is resistant against confusion attacks. + # experimental_index_url_overrides = { + # "my_package": "https://different-index-url.com", + # }, + "--index-url=${PIP_INDEX_URL:-https://pypi.org/simple}", + ], hub_name = "pip", python_version = "3.9", requirements_lock = "requirements_lock_3_9.txt", @@ -239,30 +228,34 @@ pip.parse( "sphinxcontrib-serializinghtml", ], }, - # You can use one of the values below to specify the target platform - # to generate the dependency graph for. - experimental_target_platforms = [ - # Using host python version - "linux_*", - "osx_*", - "windows_*", - # Or specifying an exact platform - "linux_x86_64", - # Or the following to get the `host` platform only - "host", - ], hub_name = "pip", python_version = "3.10", # The requirements files for each platform that we want to support. requirements_by_platform = { # Default requirements file for needs to explicitly provide the platforms "//:requirements_lock_3_10.txt": "linux_*,osx_*", + "//:requirements_windows_3_10.txt": "windows_x86_64", + }, + # These modifications were created above and we + # are providing pip.parse with the label of the mod + # and the name of the wheel. + whl_modifications = { + "@whl_mods_hub//:requests.json": "requests", + "@whl_mods_hub//:wheel.json": "wheel", + }, +) +pip.parse( + hub_name = "pip", + python_version = "3.11", + requirements_by_platform = { + # Default requirements file for needs to explicitly provide the platforms + "//:requirements_lock_3_11.txt": "linux_*,osx_*", # This API allows one to specify additional platforms that the users # configure the toolchains for themselves. In this example we add # `windows_aarch64` to illustrate that `rules_python` won't fail to # process the value, but it does not mean that this example will work # on Windows ARM. - "//:requirements_windows_3_10.txt": "windows_x86_64,windows_aarch64", + "//:requirements_windows_3_11.txt": "windows_x86_64,windows_aarch64", }, # These modifications were created above and we # are providing pip.parse with the label of the mod @@ -293,11 +286,5 @@ local_path_override( path = "other_module", ) -bazel_dep(name = "foo_external", version = "") -local_path_override( - module_name = "foo_external", - path = "py_proto_library/foo_external", -) - # example test dependencies bazel_dep(name = "rules_shell", version = "0.3.0", dev_dependency = True) diff --git a/examples/bzlmod/entry_points/tests/pylint_deps_test.py b/examples/bzlmod/entry_points/tests/pylint_deps_test.py index f6743ce9b5..54826a137c 100644 --- a/examples/bzlmod/entry_points/tests/pylint_deps_test.py +++ b/examples/bzlmod/entry_points/tests/pylint_deps_test.py @@ -15,7 +15,6 @@ import os import pathlib import subprocess -import tempfile import unittest from python.runfiles import runfiles @@ -29,9 +28,9 @@ def __init__(self, *args, **kwargs): def test_pylint_entry_point(self): rlocation_path = os.environ.get("ENTRY_POINT") - assert ( - rlocation_path is not None - ), "expected 'ENTRY_POINT' env variable to be set to rlocation of the tool" + assert rlocation_path is not None, ( + "expected 'ENTRY_POINT' env variable to be set to rlocation of the tool" + ) entry_point = pathlib.Path(runfiles.Create().Rlocation(rlocation_path)) self.assertTrue(entry_point.exists(), f"'{entry_point}' does not exist") @@ -51,20 +50,20 @@ def test_pylint_entry_point(self): "", proc.stderr.decode("utf-8").strip(), ) - self.assertRegex(proc.stdout.decode("utf-8").strip(), "^pylint 2\.15\.9") + self.assertRegex(proc.stdout.decode("utf-8").strip(), r"^pylint 2\.15\.9") def test_pylint_report_has_expected_warnings(self): rlocation_path = os.environ.get("PYLINT_REPORT") - assert ( - rlocation_path is not None - ), "expected 'PYLINT_REPORT' env variable to be set to rlocation of the report" + assert rlocation_path is not None, ( + "expected 'PYLINT_REPORT' env variable to be set to rlocation of the report" + ) pylint_report = pathlib.Path(runfiles.Create().Rlocation(rlocation_path)) self.assertTrue(pylint_report.exists(), f"'{pylint_report}' does not exist") self.assertRegex( pylint_report.read_text().strip(), - "W8201: Logging should be used instead of the print\(\) function\. \(print-function\)", + r"W8201: Logging should be used instead of the print\(\) function\. \(print-function\)", ) diff --git a/examples/bzlmod/entry_points/tests/pylint_test.py b/examples/bzlmod/entry_points/tests/pylint_test.py index c2532938d8..5a13e61920 100644 --- a/examples/bzlmod/entry_points/tests/pylint_test.py +++ b/examples/bzlmod/entry_points/tests/pylint_test.py @@ -28,9 +28,9 @@ def __init__(self, *args, **kwargs): def test_pylint_entry_point(self): rlocation_path = os.environ.get("ENTRY_POINT") - assert ( - rlocation_path is not None - ), "expected 'ENTRY_POINT' env variable to be set to rlocation of the tool" + assert rlocation_path is not None, ( + "expected 'ENTRY_POINT' env variable to be set to rlocation of the tool" + ) entry_point = pathlib.Path(runfiles.Create().Rlocation(rlocation_path)) self.assertTrue(entry_point.exists(), f"'{entry_point}' does not exist") @@ -50,7 +50,7 @@ def test_pylint_entry_point(self): "", proc.stderr.decode("utf-8").strip(), ) - self.assertRegex(proc.stdout.decode("utf-8").strip(), "^pylint 2\.15\.9") + self.assertRegex(proc.stdout.decode("utf-8").strip(), r"^pylint 2\.15\.9") if __name__ == "__main__": diff --git a/examples/bzlmod/entry_points/tests/yamllint_test.py b/examples/bzlmod/entry_points/tests/yamllint_test.py index 0a0235793b..29b5ebf9b8 100644 --- a/examples/bzlmod/entry_points/tests/yamllint_test.py +++ b/examples/bzlmod/entry_points/tests/yamllint_test.py @@ -28,9 +28,9 @@ def __init__(self, *args, **kwargs): def test_yamllint_entry_point(self): rlocation_path = os.environ.get("ENTRY_POINT") - assert ( - rlocation_path is not None - ), "expected 'ENTRY_POINT' env variable to be set to rlocation of the tool" + assert rlocation_path is not None, ( + "expected 'ENTRY_POINT' env variable to be set to rlocation of the tool" + ) entry_point = pathlib.Path(runfiles.Create().Rlocation(rlocation_path)) self.assertTrue(entry_point.exists(), f"'{entry_point}' does not exist") diff --git a/examples/bzlmod/other_module/MODULE.bazel b/examples/bzlmod/other_module/MODULE.bazel index f9d6706120..a128c39ca0 100644 --- a/examples/bzlmod/other_module/MODULE.bazel +++ b/examples/bzlmod/other_module/MODULE.bazel @@ -5,24 +5,10 @@ module( # This module is using the same version of rules_python # that the parent module uses. bazel_dep(name = "rules_python", version = "") - -# The story behind this commented out override: -# This override is necessary to generate/update the requirements file -# for this module. This is because running it via the outer -# module doesn't work -- the `requirements.update` target can't find -# the correct file to update. -# Running in the submodule itself works, but submodules using overrides -# is considered an error until Bazel 6.3, which prevents the outer module -# from depending on this module. -# So until 6.3 and higher is the minimum, we leave this commented out. -# local_path_override( -# module_name = "rules_python", -# path = "../../..", -# ) - -PYTHON_NAME_39 = "python_3_9" - -PYTHON_NAME_311 = "python_3_11" +local_path_override( + module_name = "rules_python", + path = "../../..", +) python = use_extension("@rules_python//python/extensions:python.bzl", "python") python.defaults( @@ -31,21 +17,13 @@ python.defaults( ) python.toolchain( configure_coverage_tool = True, - python_version = "3.9", + python_version = "3.12", ) python.toolchain( configure_coverage_tool = True, python_version = "3.11", ) -# created by the above python.toolchain calls. -use_repo( - python, - "python_versions", - PYTHON_NAME_39, - PYTHON_NAME_311, -) - pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") pip.parse( hub_name = "other_module_pip", diff --git a/examples/bzlmod/other_module/other_module/pkg/BUILD.bazel b/examples/bzlmod/other_module/other_module/pkg/BUILD.bazel index 53344c708a..318822d888 100644 --- a/examples/bzlmod/other_module/other_module/pkg/BUILD.bazel +++ b/examples/bzlmod/other_module/other_module/pkg/BUILD.bazel @@ -1,5 +1,6 @@ load("@rules_python//python:py_binary.bzl", "py_binary") load("@rules_python//python:py_library.bzl", "py_library") +load("@rules_python//python/zipapp:py_zipapp_binary.bzl", "py_zipapp_binary") py_library( name = "lib", @@ -26,4 +27,13 @@ py_binary( ], ) +# This is used for regression testing runfiles paths in submodules. +# https://github.com/bazel-contrib/rules_python/issues/3563. +py_zipapp_binary( + name = "bin_zipapp", + testonly = True, + binary = ":bin", + visibility = ["//visibility:public"], +) + exports_files(["data/data.txt"]) diff --git a/examples/bzlmod/other_module/other_module/rule_builder/BUILD.bazel b/examples/bzlmod/other_module/other_module/rule_builder/BUILD.bazel new file mode 100644 index 0000000000..40f66b3322 --- /dev/null +++ b/examples/bzlmod/other_module/other_module/rule_builder/BUILD.bazel @@ -0,0 +1,13 @@ +load(":rule.bzl", "custom_py_binary") + +# This target's mere existence is the regression test: analyzing it exercises +# the implicit attr defaults (build_data_writer, debugger_if_target_config, +# uncachable_version_file) that py_binary_rule_builder() bundles from +# rules_python's private package, from a rule() call made in this external +# module. +custom_py_binary( + name = "app", + srcs = ["app.py"], + main = "app.py", + visibility = ["//visibility:public"], +) diff --git a/examples/bzlmod/other_module/other_module/rule_builder/app.py b/examples/bzlmod/other_module/other_module/rule_builder/app.py new file mode 100644 index 0000000000..db51494ed6 --- /dev/null +++ b/examples/bzlmod/other_module/other_module/rule_builder/app.py @@ -0,0 +1 @@ +print("hello from a rule built via py_binary_rule_builder()") diff --git a/examples/bzlmod/other_module/other_module/rule_builder/rule.bzl b/examples/bzlmod/other_module/other_module/rule_builder/rule.bzl new file mode 100644 index 0000000000..7b1505987a --- /dev/null +++ b/examples/bzlmod/other_module/other_module/rule_builder/rule.bzl @@ -0,0 +1,17 @@ +"""A minimal custom py_binary built via the executables rule builder API. + +Regression coverage for https://github.com/bazel-contrib/rules_python/pull/3919: +py_binary_rule_builder()'s implicit attr defaults (build_data_writer, +debugger_if_target_config, uncachable_version_file) previously had no +visibility outside of rules_python, so any external module (like this one) +constructing a rule via the builder failed at analysis time with a +visibility error. +""" + +load("@rules_python//python/api:executables.bzl", "executables") + +def _make_rule(): + builder = executables.py_binary_rule_builder() + return builder.build() + +custom_py_binary = _make_rule() diff --git a/examples/bzlmod/py_proto_library/BUILD.bazel b/examples/bzlmod/py_proto_library/BUILD.bazel deleted file mode 100644 index daea410365..0000000000 --- a/examples/bzlmod/py_proto_library/BUILD.bazel +++ /dev/null @@ -1,35 +0,0 @@ -load("@bazel_skylib//rules:native_binary.bzl", "native_test") -load("@rules_python//python:py_test.bzl", "py_test") - -py_test( - name = "pricetag_test", - srcs = ["test.py"], - main = "test.py", - deps = [ - "//py_proto_library/example.com/proto:pricetag_py_pb2", - ], -) - -py_test( - name = "message_test", - srcs = ["message_test.py"], - deps = [ - "//py_proto_library/example.com/another_proto:message_py_pb2", - ], -) - -# Regression test for https://github.com/bazel-contrib/rules_python/issues/2515 -# -# This test fails before protobuf 30.0 release -# when ran with --legacy_external_runfiles=False (default in Bazel 8.0.0). -native_test( - name = "external_import_test", - src = "@foo_external//:py_binary_with_proto", - tags = ["manual"], # TODO: reenable when com_google_protobuf is upgraded - # Incompatible with Windows: native_test wrapping a py_binary doesn't work - # on Windows. - target_compatible_with = select({ - "@platforms//os:windows": ["@platforms//:incompatible"], - "//conditions:default": [], - }), -) diff --git a/examples/bzlmod/py_proto_library/example.com/another_proto/BUILD.bazel b/examples/bzlmod/py_proto_library/example.com/another_proto/BUILD.bazel deleted file mode 100644 index 29f08c21ca..0000000000 --- a/examples/bzlmod/py_proto_library/example.com/another_proto/BUILD.bazel +++ /dev/null @@ -1,16 +0,0 @@ -load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") -load("@rules_python//python:proto.bzl", "py_proto_library") - -py_proto_library( - name = "message_py_pb2", - visibility = ["//visibility:public"], - deps = [":message_proto"], -) - -proto_library( - name = "message_proto", - srcs = ["message.proto"], - # https://bazel.build/reference/be/protocol-buffer#proto_library.strip_import_prefix - strip_import_prefix = "/py_proto_library/example.com", - deps = ["//py_proto_library/example.com/proto:pricetag_proto"], -) diff --git a/examples/bzlmod/py_proto_library/example.com/another_proto/message.proto b/examples/bzlmod/py_proto_library/example.com/another_proto/message.proto deleted file mode 100644 index 6e7dcc5793..0000000000 --- a/examples/bzlmod/py_proto_library/example.com/another_proto/message.proto +++ /dev/null @@ -1,10 +0,0 @@ -syntax = "proto3"; - -package rules_python; - -import "proto/pricetag.proto"; - -message TestMessage { - uint32 index = 1; - PriceTag pricetag = 2; -} diff --git a/examples/bzlmod/py_proto_library/example.com/proto/BUILD.bazel b/examples/bzlmod/py_proto_library/example.com/proto/BUILD.bazel deleted file mode 100644 index 1f8e8f2818..0000000000 --- a/examples/bzlmod/py_proto_library/example.com/proto/BUILD.bazel +++ /dev/null @@ -1,17 +0,0 @@ -load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") -load("@rules_python//python:proto.bzl", "py_proto_library") - -py_proto_library( - name = "pricetag_py_pb2", - visibility = ["//visibility:public"], - deps = [":pricetag_proto"], -) - -proto_library( - name = "pricetag_proto", - srcs = ["pricetag.proto"], - # https://bazel.build/reference/be/protocol-buffer#proto_library.strip_import_prefix - strip_import_prefix = "/py_proto_library/example.com", - visibility = ["//visibility:public"], - deps = ["@com_google_protobuf//:any_proto"], -) diff --git a/examples/bzlmod/py_proto_library/example.com/proto/pricetag.proto b/examples/bzlmod/py_proto_library/example.com/proto/pricetag.proto deleted file mode 100644 index 3fa68de84b..0000000000 --- a/examples/bzlmod/py_proto_library/example.com/proto/pricetag.proto +++ /dev/null @@ -1,11 +0,0 @@ -syntax = "proto3"; - -import "google/protobuf/any.proto"; - -package rules_python; - -message PriceTag { - string name = 2; - double cost = 1; - google.protobuf.Any metadata = 3; -} diff --git a/examples/bzlmod/py_proto_library/foo_external/BUILD.bazel b/examples/bzlmod/py_proto_library/foo_external/BUILD.bazel deleted file mode 100644 index 183a3c28d2..0000000000 --- a/examples/bzlmod/py_proto_library/foo_external/BUILD.bazel +++ /dev/null @@ -1,22 +0,0 @@ -load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") -load("@com_google_protobuf//bazel:py_proto_library.bzl", "py_proto_library") -load("@rules_python//python:py_binary.bzl", "py_binary") - -package(default_visibility = ["//visibility:public"]) - -proto_library( - name = "proto_lib", - srcs = ["nested/foo/my_proto.proto"], - strip_import_prefix = "/nested/foo", -) - -py_proto_library( - name = "a_proto", - deps = [":proto_lib"], -) - -py_binary( - name = "py_binary_with_proto", - srcs = ["py_binary_with_proto.py"], - deps = [":a_proto"], -) diff --git a/examples/bzlmod/py_proto_library/foo_external/MODULE.bazel b/examples/bzlmod/py_proto_library/foo_external/MODULE.bazel deleted file mode 100644 index aca6f98eab..0000000000 --- a/examples/bzlmod/py_proto_library/foo_external/MODULE.bazel +++ /dev/null @@ -1,7 +0,0 @@ -module( - name = "foo_external", - version = "0.0.1", -) - -bazel_dep(name = "rules_python", version = "1.0.0") -bazel_dep(name = "protobuf", version = "28.2", repo_name = "com_google_protobuf") diff --git a/examples/bzlmod/py_proto_library/foo_external/nested/foo/my_proto.proto b/examples/bzlmod/py_proto_library/foo_external/nested/foo/my_proto.proto deleted file mode 100644 index 7b8440cbed..0000000000 --- a/examples/bzlmod/py_proto_library/foo_external/nested/foo/my_proto.proto +++ /dev/null @@ -1,6 +0,0 @@ -syntax = "proto3"; - -package my_proto; - -message MyMessage { -} diff --git a/examples/bzlmod/py_proto_library/foo_external/py_binary_with_proto.py b/examples/bzlmod/py_proto_library/foo_external/py_binary_with_proto.py deleted file mode 100644 index 67e798bb8f..0000000000 --- a/examples/bzlmod/py_proto_library/foo_external/py_binary_with_proto.py +++ /dev/null @@ -1,6 +0,0 @@ -import sys - -if __name__ == "__main__": - import my_proto_pb2 - - sys.exit(0) diff --git a/examples/bzlmod/py_proto_library/message_test.py b/examples/bzlmod/py_proto_library/message_test.py deleted file mode 100644 index b1a6942a54..0000000000 --- a/examples/bzlmod/py_proto_library/message_test.py +++ /dev/null @@ -1,16 +0,0 @@ -import sys -import unittest - -from another_proto import message_pb2 - - -class TestCase(unittest.TestCase): - def test_message(self): - got = message_pb2.TestMessage( - index=5, - ) - self.assertIsNotNone(got) - - -if __name__ == "__main__": - sys.exit(unittest.main()) diff --git a/examples/bzlmod/py_proto_library/test.py b/examples/bzlmod/py_proto_library/test.py deleted file mode 100644 index 24ab8ddc70..0000000000 --- a/examples/bzlmod/py_proto_library/test.py +++ /dev/null @@ -1,21 +0,0 @@ -import json -import unittest - -from proto import pricetag_pb2 - - -class TestCase(unittest.TestCase): - def test_pricetag(self): - got = pricetag_pb2.PriceTag( - name="dollar", - cost=5.00, - ) - - metadata = {"description": "some text..."} - got.metadata.value = json.dumps(metadata).encode("utf-8") - - self.assertIsNotNone(got) - - -if __name__ == "__main__": - unittest.main() diff --git a/examples/bzlmod/requirements_lock_3_11.txt b/examples/bzlmod/requirements_lock_3_11.txt new file mode 100644 index 0000000000..dd6ec4d29e --- /dev/null +++ b/examples/bzlmod/requirements_lock_3_11.txt @@ -0,0 +1,530 @@ +# This file was autogenerated by uv via the following command: +# bazel run //examples:bzlmod_requirements_3_11.update +--index-url https://pypi.org/simple +--extra-index-url https://pypi.org/simple/ + +alabaster==0.7.16 \ + --hash=sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65 \ + --hash=sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92 + # via sphinx +astroid==2.13.5 \ + --hash=sha256:6891f444625b6edb2ac798829b689e95297e100ddf89dbed5a8c610e34901501 \ + --hash=sha256:df164d5ac811b9f44105a72b8f9d5edfb7b5b2d7e979b04ea377a77b3229114a + # via pylint +babel==2.17.0 \ + --hash=sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d \ + --hash=sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2 + # via sphinx +certifi==2025.10.5 \ + --hash=sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de \ + --hash=sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43 + # via requests +chardet==4.0.0 \ + --hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa \ + --hash=sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5 + # via requests +colorama==0.4.6 \ + --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ + --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 + # via + # -r examples/bzlmod/requirements.in + # pylint + # sphinx +dill==0.4.0 \ + --hash=sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0 \ + --hash=sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049 + # via pylint +docutils==0.21.2 \ + --hash=sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f \ + --hash=sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2 + # via sphinx +idna==2.10 \ + --hash=sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6 \ + --hash=sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0 + # via requests +imagesize==1.4.1 \ + --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \ + --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a + # via sphinx +isort==5.13.2 \ + --hash=sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109 \ + --hash=sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6 + # via pylint +jinja2==3.1.6 \ + --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ + --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 + # via sphinx +lazy-object-proxy==1.12.0 \ + --hash=sha256:029d2b355076710505c9545aef5ab3f750d89779310e26ddf2b7b23f6ea03cd8 \ + --hash=sha256:08c465fb5cd23527512f9bd7b4c7ba6cec33e28aad36fbbe46bf7b858f9f3f7f \ + --hash=sha256:0a83c6f7a6b2bfc11ef3ed67f8cbe99f8ff500b05655d8e7df9aab993a6abc95 \ + --hash=sha256:1192e8c2f1031a6ff453ee40213afa01ba765b3dc861302cd91dbdb2e2660b00 \ + --hash=sha256:14e348185adbd03ec17d051e169ec45686dcd840a3779c9d4c10aabe2ca6e1c0 \ + --hash=sha256:15400b18893f345857b9e18b9bd87bd06aba84af6ed086187add70aeaa3f93f1 \ + --hash=sha256:1cf69cd1a6c7fe2dbcc3edaa017cf010f4192e53796538cc7d5e1fedbfa4bcff \ + --hash=sha256:1f5a462d92fd0cfb82f1fab28b51bfb209fabbe6aabf7f0d51472c0c124c0c61 \ + --hash=sha256:256262384ebd2a77b023ad02fbcc9326282bcfd16484d5531154b02bc304f4c5 \ + --hash=sha256:31020c84005d3daa4cc0fa5a310af2066efe6b0d82aeebf9ab199292652ff036 \ + --hash=sha256:338ab2f132276203e404951205fe80c3fd59429b3a724e7b662b2eb539bb1be9 \ + --hash=sha256:3605b632e82a1cbc32a1e5034278a64db555b3496e0795723ee697006b980508 \ + --hash=sha256:3d3964fbd326578bcdfffd017ef101b6fb0484f34e731fe060ba9b8816498c36 \ + --hash=sha256:424a8ab6695400845c39f13c685050eab69fa0bbac5790b201cd27375e5e41d7 \ + --hash=sha256:4a79b909aa16bde8ae606f06e6bbc9d3219d2e57fb3e0076e17879072b742c65 \ + --hash=sha256:4ab2c584e3cc8be0dfca422e05ad30a9abe3555ce63e9ab7a559f62f8dbc6ff9 \ + --hash=sha256:53c7fd99eb156bbb82cbc5d5188891d8fdd805ba6c1e3b92b90092da2a837073 \ + --hash=sha256:563d2ec8e4d4b68ee7848c5ab4d6057a6d703cb7963b342968bb8758dda33a23 \ + --hash=sha256:61d5e3310a4aa5792c2b599a7a78ccf8687292c8eb09cf187cca8f09cf6a7519 \ + --hash=sha256:6763941dbf97eea6b90f5b06eb4da9418cc088fce0e3883f5816090f9afcde4a \ + --hash=sha256:67f07ab742f1adfb3966c40f630baaa7902be4222a17941f3d85fd1dae5565ff \ + --hash=sha256:717484c309df78cedf48396e420fa57fc8a2b1f06ea889df7248fdd156e58847 \ + --hash=sha256:75ba769017b944fcacbf6a80c18b2761a1795b03f8899acdad1f1c39db4409be \ + --hash=sha256:7601ec171c7e8584f8ff3f4e440aa2eebf93e854f04639263875b8c2971f819f \ + --hash=sha256:7b22c2bbfb155706b928ac4d74c1a63ac8552a55ba7fff4445155523ea4067e1 \ + --hash=sha256:800f32b00a47c27446a2b767df7538e6c66a3488632c402b4fb2224f9794f3c0 \ + --hash=sha256:81d1852fb30fab81696f93db1b1e55a5d1ff7940838191062f5f56987d5fcc3e \ + --hash=sha256:86fd61cb2ba249b9f436d789d1356deae69ad3231dc3c0f17293ac535162672e \ + --hash=sha256:8c40b3c9faee2e32bfce0df4ae63f4e73529766893258eca78548bac801c8f66 \ + --hash=sha256:8ee0d6027b760a11cc18281e702c0309dd92da458a74b4c15025d7fc490deede \ + --hash=sha256:997b1d6e10ecc6fb6fe0f2c959791ae59599f41da61d652f6c903d1ee58b7370 \ + --hash=sha256:a61095f5d9d1a743e1e20ec6d6db6c2ca511961777257ebd9b288951b23b44fa \ + --hash=sha256:a6b7ea5ea1ffe15059eb44bcbcb258f97bcb40e139b88152c40d07b1a1dfc9ac \ + --hash=sha256:ae575ad9b674d0029fc077c5231b3bc6b433a3d1a62a8c363df96974b5534728 \ + --hash=sha256:be5fe974e39ceb0d6c9db0663c0464669cf866b2851c73971409b9566e880eab \ + --hash=sha256:be9045646d83f6c2664c1330904b245ae2371b5c57a3195e4028aedc9f999655 \ + --hash=sha256:c1ca33565f698ac1aece152a10f432415d1a2aa9a42dfe23e5ba2bc255ab91f6 \ + --hash=sha256:c3b2e0af1f7f77c4263759c4824316ce458fabe0fceadcd24ef8ca08b2d1e402 \ + --hash=sha256:c4fcbe74fb85df8ba7825fa05eddca764138da752904b378f0ae5ab33a36c308 \ + --hash=sha256:c9defba70ab943f1df98a656247966d7729da2fe9c2d5d85346464bf320820a3 \ + --hash=sha256:cc6e3614eca88b1c8a625fc0a47d0d745e7c3255b21dac0e30b3037c5e3deeb8 \ + --hash=sha256:d01c7819a410f7c255b20799b65d36b414379a30c6f1684c7bd7eb6777338c1b \ + --hash=sha256:efff4375a8c52f55a145dc8487a2108c2140f0bec4151ab4e1843e52eb9987ad \ + --hash=sha256:fdc70d81235fc586b9e3d1aeef7d1553259b62ecaae9db2167a5d2550dcc391a + # via astroid +markupsafe==3.0.3 \ + --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ + --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ + --hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \ + --hash=sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19 \ + --hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \ + --hash=sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c \ + --hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \ + --hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \ + --hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \ + --hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \ + --hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \ + --hash=sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26 \ + --hash=sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1 \ + --hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \ + --hash=sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218 \ + --hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \ + --hash=sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695 \ + --hash=sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad \ + --hash=sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73 \ + --hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \ + --hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \ + --hash=sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa \ + --hash=sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559 \ + --hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \ + --hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \ + --hash=sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758 \ + --hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \ + --hash=sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8 \ + --hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \ + --hash=sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c \ + --hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \ + --hash=sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a \ + --hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \ + --hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \ + --hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \ + --hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \ + --hash=sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2 \ + --hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \ + --hash=sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354 \ + --hash=sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50 \ + --hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \ + --hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \ + --hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \ + --hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \ + --hash=sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115 \ + --hash=sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e \ + --hash=sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485 \ + --hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \ + --hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \ + --hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \ + --hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \ + --hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \ + --hash=sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b \ + --hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \ + --hash=sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 \ + --hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \ + --hash=sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d \ + --hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \ + --hash=sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287 \ + --hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \ + --hash=sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f \ + --hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \ + --hash=sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed \ + --hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \ + --hash=sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c \ + --hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \ + --hash=sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8 \ + --hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \ + --hash=sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6 \ + --hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \ + --hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \ + --hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d \ + --hash=sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01 \ + --hash=sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7 \ + --hash=sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419 \ + --hash=sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795 \ + --hash=sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1 \ + --hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \ + --hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \ + --hash=sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42 \ + --hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \ + --hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \ + --hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \ + --hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \ + --hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \ + --hash=sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591 \ + --hash=sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc \ + --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ + --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 + # via jinja2 +mccabe==0.7.0 \ + --hash=sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325 \ + --hash=sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e + # via pylint +packaging==25.0 \ + --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ + --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f + # via sphinx +pathspec==0.12.1 \ + --hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \ + --hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712 + # via yamllint +platformdirs==4.4.0 \ + --hash=sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85 \ + --hash=sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf + # via pylint +pygments==2.19.2 \ + --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ + --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b + # via sphinx +pylint==2.15.10 \ + --hash=sha256:9df0d07e8948a1c3ffa3b6e2d7e6e63d9fb457c5da5b961ed63106594780cc7e \ + --hash=sha256:b3dc5ef7d33858f297ac0d06cc73862f01e4f2e74025ec3eff347ce0bc60baf5 + # via + # -r examples/bzlmod/requirements.in + # pylint-print +pylint-print==1.0.1 \ + --hash=sha256:30aa207e9718ebf4ceb47fb87012092e6d8743aab932aa07aa14a73e750ad3d0 \ + --hash=sha256:a2b2599e7887b93e551db2624c523c1e6e9e58c3be8416cd98d41e4427e2669b + # via -r examples/bzlmod/requirements.in +python-dateutil==2.9.0.post0 \ + --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ + --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 + # via + # -r examples/bzlmod/requirements.in + # s3cmd +python-magic==0.4.27 \ + --hash=sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b \ + --hash=sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3 + # via s3cmd +pyyaml==6.0.3 \ + --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ + --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ + --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ + --hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \ + --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \ + --hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \ + --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \ + --hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \ + --hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \ + --hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \ + --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \ + --hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \ + --hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \ + --hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \ + --hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \ + --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \ + --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \ + --hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \ + --hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \ + --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \ + --hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \ + --hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \ + --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \ + --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \ + --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \ + --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \ + --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \ + --hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \ + --hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \ + --hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \ + --hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \ + --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \ + --hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \ + --hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \ + --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \ + --hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \ + --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \ + --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \ + --hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \ + --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \ + --hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \ + --hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \ + --hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \ + --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \ + --hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \ + --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \ + --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \ + --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \ + --hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \ + --hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \ + --hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \ + --hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \ + --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \ + --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \ + --hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \ + --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \ + --hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \ + --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \ + --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \ + --hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \ + --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \ + --hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \ + --hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \ + --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \ + --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \ + --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ + --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \ + --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \ + --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \ + --hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \ + --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ + --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ + --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 + # via yamllint +requests==2.25.1 \ + --hash=sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804 \ + --hash=sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e + # via + # -r examples/bzlmod/requirements.in + # sphinx +s3cmd==2.1.0 \ + --hash=sha256:49cd23d516b17974b22b611a95ce4d93fe326feaa07320bd1d234fed68cbccfa \ + --hash=sha256:966b0a494a916fc3b4324de38f089c86c70ee90e8e1cae6d59102103a4c0cc03 + # via -r examples/bzlmod/requirements.in +six==1.17.0 \ + --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ + --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 + # via python-dateutil +snowballstemmer==3.0.1 \ + --hash=sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064 \ + --hash=sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895 + # via sphinx +sphinx==7.3.7 \ + --hash=sha256:413f75440be4cacf328f580b4274ada4565fb2187d696a84970c23f77b64d8c3 \ + --hash=sha256:a4a7db75ed37531c05002d56ed6948d4c42f473a36f46e1382b0bd76ca9627bc + # via -r examples/bzlmod/requirements.in +sphinxcontrib-applehelp==2.0.0 \ + --hash=sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1 \ + --hash=sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5 + # via sphinx +sphinxcontrib-devhelp==2.0.0 \ + --hash=sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad \ + --hash=sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2 + # via sphinx +sphinxcontrib-htmlhelp==2.1.0 \ + --hash=sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8 \ + --hash=sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9 + # via sphinx +sphinxcontrib-jsmath==1.0.1 \ + --hash=sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178 \ + --hash=sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8 + # via sphinx +sphinxcontrib-qthelp==2.0.0 \ + --hash=sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab \ + --hash=sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb + # via sphinx +sphinxcontrib-serializinghtml==2.0.0 \ + --hash=sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331 \ + --hash=sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d + # via + # -r examples/bzlmod/requirements.in + # sphinx +tabulate==0.9.0 \ + --hash=sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c \ + --hash=sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f + # via -r examples/bzlmod/requirements.in +tomlkit==0.13.3 \ + --hash=sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1 \ + --hash=sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0 + # via pylint +urllib3==1.26.20 \ + --hash=sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e \ + --hash=sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32 + # via requests +websockets==15.0.1 \ + --hash=sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2 \ + --hash=sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9 \ + --hash=sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5 \ + --hash=sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3 \ + --hash=sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8 \ + --hash=sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e \ + --hash=sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1 \ + --hash=sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256 \ + --hash=sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85 \ + --hash=sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880 \ + --hash=sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123 \ + --hash=sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375 \ + --hash=sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065 \ + --hash=sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed \ + --hash=sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41 \ + --hash=sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411 \ + --hash=sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597 \ + --hash=sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f \ + --hash=sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c \ + --hash=sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3 \ + --hash=sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb \ + --hash=sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e \ + --hash=sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee \ + --hash=sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f \ + --hash=sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf \ + --hash=sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf \ + --hash=sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4 \ + --hash=sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a \ + --hash=sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665 \ + --hash=sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22 \ + --hash=sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675 \ + --hash=sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4 \ + --hash=sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d \ + --hash=sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5 \ + --hash=sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65 \ + --hash=sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792 \ + --hash=sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57 \ + --hash=sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9 \ + --hash=sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3 \ + --hash=sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151 \ + --hash=sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d \ + --hash=sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475 \ + --hash=sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940 \ + --hash=sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431 \ + --hash=sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee \ + --hash=sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413 \ + --hash=sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8 \ + --hash=sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b \ + --hash=sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a \ + --hash=sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054 \ + --hash=sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb \ + --hash=sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205 \ + --hash=sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04 \ + --hash=sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4 \ + --hash=sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa \ + --hash=sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9 \ + --hash=sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122 \ + --hash=sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b \ + --hash=sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905 \ + --hash=sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770 \ + --hash=sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe \ + --hash=sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b \ + --hash=sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562 \ + --hash=sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561 \ + --hash=sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215 \ + --hash=sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931 \ + --hash=sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9 \ + --hash=sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f \ + --hash=sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7 + # via -r examples/bzlmod/requirements.in +wheel==0.45.1 \ + --hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 \ + --hash=sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248 + # via -r examples/bzlmod/requirements.in +wrapt==1.17.3 \ + --hash=sha256:02b551d101f31694fc785e58e0720ef7d9a10c4e62c1c9358ce6f63f23e30a56 \ + --hash=sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828 \ + --hash=sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f \ + --hash=sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396 \ + --hash=sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77 \ + --hash=sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d \ + --hash=sha256:0f5f51a6466667a5a356e6381d362d259125b57f059103dd9fdc8c0cf1d14139 \ + --hash=sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7 \ + --hash=sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb \ + --hash=sha256:1f23fa283f51c890eda8e34e4937079114c74b4c81d2b2f1f1d94948f5cc3d7f \ + --hash=sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f \ + --hash=sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067 \ + --hash=sha256:24c2ed34dc222ed754247a2702b1e1e89fdbaa4016f324b4b8f1a802d4ffe87f \ + --hash=sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7 \ + --hash=sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b \ + --hash=sha256:30ce38e66630599e1193798285706903110d4f057aab3168a34b7fdc85569afc \ + --hash=sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05 \ + --hash=sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd \ + --hash=sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7 \ + --hash=sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9 \ + --hash=sha256:3e62d15d3cfa26e3d0788094de7b64efa75f3a53875cdbccdf78547aed547a81 \ + --hash=sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977 \ + --hash=sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa \ + --hash=sha256:46acc57b331e0b3bcb3e1ca3b421d65637915cfcd65eb783cb2f78a511193f9b \ + --hash=sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe \ + --hash=sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58 \ + --hash=sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8 \ + --hash=sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77 \ + --hash=sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85 \ + --hash=sha256:55cbbc356c2842f39bcc553cf695932e8b30e30e797f961860afb308e6b1bb7c \ + --hash=sha256:59923aa12d0157f6b82d686c3fd8e1166fa8cdfb3e17b42ce3b6147ff81528df \ + --hash=sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454 \ + --hash=sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a \ + --hash=sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e \ + --hash=sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c \ + --hash=sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6 \ + --hash=sha256:656873859b3b50eeebe6db8b1455e99d90c26ab058db8e427046dbc35c3140a5 \ + --hash=sha256:65d1d00fbfb3ea5f20add88bbc0f815150dbbde3b026e6c24759466c8b5a9ef9 \ + --hash=sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd \ + --hash=sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277 \ + --hash=sha256:70d86fa5197b8947a2fa70260b48e400bf2ccacdcab97bb7de47e3d1e6312225 \ + --hash=sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22 \ + --hash=sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116 \ + --hash=sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16 \ + --hash=sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc \ + --hash=sha256:758895b01d546812d1f42204bd443b8c433c44d090248bf22689df673ccafe00 \ + --hash=sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2 \ + --hash=sha256:7e18f01b0c3e4a07fe6dfdb00e29049ba17eadbc5e7609a2a3a4af83ab7d710a \ + --hash=sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804 \ + --hash=sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04 \ + --hash=sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1 \ + --hash=sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba \ + --hash=sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390 \ + --hash=sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0 \ + --hash=sha256:a7c06742645f914f26c7f1fa47b8bc4c91d222f76ee20116c43d5ef0912bba2d \ + --hash=sha256:a9a2203361a6e6404f80b99234fe7fb37d1fc73487b5a78dc1aa5b97201e0f22 \ + --hash=sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0 \ + --hash=sha256:ad85e269fe54d506b240d2d7b9f5f2057c2aa9a2ea5b32c66f8902f768117ed2 \ + --hash=sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18 \ + --hash=sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6 \ + --hash=sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311 \ + --hash=sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89 \ + --hash=sha256:caea3e9c79d5f0d2c6d9ab96111601797ea5da8e6d0723f77eabb0d4068d2b2f \ + --hash=sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39 \ + --hash=sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4 \ + --hash=sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5 \ + --hash=sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa \ + --hash=sha256:df7d30371a2accfe4013e90445f6388c570f103d61019b6b7c57e0265250072a \ + --hash=sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050 \ + --hash=sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6 \ + --hash=sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235 \ + --hash=sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056 \ + --hash=sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2 \ + --hash=sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418 \ + --hash=sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c \ + --hash=sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a \ + --hash=sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6 \ + --hash=sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0 \ + --hash=sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775 \ + --hash=sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10 \ + --hash=sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c + # via astroid +yamllint==1.37.1 \ + --hash=sha256:364f0d79e81409f591e323725e6a9f4504c8699ddf2d7263d8d2b539cd66a583 \ + --hash=sha256:81f7c0c5559becc8049470d86046b36e96113637bcbe4753ecef06977c00245d + # via -r examples/bzlmod/requirements.in diff --git a/examples/bzlmod/requirements_windows_3_11.txt b/examples/bzlmod/requirements_windows_3_11.txt new file mode 100644 index 0000000000..fd7895b273 --- /dev/null +++ b/examples/bzlmod/requirements_windows_3_11.txt @@ -0,0 +1,530 @@ +# This file was autogenerated by uv via the following command: +# bazel run //examples:bzlmod_requirements_3_11_windows.update +--index-url https://pypi.org/simple +--extra-index-url https://pypi.org/simple/ + +alabaster==0.7.16 \ + --hash=sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65 \ + --hash=sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92 + # via sphinx +astroid==2.13.5 \ + --hash=sha256:6891f444625b6edb2ac798829b689e95297e100ddf89dbed5a8c610e34901501 \ + --hash=sha256:df164d5ac811b9f44105a72b8f9d5edfb7b5b2d7e979b04ea377a77b3229114a + # via pylint +babel==2.17.0 \ + --hash=sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d \ + --hash=sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2 + # via sphinx +certifi==2025.10.5 \ + --hash=sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de \ + --hash=sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43 + # via requests +chardet==4.0.0 \ + --hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa \ + --hash=sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5 + # via requests +colorama==0.4.6 \ + --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ + --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 + # via + # -r examples/bzlmod/requirements.in + # pylint + # sphinx +dill==0.4.0 \ + --hash=sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0 \ + --hash=sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049 + # via pylint +docutils==0.21.2 \ + --hash=sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f \ + --hash=sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2 + # via sphinx +idna==2.10 \ + --hash=sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6 \ + --hash=sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0 + # via requests +imagesize==1.4.1 \ + --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \ + --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a + # via sphinx +isort==5.13.2 \ + --hash=sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109 \ + --hash=sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6 + # via pylint +jinja2==3.1.6 \ + --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ + --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 + # via sphinx +lazy-object-proxy==1.12.0 \ + --hash=sha256:029d2b355076710505c9545aef5ab3f750d89779310e26ddf2b7b23f6ea03cd8 \ + --hash=sha256:08c465fb5cd23527512f9bd7b4c7ba6cec33e28aad36fbbe46bf7b858f9f3f7f \ + --hash=sha256:0a83c6f7a6b2bfc11ef3ed67f8cbe99f8ff500b05655d8e7df9aab993a6abc95 \ + --hash=sha256:1192e8c2f1031a6ff453ee40213afa01ba765b3dc861302cd91dbdb2e2660b00 \ + --hash=sha256:14e348185adbd03ec17d051e169ec45686dcd840a3779c9d4c10aabe2ca6e1c0 \ + --hash=sha256:15400b18893f345857b9e18b9bd87bd06aba84af6ed086187add70aeaa3f93f1 \ + --hash=sha256:1cf69cd1a6c7fe2dbcc3edaa017cf010f4192e53796538cc7d5e1fedbfa4bcff \ + --hash=sha256:1f5a462d92fd0cfb82f1fab28b51bfb209fabbe6aabf7f0d51472c0c124c0c61 \ + --hash=sha256:256262384ebd2a77b023ad02fbcc9326282bcfd16484d5531154b02bc304f4c5 \ + --hash=sha256:31020c84005d3daa4cc0fa5a310af2066efe6b0d82aeebf9ab199292652ff036 \ + --hash=sha256:338ab2f132276203e404951205fe80c3fd59429b3a724e7b662b2eb539bb1be9 \ + --hash=sha256:3605b632e82a1cbc32a1e5034278a64db555b3496e0795723ee697006b980508 \ + --hash=sha256:3d3964fbd326578bcdfffd017ef101b6fb0484f34e731fe060ba9b8816498c36 \ + --hash=sha256:424a8ab6695400845c39f13c685050eab69fa0bbac5790b201cd27375e5e41d7 \ + --hash=sha256:4a79b909aa16bde8ae606f06e6bbc9d3219d2e57fb3e0076e17879072b742c65 \ + --hash=sha256:4ab2c584e3cc8be0dfca422e05ad30a9abe3555ce63e9ab7a559f62f8dbc6ff9 \ + --hash=sha256:53c7fd99eb156bbb82cbc5d5188891d8fdd805ba6c1e3b92b90092da2a837073 \ + --hash=sha256:563d2ec8e4d4b68ee7848c5ab4d6057a6d703cb7963b342968bb8758dda33a23 \ + --hash=sha256:61d5e3310a4aa5792c2b599a7a78ccf8687292c8eb09cf187cca8f09cf6a7519 \ + --hash=sha256:6763941dbf97eea6b90f5b06eb4da9418cc088fce0e3883f5816090f9afcde4a \ + --hash=sha256:67f07ab742f1adfb3966c40f630baaa7902be4222a17941f3d85fd1dae5565ff \ + --hash=sha256:717484c309df78cedf48396e420fa57fc8a2b1f06ea889df7248fdd156e58847 \ + --hash=sha256:75ba769017b944fcacbf6a80c18b2761a1795b03f8899acdad1f1c39db4409be \ + --hash=sha256:7601ec171c7e8584f8ff3f4e440aa2eebf93e854f04639263875b8c2971f819f \ + --hash=sha256:7b22c2bbfb155706b928ac4d74c1a63ac8552a55ba7fff4445155523ea4067e1 \ + --hash=sha256:800f32b00a47c27446a2b767df7538e6c66a3488632c402b4fb2224f9794f3c0 \ + --hash=sha256:81d1852fb30fab81696f93db1b1e55a5d1ff7940838191062f5f56987d5fcc3e \ + --hash=sha256:86fd61cb2ba249b9f436d789d1356deae69ad3231dc3c0f17293ac535162672e \ + --hash=sha256:8c40b3c9faee2e32bfce0df4ae63f4e73529766893258eca78548bac801c8f66 \ + --hash=sha256:8ee0d6027b760a11cc18281e702c0309dd92da458a74b4c15025d7fc490deede \ + --hash=sha256:997b1d6e10ecc6fb6fe0f2c959791ae59599f41da61d652f6c903d1ee58b7370 \ + --hash=sha256:a61095f5d9d1a743e1e20ec6d6db6c2ca511961777257ebd9b288951b23b44fa \ + --hash=sha256:a6b7ea5ea1ffe15059eb44bcbcb258f97bcb40e139b88152c40d07b1a1dfc9ac \ + --hash=sha256:ae575ad9b674d0029fc077c5231b3bc6b433a3d1a62a8c363df96974b5534728 \ + --hash=sha256:be5fe974e39ceb0d6c9db0663c0464669cf866b2851c73971409b9566e880eab \ + --hash=sha256:be9045646d83f6c2664c1330904b245ae2371b5c57a3195e4028aedc9f999655 \ + --hash=sha256:c1ca33565f698ac1aece152a10f432415d1a2aa9a42dfe23e5ba2bc255ab91f6 \ + --hash=sha256:c3b2e0af1f7f77c4263759c4824316ce458fabe0fceadcd24ef8ca08b2d1e402 \ + --hash=sha256:c4fcbe74fb85df8ba7825fa05eddca764138da752904b378f0ae5ab33a36c308 \ + --hash=sha256:c9defba70ab943f1df98a656247966d7729da2fe9c2d5d85346464bf320820a3 \ + --hash=sha256:cc6e3614eca88b1c8a625fc0a47d0d745e7c3255b21dac0e30b3037c5e3deeb8 \ + --hash=sha256:d01c7819a410f7c255b20799b65d36b414379a30c6f1684c7bd7eb6777338c1b \ + --hash=sha256:efff4375a8c52f55a145dc8487a2108c2140f0bec4151ab4e1843e52eb9987ad \ + --hash=sha256:fdc70d81235fc586b9e3d1aeef7d1553259b62ecaae9db2167a5d2550dcc391a + # via astroid +markupsafe==3.0.3 \ + --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ + --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ + --hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \ + --hash=sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19 \ + --hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \ + --hash=sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c \ + --hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \ + --hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \ + --hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \ + --hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \ + --hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \ + --hash=sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26 \ + --hash=sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1 \ + --hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \ + --hash=sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218 \ + --hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \ + --hash=sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695 \ + --hash=sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad \ + --hash=sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73 \ + --hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \ + --hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \ + --hash=sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa \ + --hash=sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559 \ + --hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \ + --hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \ + --hash=sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758 \ + --hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \ + --hash=sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8 \ + --hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \ + --hash=sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c \ + --hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \ + --hash=sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a \ + --hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \ + --hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \ + --hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \ + --hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \ + --hash=sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2 \ + --hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \ + --hash=sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354 \ + --hash=sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50 \ + --hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \ + --hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \ + --hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \ + --hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \ + --hash=sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115 \ + --hash=sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e \ + --hash=sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485 \ + --hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \ + --hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \ + --hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \ + --hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \ + --hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \ + --hash=sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b \ + --hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \ + --hash=sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 \ + --hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \ + --hash=sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d \ + --hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \ + --hash=sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287 \ + --hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \ + --hash=sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f \ + --hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \ + --hash=sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed \ + --hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \ + --hash=sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c \ + --hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \ + --hash=sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8 \ + --hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \ + --hash=sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6 \ + --hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \ + --hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \ + --hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d \ + --hash=sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01 \ + --hash=sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7 \ + --hash=sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419 \ + --hash=sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795 \ + --hash=sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1 \ + --hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \ + --hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \ + --hash=sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42 \ + --hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \ + --hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \ + --hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \ + --hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \ + --hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \ + --hash=sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591 \ + --hash=sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc \ + --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ + --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 + # via jinja2 +mccabe==0.7.0 \ + --hash=sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325 \ + --hash=sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e + # via pylint +packaging==25.0 \ + --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ + --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f + # via sphinx +pathspec==0.12.1 \ + --hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \ + --hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712 + # via yamllint +platformdirs==4.4.0 \ + --hash=sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85 \ + --hash=sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf + # via pylint +pygments==2.19.2 \ + --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ + --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b + # via sphinx +pylint==2.15.10 \ + --hash=sha256:9df0d07e8948a1c3ffa3b6e2d7e6e63d9fb457c5da5b961ed63106594780cc7e \ + --hash=sha256:b3dc5ef7d33858f297ac0d06cc73862f01e4f2e74025ec3eff347ce0bc60baf5 + # via + # -r examples/bzlmod/requirements.in + # pylint-print +pylint-print==1.0.1 \ + --hash=sha256:30aa207e9718ebf4ceb47fb87012092e6d8743aab932aa07aa14a73e750ad3d0 \ + --hash=sha256:a2b2599e7887b93e551db2624c523c1e6e9e58c3be8416cd98d41e4427e2669b + # via -r examples/bzlmod/requirements.in +python-dateutil==2.9.0.post0 \ + --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ + --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 + # via + # -r examples/bzlmod/requirements.in + # s3cmd +python-magic==0.4.27 \ + --hash=sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b \ + --hash=sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3 + # via s3cmd +pyyaml==6.0.3 \ + --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ + --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ + --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ + --hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \ + --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \ + --hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \ + --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \ + --hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \ + --hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \ + --hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \ + --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \ + --hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \ + --hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \ + --hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \ + --hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \ + --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \ + --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \ + --hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \ + --hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \ + --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \ + --hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \ + --hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \ + --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \ + --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \ + --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \ + --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \ + --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \ + --hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \ + --hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \ + --hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \ + --hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \ + --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \ + --hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \ + --hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \ + --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \ + --hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \ + --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \ + --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \ + --hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \ + --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \ + --hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \ + --hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \ + --hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \ + --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \ + --hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \ + --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \ + --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \ + --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \ + --hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \ + --hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \ + --hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \ + --hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \ + --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \ + --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \ + --hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \ + --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \ + --hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \ + --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \ + --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \ + --hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \ + --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \ + --hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \ + --hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \ + --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \ + --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \ + --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ + --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \ + --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \ + --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \ + --hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \ + --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ + --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ + --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 + # via yamllint +requests==2.25.1 \ + --hash=sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804 \ + --hash=sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e + # via + # -r examples/bzlmod/requirements.in + # sphinx +s3cmd==2.1.0 \ + --hash=sha256:49cd23d516b17974b22b611a95ce4d93fe326feaa07320bd1d234fed68cbccfa \ + --hash=sha256:966b0a494a916fc3b4324de38f089c86c70ee90e8e1cae6d59102103a4c0cc03 + # via -r examples/bzlmod/requirements.in +six==1.17.0 \ + --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ + --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 + # via python-dateutil +snowballstemmer==3.0.1 \ + --hash=sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064 \ + --hash=sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895 + # via sphinx +sphinx==7.3.7 \ + --hash=sha256:413f75440be4cacf328f580b4274ada4565fb2187d696a84970c23f77b64d8c3 \ + --hash=sha256:a4a7db75ed37531c05002d56ed6948d4c42f473a36f46e1382b0bd76ca9627bc + # via -r examples/bzlmod/requirements.in +sphinxcontrib-applehelp==2.0.0 \ + --hash=sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1 \ + --hash=sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5 + # via sphinx +sphinxcontrib-devhelp==2.0.0 \ + --hash=sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad \ + --hash=sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2 + # via sphinx +sphinxcontrib-htmlhelp==2.1.0 \ + --hash=sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8 \ + --hash=sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9 + # via sphinx +sphinxcontrib-jsmath==1.0.1 \ + --hash=sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178 \ + --hash=sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8 + # via sphinx +sphinxcontrib-qthelp==2.0.0 \ + --hash=sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab \ + --hash=sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb + # via sphinx +sphinxcontrib-serializinghtml==2.0.0 \ + --hash=sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331 \ + --hash=sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d + # via + # -r examples/bzlmod/requirements.in + # sphinx +tabulate==0.9.0 \ + --hash=sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c \ + --hash=sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f + # via -r examples/bzlmod/requirements.in +tomlkit==0.13.3 \ + --hash=sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1 \ + --hash=sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0 + # via pylint +urllib3==1.26.20 \ + --hash=sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e \ + --hash=sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32 + # via requests +websockets==15.0.1 \ + --hash=sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2 \ + --hash=sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9 \ + --hash=sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5 \ + --hash=sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3 \ + --hash=sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8 \ + --hash=sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e \ + --hash=sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1 \ + --hash=sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256 \ + --hash=sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85 \ + --hash=sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880 \ + --hash=sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123 \ + --hash=sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375 \ + --hash=sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065 \ + --hash=sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed \ + --hash=sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41 \ + --hash=sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411 \ + --hash=sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597 \ + --hash=sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f \ + --hash=sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c \ + --hash=sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3 \ + --hash=sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb \ + --hash=sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e \ + --hash=sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee \ + --hash=sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f \ + --hash=sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf \ + --hash=sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf \ + --hash=sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4 \ + --hash=sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a \ + --hash=sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665 \ + --hash=sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22 \ + --hash=sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675 \ + --hash=sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4 \ + --hash=sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d \ + --hash=sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5 \ + --hash=sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65 \ + --hash=sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792 \ + --hash=sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57 \ + --hash=sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9 \ + --hash=sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3 \ + --hash=sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151 \ + --hash=sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d \ + --hash=sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475 \ + --hash=sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940 \ + --hash=sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431 \ + --hash=sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee \ + --hash=sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413 \ + --hash=sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8 \ + --hash=sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b \ + --hash=sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a \ + --hash=sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054 \ + --hash=sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb \ + --hash=sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205 \ + --hash=sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04 \ + --hash=sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4 \ + --hash=sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa \ + --hash=sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9 \ + --hash=sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122 \ + --hash=sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b \ + --hash=sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905 \ + --hash=sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770 \ + --hash=sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe \ + --hash=sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b \ + --hash=sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562 \ + --hash=sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561 \ + --hash=sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215 \ + --hash=sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931 \ + --hash=sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9 \ + --hash=sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f \ + --hash=sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7 + # via -r examples/bzlmod/requirements.in +wheel==0.45.1 \ + --hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 \ + --hash=sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248 + # via -r examples/bzlmod/requirements.in +wrapt==1.17.3 \ + --hash=sha256:02b551d101f31694fc785e58e0720ef7d9a10c4e62c1c9358ce6f63f23e30a56 \ + --hash=sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828 \ + --hash=sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f \ + --hash=sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396 \ + --hash=sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77 \ + --hash=sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d \ + --hash=sha256:0f5f51a6466667a5a356e6381d362d259125b57f059103dd9fdc8c0cf1d14139 \ + --hash=sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7 \ + --hash=sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb \ + --hash=sha256:1f23fa283f51c890eda8e34e4937079114c74b4c81d2b2f1f1d94948f5cc3d7f \ + --hash=sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f \ + --hash=sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067 \ + --hash=sha256:24c2ed34dc222ed754247a2702b1e1e89fdbaa4016f324b4b8f1a802d4ffe87f \ + --hash=sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7 \ + --hash=sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b \ + --hash=sha256:30ce38e66630599e1193798285706903110d4f057aab3168a34b7fdc85569afc \ + --hash=sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05 \ + --hash=sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd \ + --hash=sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7 \ + --hash=sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9 \ + --hash=sha256:3e62d15d3cfa26e3d0788094de7b64efa75f3a53875cdbccdf78547aed547a81 \ + --hash=sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977 \ + --hash=sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa \ + --hash=sha256:46acc57b331e0b3bcb3e1ca3b421d65637915cfcd65eb783cb2f78a511193f9b \ + --hash=sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe \ + --hash=sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58 \ + --hash=sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8 \ + --hash=sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77 \ + --hash=sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85 \ + --hash=sha256:55cbbc356c2842f39bcc553cf695932e8b30e30e797f961860afb308e6b1bb7c \ + --hash=sha256:59923aa12d0157f6b82d686c3fd8e1166fa8cdfb3e17b42ce3b6147ff81528df \ + --hash=sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454 \ + --hash=sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a \ + --hash=sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e \ + --hash=sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c \ + --hash=sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6 \ + --hash=sha256:656873859b3b50eeebe6db8b1455e99d90c26ab058db8e427046dbc35c3140a5 \ + --hash=sha256:65d1d00fbfb3ea5f20add88bbc0f815150dbbde3b026e6c24759466c8b5a9ef9 \ + --hash=sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd \ + --hash=sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277 \ + --hash=sha256:70d86fa5197b8947a2fa70260b48e400bf2ccacdcab97bb7de47e3d1e6312225 \ + --hash=sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22 \ + --hash=sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116 \ + --hash=sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16 \ + --hash=sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc \ + --hash=sha256:758895b01d546812d1f42204bd443b8c433c44d090248bf22689df673ccafe00 \ + --hash=sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2 \ + --hash=sha256:7e18f01b0c3e4a07fe6dfdb00e29049ba17eadbc5e7609a2a3a4af83ab7d710a \ + --hash=sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804 \ + --hash=sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04 \ + --hash=sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1 \ + --hash=sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba \ + --hash=sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390 \ + --hash=sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0 \ + --hash=sha256:a7c06742645f914f26c7f1fa47b8bc4c91d222f76ee20116c43d5ef0912bba2d \ + --hash=sha256:a9a2203361a6e6404f80b99234fe7fb37d1fc73487b5a78dc1aa5b97201e0f22 \ + --hash=sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0 \ + --hash=sha256:ad85e269fe54d506b240d2d7b9f5f2057c2aa9a2ea5b32c66f8902f768117ed2 \ + --hash=sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18 \ + --hash=sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6 \ + --hash=sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311 \ + --hash=sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89 \ + --hash=sha256:caea3e9c79d5f0d2c6d9ab96111601797ea5da8e6d0723f77eabb0d4068d2b2f \ + --hash=sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39 \ + --hash=sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4 \ + --hash=sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5 \ + --hash=sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa \ + --hash=sha256:df7d30371a2accfe4013e90445f6388c570f103d61019b6b7c57e0265250072a \ + --hash=sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050 \ + --hash=sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6 \ + --hash=sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235 \ + --hash=sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056 \ + --hash=sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2 \ + --hash=sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418 \ + --hash=sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c \ + --hash=sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a \ + --hash=sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6 \ + --hash=sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0 \ + --hash=sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775 \ + --hash=sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10 \ + --hash=sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c + # via astroid +yamllint==1.37.1 \ + --hash=sha256:364f0d79e81409f591e323725e6a9f4504c8699ddf2d7263d8d2b539cd66a583 \ + --hash=sha256:81f7c0c5559becc8049470d86046b36e96113637bcbe4753ecef06977c00245d + # via -r examples/bzlmod/requirements.in diff --git a/examples/bzlmod/test.py b/examples/bzlmod/test.py index 24be3ba3fe..3febed7585 100644 --- a/examples/bzlmod/test.py +++ b/examples/bzlmod/test.py @@ -13,7 +13,6 @@ # limitations under the License. import os -import pathlib import re import sys import unittest @@ -59,7 +58,7 @@ def test_coverage_sys_path(self): f"sys.path has {len(sys.path)} items:\n {all_paths}", ) - first_item, last_item = sys.path[0], sys.path[-1] + first_item, _ = sys.path[0], sys.path[-1] self.assertFalse( first_item.endswith("coverage"), f"Expected the first item in sys.path '{first_item}' to not be related to coverage", diff --git a/examples/bzlmod/tests/my_lib_test.py b/examples/bzlmod/tests/my_lib_test.py index b06374c983..019d29e31f 100644 --- a/examples/bzlmod/tests/my_lib_test.py +++ b/examples/bzlmod/tests/my_lib_test.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os import sys import libs.my_lib as my_lib diff --git a/examples/bzlmod/tests/other_module/BUILD.bazel b/examples/bzlmod/tests/other_module/BUILD.bazel index 1bd8a900a9..f4756e7c3e 100644 --- a/examples/bzlmod/tests/other_module/BUILD.bazel +++ b/examples/bzlmod/tests/other_module/BUILD.bazel @@ -5,6 +5,7 @@ # in the root module. load("@bazel_skylib//rules:build_test.bzl", "build_test") +load("@rules_python//python:py_test.bzl", "py_test") build_test( name = "other_module_bin_build_test", @@ -12,3 +13,27 @@ build_test( "@our_other_module//other_module/pkg:bin", ], ) + +# Regression test for https://github.com/bazel-contrib/rules_python/pull/3919: py_binary_rule_builder()'s implicit +# attr defaults previously had no visibility outside of rules_python, so +# building this target (defined via the builder from an external module) +# failed at analysis time with a visibility error. +build_test( + name = "other_module_rule_builder_build_test", + targets = [ + "@our_other_module//other_module/rule_builder:app", + ], +) + +py_test( + name = "other_module_import_test", + srcs = ["other_module_import_test.py"], + data = ["@our_other_module//other_module/pkg:bin_zipapp"], + env = {"ZIPAPP_PATH": "$(location @our_other_module//other_module/pkg:bin_zipapp)"}, + # For now, skip this test on Windows because it fails for reasons + # other than the code path being tested. + target_compatible_with = select({ + "@platforms//os:windows": ["@platforms//:incompatible"], + "//conditions:default": [], + }), +) diff --git a/examples/bzlmod/tests/other_module/other_module_import_test.py b/examples/bzlmod/tests/other_module/other_module_import_test.py new file mode 100644 index 0000000000..b5b15c383d --- /dev/null +++ b/examples/bzlmod/tests/other_module/other_module_import_test.py @@ -0,0 +1,25 @@ +"""Regression test for https://github.com/bazel-contrib/rules_python/issues/3563""" + +import os +import subprocess +import sys + + +def main(): + # The rlocation path for the bin_zipapp. It is in the "our_other_module" repository. + zipapp_path = os.environ.get("ZIPAPP_PATH") + print(f"Running bin_zipapp at: {zipapp_path}") + + result = subprocess.run([zipapp_path], capture_output=True, text=True) + print("--- bin_zippapp stdout ---") + print(result.stdout) + print("--- bin_zippapp stderr ---") + print(result.stderr) + + if result.returncode != 0: + print(f"bin_zippapp failed with return code {result.returncode}") + sys.exit(result.returncode) + + +if __name__ == "__main__": + main() diff --git a/examples/bzlmod_build_file_generation/.bazelrc b/examples/bzlmod_build_file_generation/.bazelrc deleted file mode 100644 index 0289886d4d..0000000000 --- a/examples/bzlmod_build_file_generation/.bazelrc +++ /dev/null @@ -1,9 +0,0 @@ -test --test_output=errors --enable_runfiles - -# Windows requires these for multi-python support: -build --enable_runfiles - -common --experimental_enable_bzlmod - -coverage --java_runtime_version=remotejdk_11 -common:bazel7.x --incompatible_python_disallow_native_rules diff --git a/examples/bzlmod_build_file_generation/other_module/MODULE.bazel b/examples/bzlmod_build_file_generation/other_module/MODULE.bazel deleted file mode 100644 index 992e120760..0000000000 --- a/examples/bzlmod_build_file_generation/other_module/MODULE.bazel +++ /dev/null @@ -1,5 +0,0 @@ -module( - name = "other_module", -) - -bazel_dep(name = "rules_python", version = "") diff --git a/examples/multi_python_versions/MODULE.bazel b/examples/multi_python_versions/MODULE.bazel index 4e4a0473c2..82faaf8214 100644 --- a/examples/multi_python_versions/MODULE.bazel +++ b/examples/multi_python_versions/MODULE.bazel @@ -12,20 +12,28 @@ local_path_override( python = use_extension("@rules_python//python/extensions:python.bzl", "python") python.defaults( # The environment variable takes precedence if set. - python_version = "3.9", + python_version = "3.10", python_version_env = "BAZEL_PYTHON_VERSION", ) python.toolchain( configure_coverage_tool = True, - python_version = "3.9", + python_version = "3.10", ) python.toolchain( configure_coverage_tool = True, - python_version = "3.10", + python_version = "3.11", ) python.toolchain( configure_coverage_tool = True, - python_version = "3.11", + python_version = "3.12", +) +python.toolchain( + configure_coverage_tool = True, + python_version = "3.13", +) +python.toolchain( + configure_coverage_tool = True, + python_version = "3.14", ) use_repo( python, @@ -35,11 +43,7 @@ use_repo( pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") use_repo(pip, "pypi") -pip.parse( - hub_name = "pypi", - python_version = "3.9", - requirements_lock = "//requirements:requirements_lock_3_9.txt", -) + pip.parse( hub_name = "pypi", python_version = "3.10", @@ -50,10 +54,25 @@ pip.parse( python_version = "3.11", requirements_lock = "//requirements:requirements_lock_3_11.txt", ) +pip.parse( + hub_name = "pypi", + python_version = "3.12", + requirements_lock = "//requirements:requirements_lock_3_12.txt", +) +pip.parse( + hub_name = "pypi", + python_version = "3.13", + requirements_lock = "//requirements:requirements_lock_3_13.txt", +) +pip.parse( + hub_name = "pypi", + python_version = "3.14", + requirements_lock = "//requirements:requirements_lock_3_14.txt", +) # example test dependencies bazel_dep(name = "rules_shell", version = "0.2.0", dev_dependency = True) # Only needed to make rules_python's CI happy. rules_java 8.3.0+ is needed so # that --java_runtime_version=remotejdk_11 works with Bazel 8. -bazel_dep(name = "rules_java", version = "8.3.1") +bazel_dep(name = "rules_java", version = "8.16.1") diff --git a/examples/multi_python_versions/WORKSPACE b/examples/multi_python_versions/WORKSPACE index 6b69e0a891..0b6b8a0cbf 100644 --- a/examples/multi_python_versions/WORKSPACE +++ b/examples/multi_python_versions/WORKSPACE @@ -9,15 +9,17 @@ load("@rules_python//python:repositories.bzl", "py_repositories", "python_regist py_repositories() -default_python_version = "3.9" +default_python_version = "3.10" python_register_multi_toolchains( name = "python", default_version = default_python_version, python_versions = [ - "3.9", "3.10", "3.11", + "3.12", + "3.13", + "3.14", ], register_coverage_tool = True, ) @@ -30,12 +32,16 @@ multi_pip_parse( python_interpreter_target = { "3.10": "@python_3_10_host//:python", "3.11": "@python_3_11_host//:python", - "3.9": "@python_3_9_host//:python", + "3.12": "@python_3_12_host//:python", + "3.13": "@python_3_13_host//:python", + "3.14": "@python_3_14_host//:python", }, requirements_lock = { "3.10": "//requirements:requirements_lock_3_10.txt", "3.11": "//requirements:requirements_lock_3_11.txt", - "3.9": "//requirements:requirements_lock_3_9.txt", + "3.12": "//requirements:requirements_lock_3_12.txt", + "3.13": "//requirements:requirements_lock_3_13.txt", + "3.14": "//requirements:requirements_lock_3_14.txt", }, ) diff --git a/examples/multi_python_versions/requirements/BUILD.bazel b/examples/multi_python_versions/requirements/BUILD.bazel index 516a378df8..ee8ff029f8 100644 --- a/examples/multi_python_versions/requirements/BUILD.bazel +++ b/examples/multi_python_versions/requirements/BUILD.bazel @@ -1,12 +1,5 @@ load("@rules_python//python:pip.bzl", "compile_pip_requirements") -compile_pip_requirements( - name = "requirements_3_9", - src = "requirements.in", - python_version = "3.9", - requirements_txt = "requirements_lock_3_9.txt", -) - compile_pip_requirements( name = "requirements_3_10", src = "requirements.in", @@ -20,3 +13,24 @@ compile_pip_requirements( python_version = "3.11", requirements_txt = "requirements_lock_3_11.txt", ) + +compile_pip_requirements( + name = "requirements_3_12", + src = "requirements.in", + python_version = "3.12", + requirements_txt = "requirements_lock_3_12.txt", +) + +compile_pip_requirements( + name = "requirements_3_13", + src = "requirements.in", + python_version = "3.13", + requirements_txt = "requirements_lock_3_13.txt", +) + +compile_pip_requirements( + name = "requirements_3_14", + src = "requirements.in", + python_version = "3.14", + requirements_txt = "requirements_lock_3_14.txt", +) diff --git a/examples/multi_python_versions/requirements/requirements_lock_3_9.txt b/examples/multi_python_versions/requirements/requirements_lock_3_12.txt similarity index 98% rename from examples/multi_python_versions/requirements/requirements_lock_3_9.txt rename to examples/multi_python_versions/requirements/requirements_lock_3_12.txt index 3c696a865e..818b20e14c 100644 --- a/examples/multi_python_versions/requirements/requirements_lock_3_9.txt +++ b/examples/multi_python_versions/requirements/requirements_lock_3_12.txt @@ -1,8 +1,8 @@ # -# This file is autogenerated by pip-compile with Python 3.9 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # -# bazel run //requirements:requirements_3_9.update +# bazel run //requirements:requirements_3_12.update # websockets==11.0.3 ; python_full_version > "3.9.1" \ --hash=sha256:01f5567d9cf6f502d655151645d4e8b72b453413d3819d2b6f1185abc23e82dd \ diff --git a/examples/multi_python_versions/requirements/requirements_lock_3_13.txt b/examples/multi_python_versions/requirements/requirements_lock_3_13.txt new file mode 100644 index 0000000000..8dc44b8a07 --- /dev/null +++ b/examples/multi_python_versions/requirements/requirements_lock_3_13.txt @@ -0,0 +1,78 @@ +# +# This file is autogenerated by pip-compile with Python 3.13 +# by the following command: +# +# bazel run //requirements:requirements_3_13.update +# +websockets==11.0.3 ; python_full_version > "3.9.1" \ + --hash=sha256:01f5567d9cf6f502d655151645d4e8b72b453413d3819d2b6f1185abc23e82dd \ + --hash=sha256:03aae4edc0b1c68498f41a6772d80ac7c1e33c06c6ffa2ac1c27a07653e79d6f \ + --hash=sha256:0ac56b661e60edd453585f4bd68eb6a29ae25b5184fd5ba51e97652580458998 \ + --hash=sha256:0ee68fe502f9031f19d495dae2c268830df2760c0524cbac5d759921ba8c8e82 \ + --hash=sha256:1553cb82942b2a74dd9b15a018dce645d4e68674de2ca31ff13ebc2d9f283788 \ + --hash=sha256:1a073fc9ab1c8aff37c99f11f1641e16da517770e31a37265d2755282a5d28aa \ + --hash=sha256:1d2256283fa4b7f4c7d7d3e84dc2ece74d341bce57d5b9bf385df109c2a1a82f \ + --hash=sha256:1d5023a4b6a5b183dc838808087033ec5df77580485fc533e7dab2567851b0a4 \ + --hash=sha256:1fdf26fa8a6a592f8f9235285b8affa72748dc12e964a5518c6c5e8f916716f7 \ + --hash=sha256:2529338a6ff0eb0b50c7be33dc3d0e456381157a31eefc561771ee431134a97f \ + --hash=sha256:279e5de4671e79a9ac877427f4ac4ce93751b8823f276b681d04b2156713b9dd \ + --hash=sha256:2d903ad4419f5b472de90cd2d40384573b25da71e33519a67797de17ef849b69 \ + --hash=sha256:332d126167ddddec94597c2365537baf9ff62dfcc9db4266f263d455f2f031cb \ + --hash=sha256:34fd59a4ac42dff6d4681d8843217137f6bc85ed29722f2f7222bd619d15e95b \ + --hash=sha256:3580dd9c1ad0701169e4d6fc41e878ffe05e6bdcaf3c412f9d559389d0c9e016 \ + --hash=sha256:3ccc8a0c387629aec40f2fc9fdcb4b9d5431954f934da3eaf16cdc94f67dbfac \ + --hash=sha256:41f696ba95cd92dc047e46b41b26dd24518384749ed0d99bea0a941ca87404c4 \ + --hash=sha256:42cc5452a54a8e46a032521d7365da775823e21bfba2895fb7b77633cce031bb \ + --hash=sha256:4841ed00f1026dfbced6fca7d963c4e7043aa832648671b5138008dc5a8f6d99 \ + --hash=sha256:4b253869ea05a5a073ebfdcb5cb3b0266a57c3764cf6fe114e4cd90f4bfa5f5e \ + --hash=sha256:54c6e5b3d3a8936a4ab6870d46bdd6ec500ad62bde9e44462c32d18f1e9a8e54 \ + --hash=sha256:619d9f06372b3a42bc29d0cd0354c9bb9fb39c2cbc1a9c5025b4538738dbffaf \ + --hash=sha256:6505c1b31274723ccaf5f515c1824a4ad2f0d191cec942666b3d0f3aa4cb4007 \ + --hash=sha256:660e2d9068d2bedc0912af508f30bbeb505bbbf9774d98def45f68278cea20d3 \ + --hash=sha256:6681ba9e7f8f3b19440921e99efbb40fc89f26cd71bf539e45d8c8a25c976dc6 \ + --hash=sha256:68b977f21ce443d6d378dbd5ca38621755f2063d6fdb3335bda981d552cfff86 \ + --hash=sha256:69269f3a0b472e91125b503d3c0b3566bda26da0a3261c49f0027eb6075086d1 \ + --hash=sha256:6f1a3f10f836fab6ca6efa97bb952300b20ae56b409414ca85bff2ad241d2a61 \ + --hash=sha256:7622a89d696fc87af8e8d280d9b421db5133ef5b29d3f7a1ce9f1a7bf7fcfa11 \ + --hash=sha256:777354ee16f02f643a4c7f2b3eff8027a33c9861edc691a2003531f5da4f6bc8 \ + --hash=sha256:84d27a4832cc1a0ee07cdcf2b0629a8a72db73f4cf6de6f0904f6661227f256f \ + --hash=sha256:8531fdcad636d82c517b26a448dcfe62f720e1922b33c81ce695d0edb91eb931 \ + --hash=sha256:86d2a77fd490ae3ff6fae1c6ceaecad063d3cc2320b44377efdde79880e11526 \ + --hash=sha256:88fc51d9a26b10fc331be344f1781224a375b78488fc343620184e95a4b27016 \ + --hash=sha256:8a34e13a62a59c871064dfd8ffb150867e54291e46d4a7cf11d02c94a5275bae \ + --hash=sha256:8c82f11964f010053e13daafdc7154ce7385ecc538989a354ccc7067fd7028fd \ + --hash=sha256:92b2065d642bf8c0a82d59e59053dd2fdde64d4ed44efe4870fa816c1232647b \ + --hash=sha256:97b52894d948d2f6ea480171a27122d77af14ced35f62e5c892ca2fae9344311 \ + --hash=sha256:9d9acd80072abcc98bd2c86c3c9cd4ac2347b5a5a0cae7ed5c0ee5675f86d9af \ + --hash=sha256:9f59a3c656fef341a99e3d63189852be7084c0e54b75734cde571182c087b152 \ + --hash=sha256:aa5003845cdd21ac0dc6c9bf661c5beddd01116f6eb9eb3c8e272353d45b3288 \ + --hash=sha256:b16fff62b45eccb9c7abb18e60e7e446998093cdcb50fed33134b9b6878836de \ + --hash=sha256:b30c6590146e53149f04e85a6e4fcae068df4289e31e4aee1fdf56a0dead8f97 \ + --hash=sha256:b58cbf0697721120866820b89f93659abc31c1e876bf20d0b3d03cef14faf84d \ + --hash=sha256:b67c6f5e5a401fc56394f191f00f9b3811fe843ee93f4a70df3c389d1adf857d \ + --hash=sha256:bceab846bac555aff6427d060f2fcfff71042dba6f5fca7dc4f75cac815e57ca \ + --hash=sha256:bee9fcb41db2a23bed96c6b6ead6489702c12334ea20a297aa095ce6d31370d0 \ + --hash=sha256:c114e8da9b475739dde229fd3bc6b05a6537a88a578358bc8eb29b4030fac9c9 \ + --hash=sha256:c1f0524f203e3bd35149f12157438f406eff2e4fb30f71221c8a5eceb3617b6b \ + --hash=sha256:c792ea4eabc0159535608fc5658a74d1a81020eb35195dd63214dcf07556f67e \ + --hash=sha256:c7f3cb904cce8e1be667c7e6fef4516b98d1a6a0635a58a57528d577ac18a128 \ + --hash=sha256:d67ac60a307f760c6e65dad586f556dde58e683fab03323221a4e530ead6f74d \ + --hash=sha256:dcacf2c7a6c3a84e720d1bb2b543c675bf6c40e460300b628bab1b1efc7c034c \ + --hash=sha256:de36fe9c02995c7e6ae6efe2e205816f5f00c22fd1fbf343d4d18c3d5ceac2f5 \ + --hash=sha256:def07915168ac8f7853812cc593c71185a16216e9e4fa886358a17ed0fd9fcf6 \ + --hash=sha256:df41b9bc27c2c25b486bae7cf42fccdc52ff181c8c387bfd026624a491c2671b \ + --hash=sha256:e052b8467dd07d4943936009f46ae5ce7b908ddcac3fda581656b1b19c083d9b \ + --hash=sha256:e063b1865974611313a3849d43f2c3f5368093691349cf3c7c8f8f75ad7cb280 \ + --hash=sha256:e1459677e5d12be8bbc7584c35b992eea142911a6236a3278b9b5ce3326f282c \ + --hash=sha256:e1a99a7a71631f0efe727c10edfba09ea6bee4166a6f9c19aafb6c0b5917d09c \ + --hash=sha256:e590228200fcfc7e9109509e4d9125eace2042fd52b595dd22bbc34bb282307f \ + --hash=sha256:e6316827e3e79b7b8e7d8e3b08f4e331af91a48e794d5d8b099928b6f0b85f20 \ + --hash=sha256:e7837cb169eca3b3ae94cc5787c4fed99eef74c0ab9506756eea335e0d6f3ed8 \ + --hash=sha256:e848f46a58b9fcf3d06061d17be388caf70ea5b8cc3466251963c8345e13f7eb \ + --hash=sha256:ed058398f55163a79bb9f06a90ef9ccc063b204bb346c4de78efc5d15abfe602 \ + --hash=sha256:f2e58f2c36cc52d41f2659e4c0cbf7353e28c8c9e63e30d8c6d3494dc9fdedcf \ + --hash=sha256:f467ba0050b7de85016b43f5a22b46383ef004c4f672148a8abf32bc999a87f0 \ + --hash=sha256:f61bdb1df43dc9c131791fbc2355535f9024b9a04398d3bd0684fc16ab07df74 \ + --hash=sha256:fb06eea71a00a7af0ae6aefbb932fb8a7df3cb390cc217d51a9ad7343de1b8d0 \ + --hash=sha256:ffd7dcaf744f25f82190856bc26ed81721508fc5cbf2a330751e135ff1283564 + # via -r requirements/requirements.in diff --git a/examples/multi_python_versions/requirements/requirements_lock_3_14.txt b/examples/multi_python_versions/requirements/requirements_lock_3_14.txt new file mode 100644 index 0000000000..f0aaaa90af --- /dev/null +++ b/examples/multi_python_versions/requirements/requirements_lock_3_14.txt @@ -0,0 +1,78 @@ +# +# This file is autogenerated by pip-compile with Python 3.14 +# by the following command: +# +# bazel run //requirements:requirements_3_14.update +# +websockets==11.0.3 ; python_full_version > "3.9.1" \ + --hash=sha256:01f5567d9cf6f502d655151645d4e8b72b453413d3819d2b6f1185abc23e82dd \ + --hash=sha256:03aae4edc0b1c68498f41a6772d80ac7c1e33c06c6ffa2ac1c27a07653e79d6f \ + --hash=sha256:0ac56b661e60edd453585f4bd68eb6a29ae25b5184fd5ba51e97652580458998 \ + --hash=sha256:0ee68fe502f9031f19d495dae2c268830df2760c0524cbac5d759921ba8c8e82 \ + --hash=sha256:1553cb82942b2a74dd9b15a018dce645d4e68674de2ca31ff13ebc2d9f283788 \ + --hash=sha256:1a073fc9ab1c8aff37c99f11f1641e16da517770e31a37265d2755282a5d28aa \ + --hash=sha256:1d2256283fa4b7f4c7d7d3e84dc2ece74d341bce57d5b9bf385df109c2a1a82f \ + --hash=sha256:1d5023a4b6a5b183dc838808087033ec5df77580485fc533e7dab2567851b0a4 \ + --hash=sha256:1fdf26fa8a6a592f8f9235285b8affa72748dc12e964a5518c6c5e8f916716f7 \ + --hash=sha256:2529338a6ff0eb0b50c7be33dc3d0e456381157a31eefc561771ee431134a97f \ + --hash=sha256:279e5de4671e79a9ac877427f4ac4ce93751b8823f276b681d04b2156713b9dd \ + --hash=sha256:2d903ad4419f5b472de90cd2d40384573b25da71e33519a67797de17ef849b69 \ + --hash=sha256:332d126167ddddec94597c2365537baf9ff62dfcc9db4266f263d455f2f031cb \ + --hash=sha256:34fd59a4ac42dff6d4681d8843217137f6bc85ed29722f2f7222bd619d15e95b \ + --hash=sha256:3580dd9c1ad0701169e4d6fc41e878ffe05e6bdcaf3c412f9d559389d0c9e016 \ + --hash=sha256:3ccc8a0c387629aec40f2fc9fdcb4b9d5431954f934da3eaf16cdc94f67dbfac \ + --hash=sha256:41f696ba95cd92dc047e46b41b26dd24518384749ed0d99bea0a941ca87404c4 \ + --hash=sha256:42cc5452a54a8e46a032521d7365da775823e21bfba2895fb7b77633cce031bb \ + --hash=sha256:4841ed00f1026dfbced6fca7d963c4e7043aa832648671b5138008dc5a8f6d99 \ + --hash=sha256:4b253869ea05a5a073ebfdcb5cb3b0266a57c3764cf6fe114e4cd90f4bfa5f5e \ + --hash=sha256:54c6e5b3d3a8936a4ab6870d46bdd6ec500ad62bde9e44462c32d18f1e9a8e54 \ + --hash=sha256:619d9f06372b3a42bc29d0cd0354c9bb9fb39c2cbc1a9c5025b4538738dbffaf \ + --hash=sha256:6505c1b31274723ccaf5f515c1824a4ad2f0d191cec942666b3d0f3aa4cb4007 \ + --hash=sha256:660e2d9068d2bedc0912af508f30bbeb505bbbf9774d98def45f68278cea20d3 \ + --hash=sha256:6681ba9e7f8f3b19440921e99efbb40fc89f26cd71bf539e45d8c8a25c976dc6 \ + --hash=sha256:68b977f21ce443d6d378dbd5ca38621755f2063d6fdb3335bda981d552cfff86 \ + --hash=sha256:69269f3a0b472e91125b503d3c0b3566bda26da0a3261c49f0027eb6075086d1 \ + --hash=sha256:6f1a3f10f836fab6ca6efa97bb952300b20ae56b409414ca85bff2ad241d2a61 \ + --hash=sha256:7622a89d696fc87af8e8d280d9b421db5133ef5b29d3f7a1ce9f1a7bf7fcfa11 \ + --hash=sha256:777354ee16f02f643a4c7f2b3eff8027a33c9861edc691a2003531f5da4f6bc8 \ + --hash=sha256:84d27a4832cc1a0ee07cdcf2b0629a8a72db73f4cf6de6f0904f6661227f256f \ + --hash=sha256:8531fdcad636d82c517b26a448dcfe62f720e1922b33c81ce695d0edb91eb931 \ + --hash=sha256:86d2a77fd490ae3ff6fae1c6ceaecad063d3cc2320b44377efdde79880e11526 \ + --hash=sha256:88fc51d9a26b10fc331be344f1781224a375b78488fc343620184e95a4b27016 \ + --hash=sha256:8a34e13a62a59c871064dfd8ffb150867e54291e46d4a7cf11d02c94a5275bae \ + --hash=sha256:8c82f11964f010053e13daafdc7154ce7385ecc538989a354ccc7067fd7028fd \ + --hash=sha256:92b2065d642bf8c0a82d59e59053dd2fdde64d4ed44efe4870fa816c1232647b \ + --hash=sha256:97b52894d948d2f6ea480171a27122d77af14ced35f62e5c892ca2fae9344311 \ + --hash=sha256:9d9acd80072abcc98bd2c86c3c9cd4ac2347b5a5a0cae7ed5c0ee5675f86d9af \ + --hash=sha256:9f59a3c656fef341a99e3d63189852be7084c0e54b75734cde571182c087b152 \ + --hash=sha256:aa5003845cdd21ac0dc6c9bf661c5beddd01116f6eb9eb3c8e272353d45b3288 \ + --hash=sha256:b16fff62b45eccb9c7abb18e60e7e446998093cdcb50fed33134b9b6878836de \ + --hash=sha256:b30c6590146e53149f04e85a6e4fcae068df4289e31e4aee1fdf56a0dead8f97 \ + --hash=sha256:b58cbf0697721120866820b89f93659abc31c1e876bf20d0b3d03cef14faf84d \ + --hash=sha256:b67c6f5e5a401fc56394f191f00f9b3811fe843ee93f4a70df3c389d1adf857d \ + --hash=sha256:bceab846bac555aff6427d060f2fcfff71042dba6f5fca7dc4f75cac815e57ca \ + --hash=sha256:bee9fcb41db2a23bed96c6b6ead6489702c12334ea20a297aa095ce6d31370d0 \ + --hash=sha256:c114e8da9b475739dde229fd3bc6b05a6537a88a578358bc8eb29b4030fac9c9 \ + --hash=sha256:c1f0524f203e3bd35149f12157438f406eff2e4fb30f71221c8a5eceb3617b6b \ + --hash=sha256:c792ea4eabc0159535608fc5658a74d1a81020eb35195dd63214dcf07556f67e \ + --hash=sha256:c7f3cb904cce8e1be667c7e6fef4516b98d1a6a0635a58a57528d577ac18a128 \ + --hash=sha256:d67ac60a307f760c6e65dad586f556dde58e683fab03323221a4e530ead6f74d \ + --hash=sha256:dcacf2c7a6c3a84e720d1bb2b543c675bf6c40e460300b628bab1b1efc7c034c \ + --hash=sha256:de36fe9c02995c7e6ae6efe2e205816f5f00c22fd1fbf343d4d18c3d5ceac2f5 \ + --hash=sha256:def07915168ac8f7853812cc593c71185a16216e9e4fa886358a17ed0fd9fcf6 \ + --hash=sha256:df41b9bc27c2c25b486bae7cf42fccdc52ff181c8c387bfd026624a491c2671b \ + --hash=sha256:e052b8467dd07d4943936009f46ae5ce7b908ddcac3fda581656b1b19c083d9b \ + --hash=sha256:e063b1865974611313a3849d43f2c3f5368093691349cf3c7c8f8f75ad7cb280 \ + --hash=sha256:e1459677e5d12be8bbc7584c35b992eea142911a6236a3278b9b5ce3326f282c \ + --hash=sha256:e1a99a7a71631f0efe727c10edfba09ea6bee4166a6f9c19aafb6c0b5917d09c \ + --hash=sha256:e590228200fcfc7e9109509e4d9125eace2042fd52b595dd22bbc34bb282307f \ + --hash=sha256:e6316827e3e79b7b8e7d8e3b08f4e331af91a48e794d5d8b099928b6f0b85f20 \ + --hash=sha256:e7837cb169eca3b3ae94cc5787c4fed99eef74c0ab9506756eea335e0d6f3ed8 \ + --hash=sha256:e848f46a58b9fcf3d06061d17be388caf70ea5b8cc3466251963c8345e13f7eb \ + --hash=sha256:ed058398f55163a79bb9f06a90ef9ccc063b204bb346c4de78efc5d15abfe602 \ + --hash=sha256:f2e58f2c36cc52d41f2659e4c0cbf7353e28c8c9e63e30d8c6d3494dc9fdedcf \ + --hash=sha256:f467ba0050b7de85016b43f5a22b46383ef004c4f672148a8abf32bc999a87f0 \ + --hash=sha256:f61bdb1df43dc9c131791fbc2355535f9024b9a04398d3bd0684fc16ab07df74 \ + --hash=sha256:fb06eea71a00a7af0ae6aefbb932fb8a7df3cb390cc217d51a9ad7343de1b8d0 \ + --hash=sha256:ffd7dcaf744f25f82190856bc26ed81721508fc5cbf2a330751e135ff1283564 + # via -r requirements/requirements.in diff --git a/examples/multi_python_versions/tests/BUILD.bazel b/examples/multi_python_versions/tests/BUILD.bazel index 11fb98ca61..607058d992 100644 --- a/examples/multi_python_versions/tests/BUILD.bazel +++ b/examples/multi_python_versions/tests/BUILD.bazel @@ -23,24 +23,38 @@ py_binary( ) py_binary( - name = "version_3_9", + name = "version_3_10", srcs = ["version.py"], main = "version.py", - python_version = "3.9", + python_version = "3.10", ) py_binary( - name = "version_3_10", + name = "version_3_11", srcs = ["version.py"], main = "version.py", - python_version = "3.10", + python_version = "3.11", ) py_binary( - name = "version_3_11", + name = "version_3_12", srcs = ["version.py"], main = "version.py", - python_version = "3.11", + python_version = "3.12", +) + +py_binary( + name = "version_3_13", + srcs = ["version.py"], + main = "version.py", + python_version = "3.13", +) + +py_binary( + name = "version_3_14", + srcs = ["version.py"], + main = "version.py", + python_version = "3.14", ) py_test( @@ -51,26 +65,42 @@ py_test( ) py_test( - name = "my_lib_3_9_test", + name = "my_lib_3_10_test", + srcs = ["my_lib_test.py"], + main = "my_lib_test.py", + python_version = "3.10", + deps = ["//libs/my_lib"], +) + +py_test( + name = "my_lib_3_11_test", srcs = ["my_lib_test.py"], main = "my_lib_test.py", - python_version = "3.9", + python_version = "3.11", deps = ["//libs/my_lib"], ) py_test( - name = "my_lib_3_10_test", + name = "my_lib_3_12_test", srcs = ["my_lib_test.py"], main = "my_lib_test.py", - python_version = "3.10", + python_version = "3.12", deps = ["//libs/my_lib"], ) py_test( - name = "my_lib_3_11_test", + name = "my_lib_3_13_test", srcs = ["my_lib_test.py"], main = "my_lib_test.py", - python_version = "3.11", + python_version = "3.13", + deps = ["//libs/my_lib"], +) + +py_test( + name = "my_lib_3_14_test", + srcs = ["my_lib_test.py"], + main = "my_lib_test.py", + python_version = "3.14", deps = ["//libs/my_lib"], ) @@ -84,15 +114,7 @@ copy_file( py_test( name = "version_default_test", srcs = ["version_default_test.py"], - env = {"VERSION_CHECK": "3.9"}, # The default defined in the WORKSPACE. -) - -py_test( - name = "version_3_9_test", - srcs = ["version_test.py"], - env = {"VERSION_CHECK": "3.9"}, - main = "version_test.py", - python_version = "3.9", + env = {"VERSION_CHECK": "3.10"}, # The default defined in the WORKSPACE/MODULE ) py_test( @@ -112,28 +134,52 @@ py_test( ) py_test( - name = "version_default_takes_3_10_subprocess_test", + name = "version_3_12_test", + srcs = ["version_test.py"], + env = {"VERSION_CHECK": "3.12"}, + main = "version_test.py", + python_version = "3.12", +) + +py_test( + name = "version_3_13_test", + srcs = ["version_test.py"], + env = {"VERSION_CHECK": "3.13"}, + main = "version_test.py", + python_version = "3.13", +) + +py_test( + name = "version_3_14_test", + srcs = ["version_test.py"], + env = {"VERSION_CHECK": "3.14"}, + main = "version_test.py", + python_version = "3.14", +) + +py_test( + name = "version_default_takes_3_11_subprocess_test", srcs = ["cross_version_test.py"], - data = [":version_3_10"], + data = [":version_3_11"], env = { - "SUBPROCESS_VERSION_CHECK": "3.10", - "SUBPROCESS_VERSION_PY_BINARY": "$(rootpaths :version_3_10)", - "VERSION_CHECK": "3.9", + "SUBPROCESS_VERSION_CHECK": "3.11", + "SUBPROCESS_VERSION_PY_BINARY": "$(rootpaths :version_3_11)", + "VERSION_CHECK": "3.10", }, main = "cross_version_test.py", ) py_test( - name = "version_3_10_takes_3_9_subprocess_test", + name = "version_3_11_takes_3_10_subprocess_test", srcs = ["cross_version_test.py"], - data = [":version_3_9"], + data = [":version_3_10"], env = { - "SUBPROCESS_VERSION_CHECK": "3.9", - "SUBPROCESS_VERSION_PY_BINARY": "$(rootpaths :version_3_9)", - "VERSION_CHECK": "3.10", + "SUBPROCESS_VERSION_CHECK": "3.10", + "SUBPROCESS_VERSION_PY_BINARY": "$(rootpaths :version_3_10)", + "VERSION_CHECK": "3.11", }, main = "cross_version_test.py", - python_version = "3.10", + python_version = "3.11", ) sh_test( @@ -141,28 +187,28 @@ sh_test( srcs = ["version_test.sh"], data = [":version_default"], env = { - "VERSION_CHECK": "3.9", # The default defined in the WORKSPACE. + "VERSION_CHECK": "3.10", # The default defined in the WORKSPACE/MODULE. "VERSION_PY_BINARY": "$(rootpaths :version_default)", }, ) sh_test( - name = "version_test_binary_3_9", + name = "version_test_binary_3_10", srcs = ["version_test.sh"], - data = [":version_3_9"], + data = [":version_3_10"], env = { - "VERSION_CHECK": "3.9", - "VERSION_PY_BINARY": "$(rootpaths :version_3_9)", + "VERSION_CHECK": "3.10", + "VERSION_PY_BINARY": "$(rootpaths :version_3_10)", }, ) sh_test( - name = "version_test_binary_3_10", + name = "version_test_binary_3_11", srcs = ["version_test.sh"], - data = [":version_3_10"], + data = [":version_3_11"], env = { - "VERSION_CHECK": "3.10", - "VERSION_PY_BINARY": "$(rootpaths :version_3_10)", + "VERSION_CHECK": "3.11", + "VERSION_PY_BINARY": "$(rootpaths :version_3_11)", }, ) diff --git a/examples/multi_python_versions/tests/my_lib_test.py b/examples/multi_python_versions/tests/my_lib_test.py index 449cb8473c..b6c577c55a 100644 --- a/examples/multi_python_versions/tests/my_lib_test.py +++ b/examples/multi_python_versions/tests/my_lib_test.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os import sys import libs.my_lib as my_lib diff --git a/examples/pip_parse/BUILD.bazel b/examples/pip_parse/BUILD.bazel index 6ed8d26286..2dc9d61127 100644 --- a/examples/pip_parse/BUILD.bazel +++ b/examples/pip_parse/BUILD.bazel @@ -45,9 +45,6 @@ py_test( deps = [":main"], ) -# For pip dependencies which have entry points, the `entry_point` macro can be -# used from the generated `pip_parse` repository to access a runnable binary. - py_console_script_binary( name = "yamllint", pkg = "@pypi//yamllint", @@ -79,5 +76,8 @@ py_test( "WHEEL_DIST_INFO_CONTENTS": "$(rootpaths @pypi//requests:dist_info)", "YAMLLINT_ENTRY_POINT": "$(rlocationpath :yamllint)", }, - deps = ["@rules_python//python/runfiles"], + deps = [ + "@pypi//libclang", + "@rules_python//python/runfiles", + ], ) diff --git a/examples/pip_parse/MODULE.bazel b/examples/pip_parse/MODULE.bazel index f9ca90833f..ead5a06e29 100644 --- a/examples/pip_parse/MODULE.bazel +++ b/examples/pip_parse/MODULE.bazel @@ -9,14 +9,14 @@ local_path_override( python = use_extension("@rules_python//python/extensions:python.bzl", "python") python.toolchain( # We can specify the exact version. - python_version = "3.9.13", + python_version = "3.9.25", ) # You can use this repo mapping to ensure that your BUILD.bazel files don't need # to be updated when the python version changes to a different `3.9` version. use_repo( python, - python_3_9 = "python_3_9_13", + python_3_9 = "python_3_9_25", ) pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") @@ -34,7 +34,7 @@ pip.parse( }, hub_name = "pypi", # We need to use the same version here as in the `python.toolchain` call. - python_version = "3.9.13", + python_version = "3.9.25", requirements_lock = "//:requirements_lock.txt", requirements_windows = "//:requirements_windows.txt", ) diff --git a/examples/pip_parse/WORKSPACE b/examples/pip_parse/WORKSPACE index bb4714d941..e0d60af9ff 100644 --- a/examples/pip_parse/WORKSPACE +++ b/examples/pip_parse/WORKSPACE @@ -11,7 +11,7 @@ py_repositories() python_register_toolchains( name = "python_3_9", - python_version = "3.9.13", + python_version = "3.9.25", ) load("@rules_python//python:pip.bzl", "pip_parse") diff --git a/examples/pip_parse/pip_parse_test.py b/examples/pip_parse/pip_parse_test.py index 2fdd45477e..89e5eca254 100644 --- a/examples/pip_parse/pip_parse_test.py +++ b/examples/pip_parse/pip_parse_test.py @@ -50,18 +50,22 @@ def test_entry_point(self): def test_data(self): actual = os.environ.get("WHEEL_DATA_CONTENTS") self.assertIsNotNone(actual) - actual = self._remove_leading_dirs(actual.split(" ")) + actual = set(self._remove_leading_dirs(actual.split(" "))) - self.assertListEqual( - actual, - [ - "data/share/doc/packages/s3cmd/INSTALL.md", - "data/share/doc/packages/s3cmd/LICENSE", - "data/share/doc/packages/s3cmd/NEWS", - "data/share/doc/packages/s3cmd/README.md", - "data/share/man/man1/s3cmd.1", - ], - ) + s3cmd_bin = "bin/s3cmd" + if os.name == "nt": + s3cmd_bin += ".bat" + + expected = { + s3cmd_bin, + "data/share/doc/packages/s3cmd/INSTALL.md", + "data/share/doc/packages/s3cmd/LICENSE", + "data/share/doc/packages/s3cmd/NEWS", + "data/share/doc/packages/s3cmd/README.md", + "data/share/man/man1/s3cmd.1", + } + + self.assertEqual(actual, expected) def test_dist_info(self): actual = os.environ.get("WHEEL_DIST_INFO_CONTENTS") diff --git a/examples/pip_parse/requirements.in b/examples/pip_parse/requirements.in index 9d9e766d21..e4af3b1efe 100644 --- a/examples/pip_parse/requirements.in +++ b/examples/pip_parse/requirements.in @@ -3,3 +3,4 @@ s3cmd~=2.1.0 yamllint~=1.28.0 sphinx sphinxcontrib-serializinghtml +libclang diff --git a/examples/pip_parse/requirements_lock.txt b/examples/pip_parse/requirements_lock.txt index dc34b45a45..2b9d8fcd47 100644 --- a/examples/pip_parse/requirements_lock.txt +++ b/examples/pip_parse/requirements_lock.txt @@ -16,7 +16,7 @@ certifi==2025.4.26 \ --hash=sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6 \ --hash=sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3 # via - # -c ./constraints_certifi.txt + # -c constraints_certifi.txt # requests chardet==4.0.0 \ --hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa \ @@ -42,6 +42,18 @@ jinja2==3.1.6 \ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 # via sphinx +libclang==18.1.1 \ + --hash=sha256:0b2e143f0fac830156feb56f9231ff8338c20aecfe72b4ffe96f19e5a1dbb69a \ + --hash=sha256:3f0e1f49f04d3cd198985fea0511576b0aee16f9ff0e0f0cad7f9c57ec3c20e8 \ + --hash=sha256:4dd2d3b82fab35e2bf9ca717d7b63ac990a3519c7e312f19fa8e86dcc712f7fb \ + --hash=sha256:54dda940a4a0491a9d1532bf071ea3ef26e6dbaf03b5000ed94dd7174e8f9592 \ + --hash=sha256:69f8eb8f65c279e765ffd28aaa7e9e364c776c17618af8bff22a8df58677ff4f \ + --hash=sha256:6f14c3f194704e5d09769108f03185fce7acaf1d1ae4bbb2f30a72c2400cb7c5 \ + --hash=sha256:83ce5045d101b669ac38e6da8e58765f12da2d3aafb3b9b98d88b286a60964d8 \ + --hash=sha256:a1214966d08d73d971287fc3ead8dfaf82eb07fb197680d8b3859dbbbbf78250 \ + --hash=sha256:c533091d8a3bbf7460a00cb6c1a71da93bffe148f172c7d03b1c31fbf8aa2a0b \ + --hash=sha256:cf4a99b05376513717ab5d82a0db832c56ccea4fd61a69dbb7bccf2dfb207dbe + # via -r requirements.in markupsafe==2.1.3 \ --hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \ --hash=sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e \ @@ -224,7 +236,7 @@ urllib3==1.26.20 \ --hash=sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e \ --hash=sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32 # via - # -c ./constraints_urllib3.txt + # -c constraints_urllib3.txt # requests yamllint==1.28.0 \ --hash=sha256:89bb5b5ac33b1ade059743cf227de73daa34d5e5a474b06a5e17fc16583b0cf2 \ diff --git a/examples/pip_parse/requirements_windows.txt b/examples/pip_parse/requirements_windows.txt index 78c1a45690..c407d0c8cb 100644 --- a/examples/pip_parse/requirements_windows.txt +++ b/examples/pip_parse/requirements_windows.txt @@ -16,7 +16,7 @@ certifi==2025.4.26 \ --hash=sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6 \ --hash=sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3 # via - # -c ./constraints_certifi.txt + # -c constraints_certifi.txt # requests chardet==4.0.0 \ --hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa \ @@ -46,6 +46,18 @@ jinja2==3.1.6 \ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 # via sphinx +libclang==18.1.1 \ + --hash=sha256:0b2e143f0fac830156feb56f9231ff8338c20aecfe72b4ffe96f19e5a1dbb69a \ + --hash=sha256:3f0e1f49f04d3cd198985fea0511576b0aee16f9ff0e0f0cad7f9c57ec3c20e8 \ + --hash=sha256:4dd2d3b82fab35e2bf9ca717d7b63ac990a3519c7e312f19fa8e86dcc712f7fb \ + --hash=sha256:54dda940a4a0491a9d1532bf071ea3ef26e6dbaf03b5000ed94dd7174e8f9592 \ + --hash=sha256:69f8eb8f65c279e765ffd28aaa7e9e364c776c17618af8bff22a8df58677ff4f \ + --hash=sha256:6f14c3f194704e5d09769108f03185fce7acaf1d1ae4bbb2f30a72c2400cb7c5 \ + --hash=sha256:83ce5045d101b669ac38e6da8e58765f12da2d3aafb3b9b98d88b286a60964d8 \ + --hash=sha256:a1214966d08d73d971287fc3ead8dfaf82eb07fb197680d8b3859dbbbbf78250 \ + --hash=sha256:c533091d8a3bbf7460a00cb6c1a71da93bffe148f172c7d03b1c31fbf8aa2a0b \ + --hash=sha256:cf4a99b05376513717ab5d82a0db832c56ccea4fd61a69dbb7bccf2dfb207dbe + # via -r requirements.in markupsafe==2.1.3 \ --hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \ --hash=sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e \ @@ -228,7 +240,7 @@ urllib3==1.26.20 \ --hash=sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e \ --hash=sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32 # via - # -c ./constraints_urllib3.txt + # -c constraints_urllib3.txt # requests yamllint==1.28.0 \ --hash=sha256:89bb5b5ac33b1ade059743cf227de73daa34d5e5a474b06a5e17fc16583b0cf2 \ diff --git a/examples/pip_parse_vendored/.bazelversion b/examples/pip_parse_vendored/.bazelversion new file mode 100644 index 0000000000..35907cd9ca --- /dev/null +++ b/examples/pip_parse_vendored/.bazelversion @@ -0,0 +1 @@ +7.x diff --git a/examples/pip_parse_vendored/BUILD.bazel b/examples/pip_parse_vendored/BUILD.bazel index 8d81e4ba8b..74b9286359 100644 --- a/examples/pip_parse_vendored/BUILD.bazel +++ b/examples/pip_parse_vendored/BUILD.bazel @@ -3,6 +3,7 @@ load("@bazel_skylib//rules:diff_test.bzl", "diff_test") load("@bazel_skylib//rules:write_file.bzl", "write_file") load("@rules_python//python:pip.bzl", "compile_pip_requirements") load("@rules_python//python:py_test.bzl", "py_test") +load("@rules_shell//shell:sh_binary.bzl", "sh_binary") load("//:requirements.bzl", "all_data_requirements", "all_requirements", "all_whl_requirements", "requirement") # This rule adds a convenient way to update the requirements.txt diff --git a/examples/pip_parse_vendored/WORKSPACE b/examples/pip_parse_vendored/WORKSPACE index d7a11ea596..5e80b4116b 100644 --- a/examples/pip_parse_vendored/WORKSPACE +++ b/examples/pip_parse_vendored/WORKSPACE @@ -39,3 +39,19 @@ pip_parse( load("//:requirements.bzl", "install_deps") install_deps() + +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +# See https://github.com/bazelbuild/rules_shell/releases/tag/v0.2.0 +http_archive( + name = "rules_shell", + sha256 = "410e8ff32e018b9efd2743507e7595c26e2628567c42224411ff533b57d27c28", + strip_prefix = "rules_shell-0.2.0", + url = "https://github.com/bazelbuild/rules_shell/releases/download/v0.2.0/rules_shell-v0.2.0.tar.gz", +) + +load("@rules_shell//shell:repositories.bzl", "rules_shell_dependencies", "rules_shell_toolchains") + +rules_shell_dependencies() + +rules_shell_toolchains() diff --git a/examples/pip_parse_vendored/requirements.bzl b/examples/pip_parse_vendored/requirements.bzl index ead5c49b26..f5551573fb 100644 --- a/examples/pip_parse_vendored/requirements.bzl +++ b/examples/pip_parse_vendored/requirements.bzl @@ -4,7 +4,7 @@ """ load("@rules_python//python:pip.bzl", "pip_utils") -load("@rules_python//python/pip_install:pip_repository.bzl", "group_library", "whl_library") +load("@rules_python//python/pip_install:pip_repository.bzl", "whl_config_repo", "whl_library") all_requirements = [ "@my_project_pip_deps_vendored_certifi//:pkg", @@ -91,12 +91,17 @@ def install_deps(**whl_library_kwargs): for requirement in group_requirements } - group_repo = "my_project_pip_deps_vendored__groups" - group_library( - name = group_repo, + config_repo = "my_project_pip_deps_vendored__config" + whl_config_repo( + name = "my_project_pip_deps_vendored__config", repo_prefix = "my_project_pip_deps_vendored_", groups = all_requirement_groups, + whl_map = { + p: "" + for p in all_whl_requirements_by_package + }, ) + config_load = "@{}//:config.bzl".format(config_repo) # Install wheels which may be participants in a group whl_config = dict(_config) @@ -112,5 +117,6 @@ def install_deps(**whl_library_kwargs): group_name = group_name, group_deps = group_deps, annotation = _get_annotation(requirement), + config_load = config_load, **whl_config ) diff --git a/examples/pip_repository_annotations/.bazelrc b/examples/pip_repository_annotations/.bazelrc deleted file mode 100644 index 9397bd31b8..0000000000 --- a/examples/pip_repository_annotations/.bazelrc +++ /dev/null @@ -1,9 +0,0 @@ -# https://docs.bazel.build/versions/main/best-practices.html#using-the-bazelrc-file -try-import %workspace%/user.bazelrc - -# This example is WORKSPACE specific. The equivalent functionality -# is in examples/bzlmod as the `whl_mods` feature. -common --noenable_bzlmod -common --enable_workspace -common --legacy_external_runfiles=false -common --incompatible_python_disallow_native_rules diff --git a/examples/pip_repository_annotations/.gitignore b/examples/pip_repository_annotations/.gitignore deleted file mode 100644 index a6ef824c1f..0000000000 --- a/examples/pip_repository_annotations/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/bazel-* diff --git a/examples/pip_repository_annotations/BUILD.bazel b/examples/pip_repository_annotations/BUILD.bazel deleted file mode 100644 index 4e10c51658..0000000000 --- a/examples/pip_repository_annotations/BUILD.bazel +++ /dev/null @@ -1,28 +0,0 @@ -load("@rules_python//python:pip.bzl", "compile_pip_requirements") -load("@rules_python//python:py_test.bzl", "py_test") - -exports_files( - glob(["data/**"]), - visibility = ["//visibility:public"], -) - -# This rule adds a convenient way to update the requirements file. -compile_pip_requirements( - name = "requirements", - src = "requirements.in", -) - -py_test( - name = "pip_parse_annotations_test", - srcs = ["pip_repository_annotations_test.py"], - env = { - "REQUESTS_PKG_DIR": "pip_requests", - "WHEEL_PKG_DIR": "pip_wheel", - }, - main = "pip_repository_annotations_test.py", - deps = [ - "@pip_requests//:pkg", - "@pip_wheel//:pkg", - "@rules_python//python/runfiles", - ], -) diff --git a/examples/pip_repository_annotations/WORKSPACE b/examples/pip_repository_annotations/WORKSPACE deleted file mode 100644 index 8540555084..0000000000 --- a/examples/pip_repository_annotations/WORKSPACE +++ /dev/null @@ -1,62 +0,0 @@ -workspace(name = "pip_repository_annotations_example") - -local_repository( - name = "rules_python", - path = "../..", -) - -load("@rules_python//python:repositories.bzl", "py_repositories", "python_register_toolchains") - -py_repositories() - -python_register_toolchains( - name = "python39", - python_version = "3.9", -) - -load("@rules_python//python:pip.bzl", "package_annotation", "pip_parse") - -# Here we can see an example of annotations being applied to an arbitrary -# package. For details on `package_annotation` and it's uses, see the -# docs at @rules_python//docs:pip.md`. -ANNOTATIONS = { - # This annotation verifies that annotations work correctly for pip packages with extras - # specified, in this case requests[security]. - "requests": package_annotation( - additive_build_content = """\ -load("@bazel_skylib//rules:write_file.bzl", "write_file") -write_file( - name = "generated_file", - out = "generated_file.txt", - content = ["Hello world from requests"], -) -""", - data = [":generated_file"], - ), - "wheel": package_annotation( - additive_build_content = """\ -load("@bazel_skylib//rules:write_file.bzl", "write_file") -write_file( - name = "generated_file", - out = "generated_file.txt", - content = ["Hello world from build content file"], -) -""", - copy_executables = {"@pip_repository_annotations_example//:data/copy_executable.py": "copied_content/executable.py"}, - copy_files = {"@pip_repository_annotations_example//:data/copy_file.txt": "copied_content/file.txt"}, - data = [":generated_file"], - data_exclude_glob = ["site-packages/*.dist-info/WHEEL"], - ), -} - -# For a more thorough example of `pip_parse`. See `@rules_python//examples/pip_parse` -pip_parse( - name = "pip", - annotations = ANNOTATIONS, - python_interpreter_target = "@python39_host//:python", - requirements_lock = "//:requirements.txt", -) - -load("@pip//:requirements.bzl", "install_deps") - -install_deps() diff --git a/examples/pip_repository_annotations/data/copy_file.txt b/examples/pip_repository_annotations/data/copy_file.txt deleted file mode 100644 index b1020f7b95..0000000000 --- a/examples/pip_repository_annotations/data/copy_file.txt +++ /dev/null @@ -1 +0,0 @@ -Hello world from copied file diff --git a/examples/pip_repository_annotations/pip_repository_annotations_test.py b/examples/pip_repository_annotations/pip_repository_annotations_test.py deleted file mode 100644 index 219be1ba03..0000000000 --- a/examples/pip_repository_annotations/pip_repository_annotations_test.py +++ /dev/null @@ -1,118 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2023 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. - - -import os -import platform -import subprocess -import sys -import unittest -from pathlib import Path - -from python.runfiles import runfiles - - -class PipRepositoryAnnotationsTest(unittest.TestCase): - maxDiff = None - - def wheel_pkg_dir(self) -> str: - env = os.environ.get("WHEEL_PKG_DIR") - self.assertIsNotNone(env) - return env - - def test_build_content_and_data(self): - r = runfiles.Create() - rpath = r.Rlocation("{}/generated_file.txt".format(self.wheel_pkg_dir())) - generated_file = Path(rpath) - self.assertTrue(generated_file.exists()) - - content = generated_file.read_text().rstrip() - self.assertEqual(content, "Hello world from build content file") - - def test_copy_files(self): - r = runfiles.Create() - rpath = r.Rlocation("{}/copied_content/file.txt".format(self.wheel_pkg_dir())) - copied_file = Path(rpath) - self.assertTrue(copied_file.exists()) - - content = copied_file.read_text().rstrip() - self.assertEqual(content, "Hello world from copied file") - - def test_copy_executables(self): - r = runfiles.Create() - rpath = r.Rlocation( - "{}/copied_content/executable{}".format( - self.wheel_pkg_dir(), - ".exe" if platform.system() == "windows" else ".py", - ) - ) - executable = Path(rpath) - self.assertTrue(executable.exists()) - - proc = subprocess.run( - [sys.executable, str(executable)], - check=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - stdout = proc.stdout.decode("utf-8").strip() - self.assertEqual(stdout, "Hello world from copied executable") - - def test_data_exclude_glob(self): - current_wheel_version = "0.38.4" - - r = runfiles.Create() - dist_info_dir = "{}/site-packages/wheel-{}.dist-info".format( - self.wheel_pkg_dir(), - current_wheel_version, - ) - - # Note: `METADATA` is important as it's consumed by https://docs.python.org/3/library/importlib.metadata.html - # `METADATA` is expected to be there to show dist-info files are included in the runfiles. - metadata_path = r.Rlocation("{}/METADATA".format(dist_info_dir)) - - # However, `WHEEL` was explicitly excluded, so it should be missing - wheel_path = r.Rlocation("{}/WHEEL".format(dist_info_dir)) - - # Because windows does not have `--enable_runfiles` on by default, the - # `runfiles.Rlocation` results will be different on this platform vs - # unix platforms. See `@rules_python//python/runfiles` for more details. - if platform.system() == "Windows": - self.assertIsNotNone(metadata_path) - self.assertIsNone(wheel_path) - else: - self.assertTrue(Path(metadata_path).exists()) - self.assertFalse(Path(wheel_path).exists()) - - def requests_pkg_dir(self) -> str: - env = os.environ.get("REQUESTS_PKG_DIR") - self.assertIsNotNone(env) - return env - - def test_extra(self): - # This test verifies that annotations work correctly for pip packages with extras - # specified, in this case requests[security]. - r = runfiles.Create() - path = "{}/generated_file.txt".format(self.requests_pkg_dir()) - rpath = r.Rlocation(path) - generated_file = Path(rpath) - self.assertTrue(generated_file.exists()) - - content = generated_file.read_text().rstrip() - self.assertEqual(content, "Hello world from requests") - - -if __name__ == "__main__": - unittest.main() diff --git a/examples/pip_repository_annotations/requirements.in b/examples/pip_repository_annotations/requirements.in deleted file mode 100644 index c9afafc6f5..0000000000 --- a/examples/pip_repository_annotations/requirements.in +++ /dev/null @@ -1,7 +0,0 @@ -# This flag allows for regression testing requirements arguments in -# `pip_repository` rules. ---extra-index-url https://pypi.org/simple/ - -certifi>=2023.7.22 # https://security.snyk.io/vuln/SNYK-PYTHON-CERTIFI-5805047 -wheel -requests[security]>=2.8.1 diff --git a/examples/pip_repository_annotations/requirements.txt b/examples/pip_repository_annotations/requirements.txt deleted file mode 100644 index f1069a7452..0000000000 --- a/examples/pip_repository_annotations/requirements.txt +++ /dev/null @@ -1,34 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.9 -# by the following command: -# -# bazel run //:requirements.update -# ---extra-index-url https://pypi.org/simple/ - -certifi==2023.7.22 \ - --hash=sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082 \ - --hash=sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9 - # via - # -r requirements.in - # requests -charset-normalizer==2.1.1 \ - --hash=sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845 \ - --hash=sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f - # via requests -idna==3.7 \ - --hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \ - --hash=sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0 - # via requests -requests[security]==2.28.1 \ - --hash=sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983 \ - --hash=sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349 - # via -r requirements.in -urllib3==1.26.18 \ - --hash=sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07 \ - --hash=sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0 - # via requests -wheel==0.38.4 \ - --hash=sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac \ - --hash=sha256:b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8 - # via -r requirements.in diff --git a/examples/py_proto_library/.bazelrc b/examples/py_proto_library/.bazelrc deleted file mode 100644 index 2ed86f591e..0000000000 --- a/examples/py_proto_library/.bazelrc +++ /dev/null @@ -1,4 +0,0 @@ -# The equivalent bzlmod behavior is covered by examples/bzlmod/py_proto_library -common --noenable_bzlmod -common --enable_workspace -common --incompatible_python_disallow_native_rules diff --git a/examples/py_proto_library/.gitignore b/examples/py_proto_library/.gitignore deleted file mode 100644 index e5ae073b3c..0000000000 --- a/examples/py_proto_library/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# git ignore patterns - -/bazel-* -user.bazelrc diff --git a/examples/py_proto_library/BUILD.bazel b/examples/py_proto_library/BUILD.bazel deleted file mode 100644 index b57c528511..0000000000 --- a/examples/py_proto_library/BUILD.bazel +++ /dev/null @@ -1,18 +0,0 @@ -load("@rules_python//python:py_test.bzl", "py_test") - -py_test( - name = "pricetag_test", - srcs = ["test.py"], - main = "test.py", - deps = [ - "//example.com/proto:pricetag_py_pb2", - ], -) - -py_test( - name = "message_test", - srcs = ["message_test.py"], - deps = [ - "//example.com/another_proto:message_py_pb2", - ], -) diff --git a/examples/py_proto_library/WORKSPACE b/examples/py_proto_library/WORKSPACE deleted file mode 100644 index 9cda5b97f1..0000000000 --- a/examples/py_proto_library/WORKSPACE +++ /dev/null @@ -1,36 +0,0 @@ -# NB: short workspace name is required to workaround PATH length limitation, see -# https://github.com/bazelbuild/bazel/issues/18683#issuecomment-1843857373 -workspace(name = "p") - -# The following local_path_override is only needed to run this example as part of our CI. -local_repository( - name = "rules_python", - path = "../..", -) - -# When not using this example in the rules_python git repo you would load the python -# rules using http_archive(), as documented in the release notes. - -load("@rules_python//python:repositories.bzl", "py_repositories", "python_register_toolchains") - -# We install the rules_python dependencies using the function below. -py_repositories() - -python_register_toolchains( - name = "python39", - python_version = "3.9", -) - -# Then we need to setup dependencies in order to use py_proto_library -load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") - -http_archive( - name = "com_google_protobuf", - sha256 = "4fc5ff1b2c339fb86cd3a25f0b5311478ab081e65ad258c6789359cd84d421f8", - strip_prefix = "protobuf-26.1", - urls = ["https://github.com/protocolbuffers/protobuf/archive/v26.1.tar.gz"], -) - -load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps") - -protobuf_deps() diff --git a/examples/py_proto_library/example.com/another_proto/BUILD.bazel b/examples/py_proto_library/example.com/another_proto/BUILD.bazel deleted file mode 100644 index 55e83a209a..0000000000 --- a/examples/py_proto_library/example.com/another_proto/BUILD.bazel +++ /dev/null @@ -1,16 +0,0 @@ -load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") -load("@rules_python//python:proto.bzl", "py_proto_library") - -py_proto_library( - name = "message_py_pb2", - visibility = ["//visibility:public"], - deps = [":message_proto"], -) - -proto_library( - name = "message_proto", - srcs = ["message.proto"], - # https://bazel.build/reference/be/protocol-buffer#proto_library.strip_import_prefix - strip_import_prefix = "/example.com", - deps = ["//example.com/proto:pricetag_proto"], -) diff --git a/examples/py_proto_library/example.com/another_proto/message.proto b/examples/py_proto_library/example.com/another_proto/message.proto deleted file mode 100644 index 6e7dcc5793..0000000000 --- a/examples/py_proto_library/example.com/another_proto/message.proto +++ /dev/null @@ -1,10 +0,0 @@ -syntax = "proto3"; - -package rules_python; - -import "proto/pricetag.proto"; - -message TestMessage { - uint32 index = 1; - PriceTag pricetag = 2; -} diff --git a/examples/py_proto_library/example.com/proto/BUILD.bazel b/examples/py_proto_library/example.com/proto/BUILD.bazel deleted file mode 100644 index fdf2e6fe32..0000000000 --- a/examples/py_proto_library/example.com/proto/BUILD.bazel +++ /dev/null @@ -1,17 +0,0 @@ -load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") -load("@rules_python//python:proto.bzl", "py_proto_library") - -py_proto_library( - name = "pricetag_py_pb2", - visibility = ["//visibility:public"], - deps = [":pricetag_proto"], -) - -proto_library( - name = "pricetag_proto", - srcs = ["pricetag.proto"], - # https://bazel.build/reference/be/protocol-buffer#proto_library.strip_import_prefix - strip_import_prefix = "/example.com", - visibility = ["//visibility:public"], - deps = ["@com_google_protobuf//:any_proto"], -) diff --git a/examples/py_proto_library/example.com/proto/pricetag.proto b/examples/py_proto_library/example.com/proto/pricetag.proto deleted file mode 100644 index 3fa68de84b..0000000000 --- a/examples/py_proto_library/example.com/proto/pricetag.proto +++ /dev/null @@ -1,11 +0,0 @@ -syntax = "proto3"; - -import "google/protobuf/any.proto"; - -package rules_python; - -message PriceTag { - string name = 2; - double cost = 1; - google.protobuf.Any metadata = 3; -} diff --git a/examples/py_proto_library/message_test.py b/examples/py_proto_library/message_test.py deleted file mode 100644 index b1a6942a54..0000000000 --- a/examples/py_proto_library/message_test.py +++ /dev/null @@ -1,16 +0,0 @@ -import sys -import unittest - -from another_proto import message_pb2 - - -class TestCase(unittest.TestCase): - def test_message(self): - got = message_pb2.TestMessage( - index=5, - ) - self.assertIsNotNone(got) - - -if __name__ == "__main__": - sys.exit(unittest.main()) diff --git a/examples/py_proto_library/test.py b/examples/py_proto_library/test.py deleted file mode 100644 index 24ab8ddc70..0000000000 --- a/examples/py_proto_library/test.py +++ /dev/null @@ -1,21 +0,0 @@ -import json -import unittest - -from proto import pricetag_pb2 - - -class TestCase(unittest.TestCase): - def test_pricetag(self): - got = pricetag_pb2.PriceTag( - name="dollar", - cost=5.00, - ) - - metadata = {"description": "some text..."} - got.metadata.value = json.dumps(metadata).encode("utf-8") - - self.assertIsNotNone(got) - - -if __name__ == "__main__": - unittest.main() diff --git a/examples/wheel/BUILD.bazel b/examples/wheel/BUILD.bazel index e52e0fc3a3..01dc4fab41 100644 --- a/examples/wheel/BUILD.bazel +++ b/examples/wheel/BUILD.bazel @@ -230,6 +230,25 @@ py_wheel( ], ) +# An example of how to change the wheel package root directory using 'add_path_prefix'. +py_wheel( + name = "custom_prefix_package_root", + add_path_prefix = "custom_prefix", + # Package data. We're building "examples_custom_prefix_package_root-0.0.1-py3-none-any.whl" + distribution = "examples_custom_prefix_package_root", + entry_points = { + "console_scripts": ["main = foo.bar:baz"], + }, + python_tag = "py3", + strip_path_prefixes = [ + "examples", + ], + version = "0.0.1", + deps = [ + ":example_pkg", + ], +) + py_wheel( name = "python_requires_in_a_package", distribution = "example_python_requires_in_a_package", @@ -401,6 +420,29 @@ py_wheel( version = "0.0.1", ) +filegroup( + name = "data_files_test_group", + # Re-using some files already checked into the repo. + srcs = [ + "README.md", + "//examples/wheel:NOTICE", + ], +) + +py_wheel( + name = "data_files_installed_in_folder", + testonly = True, # Set this to verify the generated .dist target doesn't break things + # Re-using some files already checked into the repo. + data_files = { + # Single file + "//examples/wheel:NOTICE": "scripts/", + # Filegroup + ":data_files_test_group": "data/", + }, + distribution = "data_files_installed_in_folder", + version = "0.0.1", +) + py_test( name = "wheel_test", srcs = ["wheel_test.py"], @@ -409,6 +451,7 @@ py_test( ":custom_package_root_multi_prefix", ":custom_package_root_multi_prefix_reverse_order", ":customized", + ":data_files_installed_in_folder", ":empty_requires_files", ":extra_requires", ":filename_escaping", diff --git a/examples/wheel/private/wheel_utils.bzl b/examples/wheel/private/wheel_utils.bzl index 037fed0175..4cd776a98a 100644 --- a/examples/wheel/private/wheel_utils.bzl +++ b/examples/wheel/private/wheel_utils.bzl @@ -28,6 +28,7 @@ def _directory_writer_impl(ctx): ctx.actions.run( outputs = [output], + mnemonic = "PyDirWriter", arguments = [args], executable = ctx.executable._writer, ) diff --git a/examples/wheel/requirements_server.in b/examples/wheel/requirements_server.in index d5d483d56a..631bdb8fb5 100644 --- a/examples/wheel/requirements_server.in +++ b/examples/wheel/requirements_server.in @@ -1,2 +1,2 @@ # This is for running publishing tests -pypiserver +pypiserver>=2.2.0 diff --git a/examples/wheel/requirements_server.txt b/examples/wheel/requirements_server.txt index eccab1271b..7324131a7c 100644 --- a/examples/wheel/requirements_server.txt +++ b/examples/wheel/requirements_server.txt @@ -4,9 +4,17 @@ # # bazel run //examples/wheel:requirements_server.update # -pypiserver==2.0.1 \ - --hash=sha256:1dd98fb99d2da4199fb44c7284e57d69a9f7fda2c6c8dc01975c151c592677bf \ - --hash=sha256:7b58fbd54468235f79e4de07c4f7a9ff829e7ac6869bef47ec11e0710138e162 +importlib-resources==7.1.0 \ + --hash=sha256:0722d4c6212489c530f2a145a34c0a7a3b4721bc96a15fada5930e2a0b760708 \ + --hash=sha256:1bd7b48b4088eddb2cd16382150bb515af0bd2c70128194392725f82ad2c96a1 + # via pypiserver +packaging==26.1 \ + --hash=sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f \ + --hash=sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de + # via pypiserver +pypiserver==2.4.1 \ + --hash=sha256:156540f87ecfd6db06ae2c16e25ae5afe4fda6f510bd1c34e46fbb0c491bcd9e \ + --hash=sha256:45f116d0bff6aafcaed002cfad48a6832e62a82393e3a9b447d5c41a0e310fff # via -r examples/wheel/requirements_server.in # The following packages are considered to be unsafe in a requirements file: diff --git a/examples/wheel/test_publish.py b/examples/wheel/test_publish.py index 7665629c19..4bc657c52e 100644 --- a/examples/wheel/test_publish.py +++ b/examples/wheel/test_publish.py @@ -6,6 +6,7 @@ import unittest from contextlib import closing from pathlib import Path +from urllib.error import URLError from urllib.request import urlopen @@ -50,17 +51,16 @@ def setUp(self): ], ) - line = "Hit Ctrl-C to quit" interval = 0.1 wait_seconds = 40 for _ in range(int(wait_seconds / interval)): # 40 second timeout - current_logs = self.log_file.read_text() - if line in current_logs: - print(current_logs.strip()) - print("...") - break - - time.sleep(0.1) + try: + with urlopen(self.url, timeout=1) as response: + if response.status == 200: + break + except (URLError, OSError): + pass + time.sleep(interval) else: raise RuntimeError( f"Could not get the server running fast enough, waited for {wait_seconds}s" @@ -98,13 +98,15 @@ def test_upload_and_query_simple_api(self): got_content = response.read().decode("utf-8") want_content = """ - + + + Links for example-minimal-library

Links for example-minimal-library

- example_minimal_library-0.0.1-py3-none-any.whl
+ example_minimal_library-0.0.1-py3-none-any.whl
""" self.assertEqual( diff --git a/examples/wheel/wheel_test.py b/examples/wheel/wheel_test.py index 7f19ecd9f9..8dcad42138 100644 --- a/examples/wheel/wheel_test.py +++ b/examples/wheel/wheel_test.py @@ -523,7 +523,7 @@ def test_minimal_data_files(self): with zipfile.ZipFile(filename) as zf: self.assertAllEntriesHasReproducibleMetadata(zf) - metadata_file = None + metadata_file = None # noqa: F841 self.assertEqual( zf.namelist(), [ @@ -566,7 +566,9 @@ def test_extra_requires(self): ) def test_requires_dist_depends_on_extras(self): - filename = self._get_path("requires_dist_depends_on_extras-0.0.1-py3-none-any.whl") + filename = self._get_path( + "requires_dist_depends_on_extras-0.0.1-py3-none-any.whl" + ) with zipfile.ZipFile(filename) as zf: self.assertAllEntriesHasReproducibleMetadata(zf) @@ -591,7 +593,9 @@ def test_requires_dist_depends_on_extras(self): ) def test_requires_dist_depends_on_extras_file(self): - filename = self._get_path("requires_dist_depends_on_extras_using_file-0.0.1-py3-none-any.whl") + filename = self._get_path( + "requires_dist_depends_on_extras_using_file-0.0.1-py3-none-any.whl" + ) with zipfile.ZipFile(filename) as zf: self.assertAllEntriesHasReproducibleMetadata(zf) @@ -615,6 +619,25 @@ def test_requires_dist_depends_on_extras_file(self): requires, ) + def test_data_files_installed_in_folder(self): + filename = self._get_path( + "data_files_installed_in_folder-0.0.1-py3-none-any.whl" + ) + + with zipfile.ZipFile(filename) as zf: + self.assertAllEntriesHasReproducibleMetadata(zf) + self.assertEqual( + zf.namelist(), + [ + "data_files_installed_in_folder-0.0.1.dist-info/WHEEL", + "data_files_installed_in_folder-0.0.1.dist-info/METADATA", + "data_files_installed_in_folder-0.0.1.data/data/NOTICE", + "data_files_installed_in_folder-0.0.1.data/data/README.md", + "data_files_installed_in_folder-0.0.1.data/scripts/NOTICE", + "data_files_installed_in_folder-0.0.1.dist-info/RECORD", + ], + ) + if __name__ == "__main__": unittest.main() diff --git a/gazelle/.bazelignore b/gazelle/.bazelignore new file mode 100644 index 0000000000..5930a06190 --- /dev/null +++ b/gazelle/.bazelignore @@ -0,0 +1,8 @@ +bazel-bin +bazel-gazelle +bazel-out +bazel-testlogs +examples/bzlmod_build_file_generation/bazel-bin +examples/bzlmod_build_file_generation/bazel-bzlmod_build_file_generation +examples/bzlmod_build_file_generation/bazel-out +examples/bzlmod_build_file_generation/bazel-testlog diff --git a/gazelle/.bazelrc b/gazelle/.bazelrc index 97040903a6..e30216814a 100644 --- a/gazelle/.bazelrc +++ b/gazelle/.bazelrc @@ -1,3 +1,10 @@ +common --deleted_packages=examples/bzlmod_build_file_generation +common --deleted_packages=examples/bzlmod_build_file_generation/runfiles +common --experimental_downloader_config=downloader_config.cfg +common --http_timeout_scaling=10.0 +common --experimental_repository_downloader_retries=10 + + test --test_output=errors # Do NOT implicitly create empty __init__.py files in the runfiles tree. @@ -7,6 +14,7 @@ test --test_output=errors # creating (possibly empty) __init__.py files and adding them to the srcs of # Python targets as required. build --incompatible_default_to_explicit_init_py +build --@rules_python//python/config_settings:incompatible_default_to_explicit_init_py=True # Windows makes use of runfiles for some rules build --enable_runfiles diff --git a/gazelle/BUILD.bazel b/gazelle/BUILD.bazel index 0938be3dfc..30758181ed 100644 --- a/gazelle/BUILD.bazel +++ b/gazelle/BUILD.bazel @@ -1,9 +1,12 @@ load("@bazel_gazelle//:def.bzl", "gazelle") +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") # Gazelle configuration options. # See https://github.com/bazelbuild/bazel-gazelle#running-gazelle-with-bazel # gazelle:prefix github.com/bazel-contrib/rules_python/gazelle # gazelle:exclude bazel-out +# gazelle:exclude deps.bzl +# gazelle:exclude internal_dev_deps.bzl gazelle( name = "gazelle", ) @@ -36,3 +39,9 @@ filegroup( ], visibility = ["@rules_python//:__pkg__"], ) + +bzl_library( + name = "def", + srcs = ["def.bzl"], + visibility = ["//visibility:public"], +) diff --git a/gazelle/MODULE.bazel b/gazelle/MODULE.bazel index 1560e73d7b..cff6341a2b 100644 --- a/gazelle/MODULE.bazel +++ b/gazelle/MODULE.bazel @@ -4,10 +4,10 @@ module( compatibility_level = 1, ) -bazel_dep(name = "bazel_skylib", version = "1.6.1") +bazel_dep(name = "bazel_skylib", version = "1.8.2") bazel_dep(name = "rules_python", version = "0.18.0") -bazel_dep(name = "rules_go", version = "0.55.1", repo_name = "io_bazel_rules_go") -bazel_dep(name = "gazelle", version = "0.36.0", repo_name = "bazel_gazelle") +bazel_dep(name = "rules_go", version = "0.59.0", repo_name = "io_bazel_rules_go") +bazel_dep(name = "gazelle", version = "0.47.0", repo_name = "bazel_gazelle") bazel_dep(name = "rules_cc", version = "0.0.16") local_path_override( diff --git a/gazelle/WORKSPACE b/gazelle/WORKSPACE index ec0532c3f6..ebb18e7a47 100644 --- a/gazelle/WORKSPACE +++ b/gazelle/WORKSPACE @@ -2,6 +2,19 @@ workspace(name = "rules_python_gazelle_plugin") load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") +http_archive( + name = "bazel_skylib", + sha256 = "6e78f0e57de26801f6f564fa7c4a48dc8b36873e416257a92bbb0937eeac8446", + urls = [ + "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.8.2/bazel-skylib-1.8.2.tar.gz", + "https://github.com/bazelbuild/bazel-skylib/releases/download/1.8.2/bazel-skylib-1.8.2.tar.gz", + ], +) + +load("@bazel_skylib//:workspace.bzl", "bazel_skylib_workspace") + +bazel_skylib_workspace() + http_archive( name = "io_bazel_rules_go", sha256 = "9d72f7b8904128afb98d46bbef82ad7223ec9ff3718d419afb355fddd9f9484a", @@ -13,10 +26,10 @@ http_archive( http_archive( name = "bazel_gazelle", - sha256 = "75df288c4b31c81eb50f51e2e14f4763cb7548daae126817247064637fd9ea62", + sha256 = "675114d8b433d0a9f54d81171833be96ebc4113115664b791e6f204d58e93446", urls = [ - "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.36.0/bazel-gazelle-v0.36.0.tar.gz", - "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.36.0/bazel-gazelle-v0.36.0.tar.gz", + "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.47.0/bazel-gazelle-v0.47.0.tar.gz", + "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.47.0/bazel-gazelle-v0.47.0.tar.gz", ], ) @@ -25,7 +38,7 @@ load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_depe go_rules_dependencies() -go_register_toolchains(version = "1.21.13") +go_register_toolchains(version = "1.22.9") gazelle_dependencies() diff --git a/gazelle/deps.bzl b/gazelle/deps.bzl index a7b5990c3b..7072c6a372 100644 --- a/gazelle/deps.bzl +++ b/gazelle/deps.bzl @@ -37,24 +37,18 @@ def gazelle_deps(): def go_deps(): "Fetch go dependencies" - go_repository( - name = "co_honnef_go_tools", - importpath = "honnef.co/go/tools", - sum = "h1:/hemPrYIhOhy8zYrNj+069zDB68us2sMGsfkFJO0iZs=", - version = "v0.0.0-20190523083050-ea95bdfd59fc", - ) go_repository( name = "com_github_bazelbuild_bazel_gazelle", importpath = "github.com/bazelbuild/bazel-gazelle", - sum = "h1:n41ODckCkU9D2BEwBxYN+xu5E92Vd0gaW6QmsIW9l00=", - version = "v0.36.0", + sum = "h1:g3Rr1ZbkC1Pk20aOgBITxSD/efS1WbaSty5jC786Z3Q=", + version = "v0.47.0", ) go_repository( name = "com_github_bazelbuild_buildtools", build_naming_convention = "go_default_library", importpath = "github.com/bazelbuild/buildtools", - sum = "h1:VNqmvOfFzn2Hrtoni8vqgXlIQ4C2Zt22fxeZ9gOOkp0=", - version = "v0.0.0-20240313121412-66c605173954", + sum = "h1:njQAmjTv/YHRm/0Lfv9DXHFZ4MdT2IA/RKHTnqZkgDw=", + version = "v0.0.0-20250930140053-2eb4fccefb52", ) go_repository( name = "com_github_bazelbuild_rules_go", @@ -65,44 +59,8 @@ def go_deps(): go_repository( name = "com_github_bmatcuk_doublestar_v4", importpath = "github.com/bmatcuk/doublestar/v4", - sum = "h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0CXv75Q=", - version = "v4.7.1", - ) - go_repository( - name = "com_github_burntsushi_toml", - importpath = "github.com/BurntSushi/toml", - sum = "h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=", - version = "v0.3.1", - ) - go_repository( - name = "com_github_census_instrumentation_opencensus_proto", - importpath = "github.com/census-instrumentation/opencensus-proto", - sum = "h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=", - version = "v0.2.1", - ) - go_repository( - name = "com_github_chzyer_logex", - importpath = "github.com/chzyer/logex", - sum = "h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=", - version = "v1.1.10", - ) - go_repository( - name = "com_github_chzyer_readline", - importpath = "github.com/chzyer/readline", - sum = "h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=", - version = "v0.0.0-20180603132655-2972be24d48e", - ) - go_repository( - name = "com_github_chzyer_test", - importpath = "github.com/chzyer/test", - sum = "h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=", - version = "v0.0.0-20180213035817-a1ea475d72b1", - ) - go_repository( - name = "com_github_client9_misspell", - importpath = "github.com/client9/misspell", - sum = "h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=", - version = "v0.3.4", + sum = "h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=", + version = "v4.9.1", ) go_repository( name = "com_github_davecgh_go_spew", @@ -116,18 +74,6 @@ def go_deps(): sum = "h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=", version = "v1.18.1", ) - go_repository( - name = "com_github_envoyproxy_go_control_plane", - importpath = "github.com/envoyproxy/go-control-plane", - sum = "h1:4cmBvAEBNJaGARUEs3/suWRyfyBfhf7I60WBZq+bv2w=", - version = "v0.9.1-0.20191026205805-5f8ba28d4473", - ) - go_repository( - name = "com_github_envoyproxy_protoc_gen_validate", - importpath = "github.com/envoyproxy/protoc-gen-validate", - sum = "h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A=", - version = "v0.1.0", - ) go_repository( name = "com_github_fsnotify_fsnotify", importpath = "github.com/fsnotify/fsnotify", @@ -146,12 +92,6 @@ def go_deps(): sum = "h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=", version = "v1.3.2", ) - go_repository( - name = "com_github_golang_glog", - importpath = "github.com/golang/glog", - sum = "h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=", - version = "v0.0.0-20160126235308-23def4e6c14b", - ) go_repository( name = "com_github_golang_mock", importpath = "github.com/golang/mock", @@ -176,12 +116,6 @@ def go_deps(): sum = "h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=", version = "v1.0.0", ) - go_repository( - name = "com_github_prometheus_client_model", - importpath = "github.com/prometheus/client_model", - sum = "h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=", - version = "v0.0.0-20190812154241-14fe0d1b01d4", - ) go_repository( name = "com_github_smacker_go_tree_sitter", importpath = "github.com/smacker/go-tree-sitter", @@ -200,12 +134,6 @@ def go_deps(): sum = "h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=", version = "v1.9.0", ) - go_repository( - name = "com_google_cloud_go", - importpath = "cloud.google.com/go", - sum = "h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ=", - version = "v0.26.0", - ) go_repository( name = "in_gopkg_check_v1", importpath = "gopkg.in/check.v1", @@ -230,12 +158,6 @@ def go_deps(): sum = "h1:xwwDQW5We85NaTk2APgoN9202w/l0DVGp+GZMfsrh7s=", version = "v0.0.0-20210223155950-e043a3d3c984", ) - go_repository( - name = "org_golang_google_appengine", - importpath = "google.golang.org/appengine", - sum = "h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=", - version = "v1.4.0", - ) go_repository( name = "org_golang_google_genproto", importpath = "google.golang.org/genproto", @@ -266,24 +188,6 @@ def go_deps(): sum = "h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=", version = "v1.36.3", ) - go_repository( - name = "org_golang_x_crypto", - importpath = "golang.org/x/crypto", - sum = "h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=", - version = "v0.0.0-20190308221718-c2843e01d9a2", - ) - go_repository( - name = "org_golang_x_exp", - importpath = "golang.org/x/exp", - sum = "h1:c2HOrn5iMezYjSlGPncknSEr/8x5LELb/ilJbXi9DEA=", - version = "v0.0.0-20190121172915-509febef88a4", - ) - go_repository( - name = "org_golang_x_lint", - importpath = "golang.org/x/lint", - sum = "h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0=", - version = "v0.0.0-20190313153728-d0100b6bd8b3", - ) go_repository( name = "org_golang_x_mod", importpath = "golang.org/x/mod", @@ -296,12 +200,6 @@ def go_deps(): sum = "h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=", version = "v0.35.0", ) - go_repository( - name = "org_golang_x_oauth2", - importpath = "golang.org/x/oauth2", - sum = "h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=", - version = "v0.0.0-20180821212333-d2e6202438be", - ) go_repository( name = "org_golang_x_sync", importpath = "golang.org/x/sync", @@ -335,9 +233,3 @@ def go_deps(): sum = "h1:cOIJqWBl99H1dH5LWizPa+0ImeeJq3t3cJjaeOWUAL4=", version = "v0.1.0-deprecated", ) - go_repository( - name = "org_golang_x_xerrors", - importpath = "golang.org/x/xerrors", - sum = "h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=", - version = "v0.0.0-20200804184101-5ec99f83aff1", - ) diff --git a/gazelle/docs/annotations.md b/gazelle/docs/annotations.md index 728027ffda..b3f06b9991 100644 --- a/gazelle/docs/annotations.md +++ b/gazelle/docs/annotations.md @@ -22,20 +22,20 @@ def bar(): # gazelle:annotation_name value The annotations are: {.glossary} -[`# gazelle:ignore imports`](#ignore) +[`# gazelle:ignore imports`](#annotation-ignore) : Tells Gazelle to ignore import statements. `imports` is a comma-separated list of imports to ignore. * Default: n/a * Allowed Values: A comma-separated string of python package names -[`# gazelle:include_dep targets`](#include-dep) +[`# gazelle:include_dep targets`](#annotation-include-dep) : Tells Gazelle to include a set of dependencies, even if they are not imported in a Python module. `targets` is a comma-separated list of target names to include as dependencies. * Default: n/a * Allowed Values: A comma-separated string of targets -[`# gazelle:include_pytest_conftest bool`](#include-pytest-conftest) +[`# gazelle:include_pytest_conftest bool`](#annotation-include-pytest-conftest) : Whether or not to include a sibling `:conftest` target in the `deps` of a {bzl:obj}`py_test` target. The default behaviour is to include `:conftest` (i.e.: `# gazelle:include_pytest_conftest true`). @@ -43,6 +43,7 @@ The annotations are: * Allowed Values: `true`, `false` +(annotation-ignore)= ## `ignore` This annotation accepts a comma-separated string of values. Values are names of @@ -75,6 +76,7 @@ deps = ["@pypi//numpy"], ``` +(annotation-include-dep)= ## `include_dep` This annotation accepts a comma-separated string of values. Values _must_ @@ -116,6 +118,7 @@ deps = [ ``` +(annotation-include-pytest-conftest)= ## `include_pytest_conftest` :::{versionadded} 1.6.0 diff --git a/gazelle/docs/directives.md b/gazelle/docs/directives.md index a553226a59..ce8c03b9cd 100644 --- a/gazelle/docs/directives.md +++ b/gazelle/docs/directives.md @@ -16,58 +16,58 @@ the Python-specific directives in use can be found in the The Python-specific directives are: {.glossary} -[`# gazelle:python_extension value`](#python-extension) +[`# gazelle:python_extension value`](#directive-python-extension) : Controls whether the Python extension is enabled or not. Sub-packages inherit this value. * Default: `enabled` * Allowed Values: `enabled`, `disabled` -[`# gazelle:python_root`](#python-root) +[`# gazelle:python_root`](#directive-python-root) : Sets a Bazel package as a Python root. This is used on monorepos with multiple Python projects that don't share the top-level of the workspace as the root. * Default: n/a * Allowed Values: None. This direcive does not consume values. -[`# gazelle:python_manifest_file_name value`](#python-manifest-file-name) +[`# gazelle:python_manifest_file_name value`](#directive-python-manifest-file-name) : Overrides the default manifest file name. * Default: `gazelle_python.yaml` * Allowed Values: A string -[`# gazelle:python_ignore_files value`](#python-ignore-files) +[`# gazelle:python_ignore_files value`](#directive-python-ignore-files) : Controls the files which are ignored from the generated targets. * Default: n/a * Allowed Values: A comma-separated list of strings. -[`# gazelle:python_ignore_dependencies value`](#python-ignore-dependencies) +[`# gazelle:python_ignore_dependencies value`](#directive-python-ignore-dependencies) : Controls the ignored dependencies from the generated targets. * Default: n/a * Allowed Values: A comma-separated list of strings. -[`# gazelle:python_validate_import_statements bool`](#python-validate-import-statements) +[`# gazelle:python_validate_import_statements bool`](#directive-python-validate-import-statements) : Controls whether the Python import statements should be validated. * Default: `true` * Allowed Values: `true`, `false` -[`# gazelle:python_generation_mode value`](#python-generation-mode) +[`# gazelle:python_generation_mode value`](#directive-python-generation-mode) : Controls the target generation mode. * Default: `package` * Allowed Values: `file`, `package`, `project` -[`# gazelle:python_generation_mode_per_file_include_init bool`](#python-generation-mode-per-file-include-init) +[`# gazelle:python_generation_mode_per_file_include_init bool`](#directive-python-generation-mode-per-file-include-init) : Controls whether `__init__.py` files are included as srcs in each generated target when target generation mode is "file". * Default: `false` * Allowed Values: `true`, `false` -[`# gazelle:python_generation_mode_per_package_require_test_entry_point bool`](python-generation-mode-per-package-require-test-entry-point) +[`# gazelle:python_generation_mode_per_package_require_test_entry_point bool`](#directive-python-generation-mode-per-package-require-test-entry-point) : Controls whether a file called `__test__.py` or a target called `__test__` is required to generate one test target per package in package mode. * Default: `true` * Allowed Values: `true`, `false` -[`# gazelle:python_library_naming_convention value`](#python-library-naming-convention) +[`# gazelle:python_library_naming_convention value`](#directive-python-library-naming-convention) : Controls the {bzl:obj}`py_library` naming convention. It interpolates `$package_name$` with the Bazel package name. E.g. if the Bazel package name is `foo`, setting this to `$package_name$_my_lib` would result in a @@ -75,27 +75,27 @@ The Python-specific directives are: * Default: `$package_name$` * Allowed Values: A string containing `"$package_name$"` -[`# gazelle:python_binary_naming_convention value`](#python-binary-naming-convention) +[`# gazelle:python_binary_naming_convention value`](#directive-python-binary-naming-convention) : Controls the {bzl:obj}`py_binary` naming convention. Follows the same interpolation rules as `python_library_naming_convention`. * Default: `$package_name$_bin` * Allowed Values: A string containing `"$package_name$"` -[`# gazelle:python_test_naming_convention value`](#python-test-naming-convention) +[`# gazelle:python_test_naming_convention value`](#directive-python-test-naming-convention) : Controls the {bzl:obj}`py_test` naming convention. Follows the same interpolation rules as `python_library_naming_convention`. * Default: `$package_name$_test` * Allowed Values: A string containing `"$package_name$"` -[`# gazelle:python_proto_naming_convention value`](#python-proto-naming-convention) +[`# gazelle:python_proto_naming_convention value`](#directive-python-proto-naming-convention) : Controls the {bzl:obj}`py_proto_library` naming convention. It interpolates - `$proto_name$` with the {bzl:obj}`proto_library` rule name, minus any trailing - `_proto`. E.g. if the {bzl:obj}`proto_library` name is `foo_proto`, setting this + `$proto_name$` with the `proto_library` rule name, minus any trailing + `_proto`. E.g. if the `proto_library` name is `foo_proto`, setting this to `$proto_name$_my_lib` would render to `foo_my_lib`. * Default: `$proto_name$_py_pb2` * Allowed Values: A string containing `"$proto_name$"` -[`# gazelle:resolve py import-lang import-string label`](#resolve-py) +[`# gazelle:resolve py import-lang import-string label`](#directive-resolve-py) : Instructs the plugin what target to add as a dependency to satisfy a given import statement. The syntax is `# gazelle:resolve py import-string label` where `import-string` is the symbol in the python `import` statement, @@ -103,25 +103,25 @@ The Python-specific directives are: * Default: n/a * Allowed Values: See the [bazel-gazelle docs][gazelle-directives] -[`# gazelle:python_default_visibility labels`](python-default-visibility) +[`# gazelle:python_default_visibility labels`](#directive-python-default-visibility) : Instructs gazelle to use these visibility labels on all python targets. `labels` is a comma-separated list of labels (without spaces). * Default: `//$python_root$:__subpackages__` * Allowed Values: A string -[`# gazelle:python_visibility label`](python-visibility) +[`# gazelle:python_visibility label`](#directive-python-visibility) : Appends additional visibility labels to each generated target. This r directive can be set multiple times. * Default: n/a * Allowed Values: A string -[`# gazelle:python_test_file_pattern value`](python-test-file-pattern) +[`# gazelle:python_test_file_pattern value`](#directive-python-test-file-pattern) : Filenames matching these comma-separated {command}`glob`s will be mapped to {bzl:obj}`py_test` targets. * Default: `*_test.py,test_*.py` * Allowed Values: A glob string -[`# gazelle:python_label_convention value`](#python-label-convention) +[`# gazelle:python_label_convention value`](#directive-python-label-convention) : Defines the format of the distribution name in labels to third-party deps. Useful for using Gazelle plugin with other rules with different repository conventions (e.g. `rules_pycross`). Full label is always prepended with @@ -131,45 +131,57 @@ The Python-specific directives are: * Default: `$distribution_name$` * Allowed Values: A string -[`# gazelle:python_label_normalization value`](#python-label-normalization) +[`# gazelle:python_label_normalization value`](#directive-python-label-normalization) : Controls how distribution names in labels to third-party deps are normalized. Useful for using Gazelle plugin with other rules with different label conventions (e.g. `rules_pycross` uses PEP-503). * Default: `snake_case` * Allowed Values: `snake_case`, `none`, `pep503` -[`# gazelle:python_experimental_allow_relative_imports bool`](#python-experimental-allow-relative-imports) +[`# gazelle:python_experimental_allow_relative_imports bool`](#directive-python-experimental-allow-relative-imports) : Controls whether Gazelle resolves dependencies for import statements that use paths relative to the current package. * Default: `false` * Allowed Values: `true`, `false` -[`# gazelle:python_generate_pyi_deps bool`](#python-generate-pyi-deps) +[`# gazelle:python_generate_pyi_deps bool`](#directive-python-generate-pyi-deps) : Controls whether to generate a separate `pyi_deps` attribute for type-checking dependencies or merge them into the regular `deps` - attribute. When `false` (default), type-checking dependencies are - merged into `deps` for backward compatibility. When `true`, generates - separate `pyi_deps`. Imports in blocks with the format + attribute. When `true` (default), generates separate `pyi_deps`. When + `false`, type-checking dependencies are merged into `deps`. Imports in + blocks with the format `if typing.TYPE_CHECKING:` or `if TYPE_CHECKING:` and type-only stub packages (eg. boto3-stubs) are recognized as type-checking dependencies. - * Default: `false` + * Default: `true` + * Allowed Values: `true`, `false` + +[`# gazelle:python_generate_pyi_srcs bool`](#directive-python-generate-pyi-srcs) +: Controls whether to generate a `pyi_srcs` attribute if a sibling `.pyi` file + is found. When `false`, the `pyi_srcs` attribute is not added. + * Default: `true` * Allowed Values: `true`, `false` -[`# gazelle:python_generate_proto bool`](#python-generate-proto) +[`# gazelle:python_generate_proto bool`](#directive-python-generate-proto) : Controls whether to generate a {bzl:obj}`py_proto_library` for each - {bzl:obj}`proto_library` in the package. By default we load this rule from the + `proto_library` in the package. By default we load this rule from the `@protobuf` repository; use `gazelle:map_kind` if you need to load this from somewhere else. * Default: `false` * Allowed Values: `true`, `false` -[`# gazelle:python_resolve_sibling_imports bool`](#python-resolve-sibling-imports) +[`# gazelle:python_resolve_sibling_imports bool`](#directive-python-resolve-sibling-imports) : Allows absolute imports to be resolved to sibling modules (Python 2's behavior without `absolute_import`). * Default: `false` * Allowed Values: `true`, `false` +[`# gazelle:python_include_ancestor_conftest bool`](#directive-python-include-ancestor-conftest) +: Controls whether ancestor conftest targets are added to {bzl:obj}`py_test` target + dependencies. + * Default: `true` + * Allowed Values: `true`, `false` +(directive-python-extension)= ## `python_extension` :::{error} @@ -177,6 +189,7 @@ Detailed docs are not yet written. ::: +(directive-python-root)= ## `python_root` Set this directive within the Bazel package that you want to use as the Python root. @@ -213,6 +226,7 @@ py_libary( [python-packaging-user-guide]: https://github.com/pypa/packaging.python.org/blob/4c86169a/source/tutorials/packaging-projects.rst +(directive-python-manifest-file-name)= ## `python_manifest_file_name` :::{error} @@ -220,6 +234,7 @@ Detailed docs are not yet written. ::: +(directive-python-ignore-files)= ## `python_ignore_files` :::{error} @@ -227,6 +242,7 @@ Detailed docs are not yet written. ::: +(directive-python-ignore-dependencies)= ## `python_ignore_dependencies` :::{error} @@ -234,6 +250,7 @@ Detailed docs are not yet written. ::: +(directive-python-validate-import-statements)= ## `python_validate_import_statements` :::{error} @@ -241,6 +258,7 @@ Detailed docs are not yet written. ::: +(directive-python-generation-mode)= ## `python_generation_mode` :::{error} @@ -248,6 +266,7 @@ Detailed docs are not yet written. ::: +(directive-python-generation-mode-per-file-include-init)= ## `python_generation_mode_per_file_include_init` :::{error} @@ -255,6 +274,7 @@ Detailed docs are not yet written. ::: +(directive-python-generation-mode-per-package-require-test-entry-point)= ## `python_generation_mode_per_package_require_test_entry_point` When `# gazelle:python_generation_mode package`, whether a file called @@ -296,6 +316,7 @@ def py_test(name, main=None, **kwargs): ``` +(directive-python-library-naming-convention)= ## `python_library_naming_convention` :::{error} @@ -303,6 +324,7 @@ Detailed docs are not yet written. ::: +(directive-python-binary-naming-convention)= ## `python_binary_naming_convention` :::{error} @@ -310,6 +332,7 @@ Detailed docs are not yet written. ::: +(directive-python-test-naming-convention)= ## `python_test_naming_convention` :::{error} @@ -317,12 +340,17 @@ Detailed docs are not yet written. ::: +(directive-python-proto-naming-convention)= ## `python_proto_naming_convention` +:::{versionadded} 1.6.0 +{gh-pr}`3093` +::: + Set this directive to a string pattern to control how the generated {bzl:obj}`py_proto_library` targets are named. When generating new {bzl:obj}`py_proto_library` rules, Gazelle will replace `$proto_name$` in the -pattern with the name of the {bzl:obj}`proto_library` rule, stripping out a +pattern with the name of the `proto_library` rule, stripping out a trailing `_proto`. For example: ```starlark @@ -358,6 +386,7 @@ not able to map said imports, e.g. `import foo_pb2`, to fill in {gh-issue}`1703`. +(directive-resolve-py)= ## `resolve py` :::{error} @@ -365,8 +394,13 @@ Detailed docs are not yet written. ::: +(directive-python-default-visibility)= ## `python_default_visibility` +:::{versionadded} 0.32.0 +{gh-pr}`1787` +::: + Instructs gazelle to use these visibility labels on all _python_ targets (typically `py_*`, but can be modified via the `map_kind` directive). The arg to this directive is a comma-separated list (without spaces) of labels. @@ -440,8 +474,13 @@ py_library( These special values can be useful for sub-packages. +(directive-python-visibility)= ## `python_visibility` +:::{versionadded} 0.32.0 +{gh-pr}`1784` +::: + Appends additional `visibility` labels to each generated target. This directive can be set multiple times. The generated `visibility` attribute @@ -497,8 +536,13 @@ py_library( ``` +(directive-python-test-file-pattern)= ## `python_test_file_pattern` +:::{versionadded} 0.32.0 +{gh-pr}`1819` +::: + This directive adjusts which python files will be mapped to the {bzl:obj}`py_test` rule. + The default is `*_test.py,test_*.py`: both `test_*.py` and `*_test.py` files @@ -559,20 +603,31 @@ py_library( ``` +(directive-python-label-convention)= ## `python_label_convention` +:::{versionadded} 0.34.0 +{gh-pr}`1976` +::: + :::{error} Detailed docs are not yet written. ::: +(directive-python-label-normalization)= ## `python_label_normalization` +:::{versionadded} 0.34.0 +{gh-pr}`1976` +::: + :::{error} Detailed docs are not yet written. ::: +(directive-python-experimental-allow-relative-imports)= ## `python_experimental_allow_relative_imports` Enables experimental support for resolving relative imports in @@ -619,17 +674,90 @@ If the directive is set to `true`, gazelle will resolve imports that are relative to the current package. +(directive-python-generate-pyi-deps)= ## `python_generate_pyi_deps` -:::{error} -Detailed docs are not yet written. +:::{versionadded} 1.6.0 +{gh-pr}`3014` +::: + +:::{versionchanged} 2.1.0 +The default was changed from `false` to `true`. {gh-pr}`3753` +::: + +When `true`, Gazelle writes type-checking dependencies to the `pyi_deps` +attribute instead of merging them into `deps`. This is the default behavior. + +Gazelle treats imports inside `if TYPE_CHECKING:` and +`if typing.TYPE_CHECKING:` blocks as type-checking dependencies. It also adds +type stub packages, such as `boto3-stubs`, to `pyi_deps` when the corresponding +runtime package is imported normally. + +For example, assume you have the following file: + +```python +import boto3 +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import requests +``` + +The generated target will be: + +```starlark +py_library( + name = "foo", + srcs = ["foo.py"], + pyi_deps = ["@pip//requests"], + deps = ["@pip//boto3"], +) +``` + +When `false`, Gazelle merges type-checking dependencies into `deps` and does +not write `pyi_deps`. + + +(directive-python-generate-pyi-srcs)= +## `python_generate_pyi_srcs` + +:::{versionadded} 1.6.0 +{gh-pr}`3356` +::: + +:::{versionchanged} 2.1.0 +The default was changed from `false` to `true`. {gh-pr}`3753` ::: +When `true`, include any sibling `.pyi` files in the `pyi_srcs` target attribute. +For example, assume you have the following files: + +``` +foo.py +foo.pyi +``` + +The generated target will be: + +```starlark +py_library( + name = "foo", + srcs = ["foo.py"], + pyi_srcs = ["foo.pyi"], +) +``` + + +(directive-python-generate-proto)= ## `python_generate_proto` +:::{versionadded} 1.6.0 +{gh-pr}`3057` +::: + When `# gazelle:python_generate_proto true`, Gazelle will generate one -{bzl:obj}`py_proto_library` for each {bzl:obj}`proto_library`, generating Python clients for +{bzl:obj}`py_proto_library` for each `proto_library`, generating Python clients for protobuf in each package. By default this is turned off. Gazelle will also generate a load statement for the {bzl:obj}`py_proto_library` - attempting to detect the configured name for the `@protobuf` / `@com_google_protobuf` repo in your @@ -687,8 +815,82 @@ When `false`, Gazelle will ignore any {bzl:obj}`py_proto_library`, including previously-generated or hand-created rules. +(directive-python-resolve-sibling-imports)= ## `python_resolve_sibling_imports` +:::{versionadded} 1.6.0 +{gh-pr}`3106` +::: + :::{error} Detailed docs are not yet written. ::: + +(directive-python-include-ancestor-conftest)= +## `python_include_ancestor_conftest` + +:::{versionadded} 1.9.0 +{gh-pr}`3596` +::: + +Version 1.9.0 includes a fix ({gh-pr}`3498`) for a long-standing issue +({gh-issue}`3497`) where ancestor `conftest.py` files were not automatically +added as dependencies of {bzl:obj}`py_test` targets. + +However, some people may not want this behavior (see https://xkcd.com/1172/). +Thus the `python_include_ancestor_conftest` directive controls this behavior. +It defaults to `true`, which causes all ancestor `conftest.py` files to be +included as dependencies for {bzl:obj}`py_test` targets. + +Setting the directive to `false` reverts to the pre-1.9.0 behavior. + +For example, given this directory tree (not shown: intermediary `BUILD.bazel` +files) + +``` +./ +ā”œā”€ā”€ conftest.py +└── one/ + ā”œā”€ā”€ conftest.py + └── two/ + ā”œā”€ā”€ conftest.py + └── three/ + ā”œā”€ā”€ BUILD.bazel + ā”œā”€ā”€ conftest.py + └── my_test.py +``` + +Gazelle will generate this target for `foo_test.py` by default: + +```starlark +py_test( + name = "foo_test", + srcs = ["foo_test.py"], + deps = [ + ":conftest", # same as "//one:two/three:conftest" + "//:conftest", + "//one:conftest", + "//one/two:conftest", + ], +) +``` + +But when `python_include_ancestor_conftest` is `false`, only the sibling +`:conftest` target will be included as a dependency: + +:::{tip} +The [`include_pytest_conftest` annotation](annotation-include-pytest-conftest) +controls whether the sibling `:conftest` target is added to {bzl:obj}`py_test` +target dependency list. +::: + +```starlark +# gazelle:python_include_ancestor_conftest false +py_test( + name = "foo_test", + srcs = ["foo_test.py"], + deps = [ + ":conftest", + ], +) +``` diff --git a/gazelle/docs/installation_and_usage.md b/gazelle/docs/installation_and_usage.md index b151ade25e..c3a004310c 100644 --- a/gazelle/docs/installation_and_usage.md +++ b/gazelle/docs/installation_and_usage.md @@ -5,7 +5,7 @@ Examples of using Gazelle with Python can be found in the `rules_python` repo: -* bzlmod: {gh-path}`examples/bzlmod_build_file_generation` +* bzlmod: {gh-path}`gazelle/examples/bzlmod_build_file_generation` * WORKSPACE: {gh-path}`examples/build_file_generation` :::{note} diff --git a/gazelle/downloader_config.cfg b/gazelle/downloader_config.cfg new file mode 100644 index 0000000000..3fa6264eda --- /dev/null +++ b/gazelle/downloader_config.cfg @@ -0,0 +1,21 @@ +# Try GitHub first (primary) +rewrite ^github\.com/bazel-contrib/bazel_features/(.*) github.com/bazel-contrib/bazel_features/$1 +rewrite ^github\.com/bazel-contrib/rules_go/(.*) github.com/bazel-contrib/rules_go/$1 +rewrite ^github\.com/bazelbuild/bazel-skylib/(.*) github.com/bazelbuild/bazel-skylib/$1 +rewrite ^github\.com/bazelbuild/platforms/(.*) github.com/bazelbuild/platforms/$1 +rewrite ^github\.com/bazelbuild/rules_kotlin/(.*) github.com/bazelbuild/rules_kotlin/$1 +rewrite ^github\.com/bazelbuild/rules_shell/(.*) github.com/bazelbuild/rules_shell/$1 +rewrite ^github\.com/bazelbuild/rules_java/(.*) github.com/bazelbuild/rules_java/$1 +rewrite ^github\.com/bazelbuild/stardoc/(.*) github.com/bazelbuild/stardoc/$1 + + +# Fall back to mirror (secondary) +# Tracking upstream BCR mirror addition: https://github.com/bazelbuild/platforms/issues/139 +rewrite ^github\.com/bazel-contrib/bazel_features/(.*) mirror.bazel.build/github.com/bazel-contrib/bazel_features/$1 +rewrite ^github\.com/bazel-contrib/rules_go/(.*) mirror.bazel.build/github.com/bazel-contrib/rules_go/$1 +rewrite ^github\.com/bazelbuild/bazel-skylib/(.*) mirror.bazel.build/github.com/bazelbuild/bazel-skylib/$1 +rewrite ^github\.com/bazelbuild/platforms/(.*) mirror.bazel.build/github.com/bazelbuild/platforms/$1 +rewrite ^github\.com/bazelbuild/rules_kotlin/(.*) mirror.bazel.build/github.com/bazelbuild/rules_kotlin/$1 +rewrite ^github\.com/bazelbuild/rules_shell/(.*) mirror.bazel.build/github.com/bazelbuild/rules_shell/$1 +rewrite ^github\.com/bazelbuild/rules_java/(.*) mirror.bazel.build/github.com/bazelbuild/rules_java/$1 +rewrite ^github\.com/bazelbuild/stardoc/(.*) mirror.bazel.build/github.com/bazelbuild/stardoc/$1 diff --git a/examples/bzlmod_build_file_generation/.bazelignore b/gazelle/examples/bzlmod_build_file_generation/.bazelignore similarity index 100% rename from examples/bzlmod_build_file_generation/.bazelignore rename to gazelle/examples/bzlmod_build_file_generation/.bazelignore diff --git a/gazelle/examples/bzlmod_build_file_generation/.bazelrc b/gazelle/examples/bzlmod_build_file_generation/.bazelrc new file mode 100644 index 0000000000..31097b41de --- /dev/null +++ b/gazelle/examples/bzlmod_build_file_generation/.bazelrc @@ -0,0 +1,16 @@ +test --test_output=errors --enable_runfiles + +# Windows requires these for multi-python support: +build --enable_runfiles + +common --enable_bzlmod + +coverage --java_runtime_version=remotejdk_11 +common:bazel7.x --incompatible_python_disallow_native_rules + +# NOTE: This override is specific to the development of gazelle itself +# and the testing of it during its BCR release presubmits. +# In development of gazelle itself, we override it to the development +# rules_python code. In the BCR presubmits, this override is removed +# and the bazel_dep version of rules_python is used. +common --override_module=rules_python=../../../ diff --git a/examples/bzlmod_build_file_generation/.gitignore b/gazelle/examples/bzlmod_build_file_generation/.gitignore similarity index 100% rename from examples/bzlmod_build_file_generation/.gitignore rename to gazelle/examples/bzlmod_build_file_generation/.gitignore diff --git a/examples/bzlmod_build_file_generation/BUILD.bazel b/gazelle/examples/bzlmod_build_file_generation/BUILD.bazel similarity index 100% rename from examples/bzlmod_build_file_generation/BUILD.bazel rename to gazelle/examples/bzlmod_build_file_generation/BUILD.bazel diff --git a/examples/bzlmod_build_file_generation/MODULE.bazel b/gazelle/examples/bzlmod_build_file_generation/MODULE.bazel similarity index 86% rename from examples/bzlmod_build_file_generation/MODULE.bazel rename to gazelle/examples/bzlmod_build_file_generation/MODULE.bazel index 3436fbf0af..1f92ea3826 100644 --- a/examples/bzlmod_build_file_generation/MODULE.bazel +++ b/gazelle/examples/bzlmod_build_file_generation/MODULE.bazel @@ -13,32 +13,25 @@ module( # For typical setups you set the version. # See the releases page for available versions. # https://github.com/bazel-contrib/rules_python/releases -bazel_dep(name = "rules_python", version = "0.0.0") - -# The following loads rules_python from the file system. -# For usual setups you should remove this local_path_override block. -local_path_override( - module_name = "rules_python", - path = "../..", -) +bazel_dep(name = "rules_python", version = "1.4.0") # The following stanza defines the dependency rules_python_gazelle_plugin. # For typical setups you set the version. # See the releases page for available versions. # https://github.com/bazel-contrib/rules_python/releases -bazel_dep(name = "rules_python_gazelle_plugin", version = "0.0.0") +bazel_dep(name = "rules_python_gazelle_plugin", version = "1.5.0") # The following starlark loads the gazelle plugin from the file system. # For usual setups you should remove this local_path_override block. local_path_override( module_name = "rules_python_gazelle_plugin", - path = "../../gazelle", + path = "../..", ) # The following stanza defines the dependency for gazelle # See here https://github.com/bazelbuild/bazel-gazelle/releases/ for the # latest version. -bazel_dep(name = "gazelle", version = "0.36.0", repo_name = "bazel_gazelle") +bazel_dep(name = "gazelle", version = "0.47.0", repo_name = "bazel_gazelle") # The following stanze returns a proxy object representing a module extension; # its methods can be invoked to create module extension tags. @@ -84,11 +77,11 @@ use_repo(pip, "pip") # This project includes a different module that is on the local file system. # Add the module to this parent project. -bazel_dep(name = "other_module", version = "", repo_name = "our_other_module") +bazel_dep(name = "other_module", version = "0.0.0", repo_name = "our_other_module") local_path_override( module_name = "other_module", path = "other_module", ) # Only needed to make rules_python's CI happy -bazel_dep(name = "rules_java", version = "8.3.1") +bazel_dep(name = "rules_java", version = "8.16.1") diff --git a/examples/bzlmod_build_file_generation/README.md b/gazelle/examples/bzlmod_build_file_generation/README.md similarity index 100% rename from examples/bzlmod_build_file_generation/README.md rename to gazelle/examples/bzlmod_build_file_generation/README.md diff --git a/examples/bzlmod_build_file_generation/WORKSPACE b/gazelle/examples/bzlmod_build_file_generation/WORKSPACE similarity index 100% rename from examples/bzlmod_build_file_generation/WORKSPACE rename to gazelle/examples/bzlmod_build_file_generation/WORKSPACE diff --git a/examples/bzlmod_build_file_generation/__main__.py b/gazelle/examples/bzlmod_build_file_generation/__main__.py similarity index 100% rename from examples/bzlmod_build_file_generation/__main__.py rename to gazelle/examples/bzlmod_build_file_generation/__main__.py diff --git a/examples/bzlmod_build_file_generation/__test__.py b/gazelle/examples/bzlmod_build_file_generation/__test__.py similarity index 100% rename from examples/bzlmod_build_file_generation/__test__.py rename to gazelle/examples/bzlmod_build_file_generation/__test__.py diff --git a/examples/bzlmod_build_file_generation/gazelle_python.yaml b/gazelle/examples/bzlmod_build_file_generation/gazelle_python.yaml similarity index 100% rename from examples/bzlmod_build_file_generation/gazelle_python.yaml rename to gazelle/examples/bzlmod_build_file_generation/gazelle_python.yaml diff --git a/examples/bzlmod_build_file_generation/gazelle_python_with_types.yaml b/gazelle/examples/bzlmod_build_file_generation/gazelle_python_with_types.yaml similarity index 100% rename from examples/bzlmod_build_file_generation/gazelle_python_with_types.yaml rename to gazelle/examples/bzlmod_build_file_generation/gazelle_python_with_types.yaml diff --git a/examples/bzlmod_build_file_generation/lib.py b/gazelle/examples/bzlmod_build_file_generation/lib.py similarity index 100% rename from examples/bzlmod_build_file_generation/lib.py rename to gazelle/examples/bzlmod_build_file_generation/lib.py diff --git a/gazelle/examples/bzlmod_build_file_generation/other_module/MODULE.bazel b/gazelle/examples/bzlmod_build_file_generation/other_module/MODULE.bazel new file mode 100644 index 0000000000..3deeeb6ccc --- /dev/null +++ b/gazelle/examples/bzlmod_build_file_generation/other_module/MODULE.bazel @@ -0,0 +1,7 @@ +module( + name = "other_module", +) + +# Version doesn't matter because the root module overrides it, +# but Bazel requires it exist in the registry. +bazel_dep(name = "rules_python", version = "1.0.0") diff --git a/examples/bzlmod/py_proto_library/foo_external/WORKSPACE b/gazelle/examples/bzlmod_build_file_generation/other_module/WORKSPACE similarity index 100% rename from examples/bzlmod/py_proto_library/foo_external/WORKSPACE rename to gazelle/examples/bzlmod_build_file_generation/other_module/WORKSPACE diff --git a/examples/bzlmod_build_file_generation/other_module/other_module/pkg/BUILD.bazel b/gazelle/examples/bzlmod_build_file_generation/other_module/other_module/pkg/BUILD.bazel similarity index 100% rename from examples/bzlmod_build_file_generation/other_module/other_module/pkg/BUILD.bazel rename to gazelle/examples/bzlmod_build_file_generation/other_module/other_module/pkg/BUILD.bazel diff --git a/examples/bzlmod_build_file_generation/other_module/other_module/pkg/data/data.txt b/gazelle/examples/bzlmod_build_file_generation/other_module/other_module/pkg/data/data.txt similarity index 100% rename from examples/bzlmod_build_file_generation/other_module/other_module/pkg/data/data.txt rename to gazelle/examples/bzlmod_build_file_generation/other_module/other_module/pkg/data/data.txt diff --git a/examples/bzlmod_build_file_generation/other_module/other_module/pkg/lib.py b/gazelle/examples/bzlmod_build_file_generation/other_module/other_module/pkg/lib.py similarity index 100% rename from examples/bzlmod_build_file_generation/other_module/other_module/pkg/lib.py rename to gazelle/examples/bzlmod_build_file_generation/other_module/other_module/pkg/lib.py diff --git a/examples/bzlmod_build_file_generation/requirements.in b/gazelle/examples/bzlmod_build_file_generation/requirements.in similarity index 100% rename from examples/bzlmod_build_file_generation/requirements.in rename to gazelle/examples/bzlmod_build_file_generation/requirements.in diff --git a/examples/bzlmod_build_file_generation/requirements_lock.txt b/gazelle/examples/bzlmod_build_file_generation/requirements_lock.txt similarity index 100% rename from examples/bzlmod_build_file_generation/requirements_lock.txt rename to gazelle/examples/bzlmod_build_file_generation/requirements_lock.txt diff --git a/examples/bzlmod_build_file_generation/requirements_windows.txt b/gazelle/examples/bzlmod_build_file_generation/requirements_windows.txt similarity index 100% rename from examples/bzlmod_build_file_generation/requirements_windows.txt rename to gazelle/examples/bzlmod_build_file_generation/requirements_windows.txt diff --git a/examples/bzlmod_build_file_generation/runfiles/BUILD.bazel b/gazelle/examples/bzlmod_build_file_generation/runfiles/BUILD.bazel similarity index 100% rename from examples/bzlmod_build_file_generation/runfiles/BUILD.bazel rename to gazelle/examples/bzlmod_build_file_generation/runfiles/BUILD.bazel diff --git a/examples/bzlmod_build_file_generation/runfiles/data/data.txt b/gazelle/examples/bzlmod_build_file_generation/runfiles/data/data.txt similarity index 100% rename from examples/bzlmod_build_file_generation/runfiles/data/data.txt rename to gazelle/examples/bzlmod_build_file_generation/runfiles/data/data.txt diff --git a/examples/bzlmod_build_file_generation/runfiles/runfiles_test.py b/gazelle/examples/bzlmod_build_file_generation/runfiles/runfiles_test.py similarity index 100% rename from examples/bzlmod_build_file_generation/runfiles/runfiles_test.py rename to gazelle/examples/bzlmod_build_file_generation/runfiles/runfiles_test.py diff --git a/gazelle/go.mod b/gazelle/go.mod index 7623079af9..9ad4951536 100644 --- a/gazelle/go.mod +++ b/gazelle/go.mod @@ -1,12 +1,14 @@ module github.com/bazel-contrib/rules_python/gazelle -go 1.21.13 +go 1.22.9 + +toolchain go1.24.13 require ( - github.com/bazelbuild/bazel-gazelle v0.36.0 - github.com/bazelbuild/buildtools v0.0.0-20240313121412-66c605173954 + github.com/bazelbuild/bazel-gazelle v0.47.0 + github.com/bazelbuild/buildtools v0.0.0-20250930140053-2eb4fccefb52 github.com/bazelbuild/rules_go v0.55.1 - github.com/bmatcuk/doublestar/v4 v4.7.1 + github.com/bmatcuk/doublestar/v4 v4.9.1 github.com/emirpasic/gods v1.18.1 github.com/ghodss/yaml v1.0.0 github.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82 diff --git a/gazelle/go.sum b/gazelle/go.sum index 5a4d42d46a..d5fc2b1af9 100644 --- a/gazelle/go.sum +++ b/gazelle/go.sum @@ -1,106 +1,36 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/bazelbuild/bazel-gazelle v0.36.0 h1:n41ODckCkU9D2BEwBxYN+xu5E92Vd0gaW6QmsIW9l00= -github.com/bazelbuild/bazel-gazelle v0.36.0/go.mod h1:5wGHbkRpDUdz4LxREtPYwXstrWfnkV+oDmOuxNAxW1s= -github.com/bazelbuild/buildtools v0.0.0-20240313121412-66c605173954 h1:VNqmvOfFzn2Hrtoni8vqgXlIQ4C2Zt22fxeZ9gOOkp0= -github.com/bazelbuild/buildtools v0.0.0-20240313121412-66c605173954/go.mod h1:689QdV3hBP7Vo9dJMmzhoYIyo/9iMhEmHkJcnaPRCbo= +github.com/bazelbuild/bazel-gazelle v0.47.0 h1:g3Rr1ZbkC1Pk20aOgBITxSD/efS1WbaSty5jC786Z3Q= +github.com/bazelbuild/bazel-gazelle v0.47.0/go.mod h1:8Ozf20jhv+in87nCUHdmUPPcVGTfKg/gotZ/hce3T+w= +github.com/bazelbuild/buildtools v0.0.0-20250930140053-2eb4fccefb52 h1:njQAmjTv/YHRm/0Lfv9DXHFZ4MdT2IA/RKHTnqZkgDw= +github.com/bazelbuild/buildtools v0.0.0-20250930140053-2eb4fccefb52/go.mod h1:PLNUetjLa77TCCziPsz0EI8a6CUxgC+1jgmWv0H25tg= github.com/bazelbuild/rules_go v0.55.1 h1:cQYGcunY8myOB+0Ym6PGQRhc/milkRcNv0my3XgxaDU= github.com/bazelbuild/rules_go v0.55.1/go.mod h1:T90Gpyq4HDFlsrvtQa2CBdHNJ2P4rAu/uUTmQbanzf0= -github.com/bmatcuk/doublestar/v4 v4.7.1 h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0CXv75Q= -github.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= +github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82 h1:6C8qej6f1bStuePVkLSFxoU22XBS165D3klxlzRg8F4= github.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82/go.mod h1:xe4pgH49k4SsmkQq5OT8abwhWmnzkhpgnXeekbx2efw= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -go.starlark.net v0.0.0-20210223155950-e043a3d3c984/go.mod h1:t3mmBBPzAVvK0L0n1drDmrQsJ8FoIx4INCqVMTr/Zo0= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools/go/vcs v0.1.0-deprecated h1:cOIJqWBl99H1dH5LWizPa+0ImeeJq3t3cJjaeOWUAL4= golang.org/x/tools/go/vcs v0.1.0-deprecated/go.mod h1:zUrvATBAvEI9535oC0yWYsLsHIV4Z7g63sNPVMtuBy8= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/gazelle/manifest/BUILD.bazel b/gazelle/manifest/BUILD.bazel index ea81d85fbe..68f4e1be30 100644 --- a/gazelle/manifest/BUILD.bazel +++ b/gazelle/manifest/BUILD.bazel @@ -1,3 +1,4 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") exports_files([ @@ -11,8 +12,8 @@ go_library( importpath = "github.com/bazel-contrib/rules_python/gazelle/manifest", visibility = ["//visibility:public"], deps = [ - "@com_github_emirpasic_gods//sets/treeset", - "@in_gopkg_yaml_v2//:yaml_v2", + "@com_github_emirpasic_gods//sets/treeset:go_default_library", + "@in_gopkg_yaml_v2//:go_default_library", ], ) @@ -32,3 +33,14 @@ filegroup( ], visibility = ["//:__pkg__"], ) + +bzl_library( + name = "defs", + srcs = ["defs.bzl"], + visibility = ["//visibility:public"], + deps = [ + "@bazel_skylib//rules:diff_test", + "@io_bazel_rules_go//go:def", + "@rules_python//python:defs_bzl", + ], +) diff --git a/gazelle/manifest/copy_to_source.py b/gazelle/manifest/copy_to_source.py index b897b1fcf3..4342dee843 100644 --- a/gazelle/manifest/copy_to_source.py +++ b/gazelle/manifest/copy_to_source.py @@ -6,7 +6,6 @@ import os import shutil -import stat import sys from pathlib import Path @@ -20,7 +19,9 @@ def copy_to_source(generated_relative_path: Path, target_relative_path: Path) -> generated_absolute_path = Path.cwd() / generated_relative_path # Similarly, the target is relative to the source directory. - target_absolute_path = os.environ["BUILD_WORKSPACE_DIRECTORY"] / target_relative_path + target_absolute_path = ( + os.environ["BUILD_WORKSPACE_DIRECTORY"] / target_relative_path + ) print(f"Copying {generated_absolute_path} to {target_absolute_path}") target_absolute_path.parent.mkdir(parents=True, exist_ok=True) diff --git a/gazelle/manifest/defs.bzl b/gazelle/manifest/defs.bzl index 45fdb32e7d..71a4d57156 100644 --- a/gazelle/manifest/defs.bzl +++ b/gazelle/manifest/defs.bzl @@ -117,9 +117,9 @@ def gazelle_python_manifest( if requirements: attrs = { "env": { - "_TEST_MANIFEST": "$(rootpath {})".format(manifest), + "_TEST_MANIFEST": "$(rlocationpath {})".format(manifest), "_TEST_MANIFEST_GENERATOR_HASH": "$(rlocationpath {})".format(manifest_generator_hash), - "_TEST_REQUIREMENTS": "$(rootpath {})".format(requirements), + "_TEST_REQUIREMENTS": "$(rlocationpath {})".format(requirements), }, "size": "small", } @@ -184,6 +184,7 @@ def _sources_hash_impl(ctx): args.add_all(all_srcs) ctx.actions.run( outputs = [hash_file], + mnemonic = "PyGazelleManifestHash", inputs = all_srcs, arguments = [args], executable = ctx.executable._hasher, diff --git a/gazelle/manifest/test/test.go b/gazelle/manifest/test/test.go index 5804a7102e..77b354b495 100644 --- a/gazelle/manifest/test/test.go +++ b/gazelle/manifest/test/test.go @@ -26,23 +26,32 @@ import ( "path/filepath" "testing" - "github.com/bazelbuild/rules_go/go/runfiles" "github.com/bazel-contrib/rules_python/gazelle/manifest" + "github.com/bazelbuild/rules_go/go/runfiles" ) -func TestGazelleManifestIsUpdated(t *testing.T) { - requirementsPath := os.Getenv("_TEST_REQUIREMENTS") - if requirementsPath == "" { - t.Fatal("_TEST_REQUIREMENTS must be set") +// getResolvedRunfile resolves an environment variable to a runfiles path. +// It handles getting the env var, checking it's set, and resolving it through +// the runfiles mechanism, providing detailed error messages if anything fails. +func getResolvedRunfile(t *testing.T, envVar string) string { + t.Helper() + path := os.Getenv(envVar) + if path == "" { + t.Fatalf("%s must be set", envVar) } - - manifestPath := os.Getenv("_TEST_MANIFEST") - if manifestPath == "" { - t.Fatal("_TEST_MANIFEST must be set") + resolvedPath, err := runfiles.Rlocation(path) + if err != nil { + t.Fatalf("failed to resolve runfiles path for %s (%q): %v", envVar, path, err) } + return resolvedPath +} + +func TestGazelleManifestIsUpdated(t *testing.T) { + requirementsPathResolved := getResolvedRunfile(t, "_TEST_REQUIREMENTS") + manifestPathResolved := getResolvedRunfile(t, "_TEST_MANIFEST") manifestFile := new(manifest.File) - if err := manifestFile.Decode(manifestPath); err != nil { + if err := manifestFile.Decode(manifestPathResolved); err != nil { t.Fatalf("decoding manifest file: %v", err) } @@ -50,11 +59,7 @@ func TestGazelleManifestIsUpdated(t *testing.T) { t.Fatal("failed to find the Gazelle manifest file integrity") } - manifestGeneratorHashPath, err := runfiles.Rlocation( - os.Getenv("_TEST_MANIFEST_GENERATOR_HASH")) - if err != nil { - t.Fatalf("failed to resolve runfiles path of manifest: %v", err) - } + manifestGeneratorHashPath := getResolvedRunfile(t, "_TEST_MANIFEST_GENERATOR_HASH") manifestGeneratorHash, err := os.Open(manifestGeneratorHashPath) if err != nil { @@ -62,9 +67,9 @@ func TestGazelleManifestIsUpdated(t *testing.T) { } defer manifestGeneratorHash.Close() - requirements, err := os.Open(requirementsPath) + requirements, err := os.Open(requirementsPathResolved) if err != nil { - t.Fatalf("opening %q: %v", requirementsPath, err) + t.Fatalf("opening %q: %v", requirementsPathResolved, err) } defer requirements.Close() @@ -73,9 +78,9 @@ func TestGazelleManifestIsUpdated(t *testing.T) { t.Fatalf("verifying integrity: %v", err) } if !valid { - manifestRealpath, err := filepath.EvalSymlinks(manifestPath) + manifestRealpath, err := filepath.EvalSymlinks(manifestPathResolved) if err != nil { - t.Fatalf("evaluating symlink %q: %v", manifestPath, err) + t.Fatalf("evaluating symlink %q: %v", manifestPathResolved, err) } t.Errorf( "%q is out-of-date. Follow the update instructions in that file to resolve this", diff --git a/gazelle/modules_mapping/BUILD.bazel b/gazelle/modules_mapping/BUILD.bazel index 3a9a8a47f3..0e740f2636 100644 --- a/gazelle/modules_mapping/BUILD.bazel +++ b/gazelle/modules_mapping/BUILD.bazel @@ -1,3 +1,4 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") load("@bazel_skylib//rules:copy_file.bzl", "copy_file") load("@rules_python//python:defs.bzl", "py_binary", "py_test") @@ -9,6 +10,12 @@ py_binary( visibility = ["//visibility:public"], ) +py_binary( + name = "merger", + srcs = ["merger.py"], + visibility = ["//visibility:public"], +) + copy_file( name = "pytest_wheel", src = "@pytest//file", @@ -33,8 +40,26 @@ py_test( deps = [":generator"], ) +py_test( + name = "test_merger", + srcs = ["test_merger.py"], + data = [ + "django_types_wheel", + "pytest_wheel", + ], + imports = ["."], + main = "test_merger.py", + deps = [":merger"], +) + filegroup( name = "distribution", srcs = glob(["**"]), visibility = ["//:__pkg__"], ) + +bzl_library( + name = "def", + srcs = ["def.bzl"], + visibility = ["//visibility:public"], +) diff --git a/gazelle/modules_mapping/def.bzl b/gazelle/modules_mapping/def.bzl index 48a5477b93..8466edd39a 100644 --- a/gazelle/modules_mapping/def.bzl +++ b/gazelle/modules_mapping/def.bzl @@ -30,25 +30,41 @@ def _modules_mapping_impl(ctx): transitive = [dep[DefaultInfo].files for dep in ctx.attr.wheels] + [dep[DefaultInfo].data_runfiles.files for dep in ctx.attr.wheels], ) - args = ctx.actions.args() + # Run the generator once per-wheel (to leverage caching) + per_wheel_outputs = [] + for idx, whl in enumerate(all_wheels.to_list()): + wheel_modules_mapping = ctx.actions.declare_file("{}.{}".format(modules_mapping.short_path, idx)) + args = ctx.actions.args() + args.add("--output_file", wheel_modules_mapping.path) + if ctx.attr.include_stub_packages: + args.add("--include_stub_packages") + args.add_all("--exclude_patterns", ctx.attr.exclude_patterns) + args.add("--wheel", whl.path) - # Spill parameters to a file prefixed with '@'. Note, the '@' prefix is the same - # prefix as used in the `generator.py` in `fromfile_prefix_chars` attribute. - args.use_param_file(param_file_arg = "@%s") - args.set_param_file_format(format = "multiline") - if ctx.attr.include_stub_packages: - args.add("--include_stub_packages") - args.add("--output_file", modules_mapping) - args.add_all("--exclude_patterns", ctx.attr.exclude_patterns) - args.add_all("--wheels", all_wheels) + ctx.actions.run( + inputs = [whl], + mnemonic = "PyGazelleModMapGen", + outputs = [wheel_modules_mapping], + executable = ctx.executable._generator, + arguments = [args], + use_default_shell_env = False, + ) + per_wheel_outputs.append(wheel_modules_mapping) + + # Then merge the individual JSONs together + merge_args = ctx.actions.args() + merge_args.add("--output", modules_mapping.path) + merge_args.add_all("--inputs", [f.path for f in per_wheel_outputs]) ctx.actions.run( - inputs = all_wheels, + inputs = per_wheel_outputs, + mnemonic = "PyGazelleModMapMerge", outputs = [modules_mapping], - executable = ctx.executable._generator, - arguments = [args], + executable = ctx.executable._merger, + arguments = [merge_args], use_default_shell_env = False, ) + return [DefaultInfo(files = depset([modules_mapping]))] modules_mapping = rule( @@ -79,6 +95,11 @@ modules_mapping = rule( default = "//modules_mapping:generator", executable = True, ), + "_merger": attr.label( + cfg = "exec", + default = "//modules_mapping:merger", + executable = True, + ), }, doc = "Creates a modules_mapping.json file for mapping module names to wheel distribution names.", ) diff --git a/gazelle/modules_mapping/generator.py b/gazelle/modules_mapping/generator.py index ea11f3e236..611910c669 100644 --- a/gazelle/modules_mapping/generator.py +++ b/gazelle/modules_mapping/generator.py @@ -96,8 +96,7 @@ def module_for_path(self, path, whl): ext = "".join(pathlib.Path(root).suffixes) module = root[: -len(ext)].replace("/", ".") if not self.is_excluded(module): - if not self.is_excluded(module): - self.mapping[module] = wheel_name + self.mapping[module] = wheel_name def is_excluded(self, module): for pattern in self.excluded_patterns: @@ -105,14 +104,20 @@ def is_excluded(self, module): return True return False - # run is the entrypoint for the generator. - def run(self, wheels): - for whl in wheels: - try: - self.dig_wheel(whl) - except AssertionError as error: - print(error, file=self.stderr) - return 1 + def run(self, wheel: pathlib.Path) -> int: + """ + Entrypoint for the generator. + + Args: + wheel: The path to the wheel file (`.whl`) + Returns: + Exit code (for `sys.exit`) + """ + try: + self.dig_wheel(wheel) + except AssertionError as error: + print(error, file=self.stderr) + return 1 self.simplify() mapping_json = json.dumps(self.mapping) with open(self.output_file, "w") as f: @@ -152,16 +157,13 @@ def data_has_purelib_or_platlib(path): parser = argparse.ArgumentParser( prog="generator", description="Generates the modules mapping used by the Gazelle manifest.", - # Automatically read parameters from a file. Note, the '@' is the same prefix - # as set in the 'args.use_param_file' in the bazel rule. - fromfile_prefix_chars="@", ) parser.add_argument("--output_file", type=str) parser.add_argument("--include_stub_packages", action="store_true") parser.add_argument("--exclude_patterns", nargs="+", default=[]) - parser.add_argument("--wheels", nargs="+", default=[]) + parser.add_argument("--wheel", type=pathlib.Path) args = parser.parse_args() generator = Generator( sys.stderr, args.output_file, args.exclude_patterns, args.include_stub_packages ) - sys.exit(generator.run(args.wheels)) + sys.exit(generator.run(args.wheel)) diff --git a/gazelle/modules_mapping/merger.py b/gazelle/modules_mapping/merger.py new file mode 100644 index 0000000000..deb0cb2666 --- /dev/null +++ b/gazelle/modules_mapping/merger.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""Merges multiple modules_mapping.json files into a single file.""" + +import argparse +import json +from pathlib import Path + + +def merge_modules_mappings(input_files: list[Path], output_file: Path) -> None: + """Merge multiple modules_mapping.json files into one. + + Args: + input_files: List of paths to input JSON files to merge + output_file: Path where the merged output should be written + """ + merged_mapping = {} + for input_file in input_files: + mapping = json.loads(input_file.read_text()) + # Merge the mappings, with later files overwriting earlier ones + # if there are conflicts + merged_mapping.update(mapping) + + output_file.write_text(json.dumps(merged_mapping)) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Merge multiple modules_mapping.json files" + ) + parser.add_argument( + "--output", + required=True, + type=Path, + help="Output file path for merged mapping", + ) + parser.add_argument( + "--inputs", + required=True, + nargs="+", + type=Path, + help="Input JSON files to merge", + ) + + args = parser.parse_args() + merge_modules_mappings(args.inputs, args.output) diff --git a/gazelle/modules_mapping/test_merger.py b/gazelle/modules_mapping/test_merger.py new file mode 100644 index 0000000000..87c35f4404 --- /dev/null +++ b/gazelle/modules_mapping/test_merger.py @@ -0,0 +1,61 @@ +import json +import pathlib +import tempfile +import unittest + +from merger import merge_modules_mappings + + +class MergerTest(unittest.TestCase): + _tmpdir: tempfile.TemporaryDirectory + + def setUp(self) -> None: + super().setUp() + self._tmpdir = tempfile.TemporaryDirectory() + + def tearDown(self) -> None: + super().tearDown() + self._tmpdir.cleanup() + del self._tmpdir + + @property + def tmppath(self) -> pathlib.Path: + return pathlib.Path(self._tmpdir.name) + + def make_input(self, mapping: dict[str, str]) -> pathlib.Path: + _fd, file = tempfile.mkstemp(suffix=".json", dir=self._tmpdir.name) + path = pathlib.Path(file) + path.write_text(json.dumps(mapping)) + return path + + def test_merger(self): + output_path = self.tmppath / "output.json" + merge_modules_mappings( + [ + self.make_input( + { + "_pytest": "pytest", + "_pytest.__init__": "pytest", + "_pytest._argcomplete": "pytest", + "_pytest.config.argparsing": "pytest", + } + ), + self.make_input({"django_types": "django_types"}), + ], + output_path, + ) + + self.assertEqual( + { + "_pytest": "pytest", + "_pytest.__init__": "pytest", + "_pytest._argcomplete": "pytest", + "_pytest.config.argparsing": "pytest", + "django_types": "django_types", + }, + json.loads(output_path.read_text()), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/gazelle/python/BUILD.bazel b/gazelle/python/BUILD.bazel index b988e493c7..8218d0b6d9 100644 --- a/gazelle/python/BUILD.bazel +++ b/gazelle/python/BUILD.bazel @@ -30,19 +30,19 @@ go_library( visibility = ["//visibility:public"], deps = [ "//pythonconfig", - "@bazel_gazelle//config:go_default_library", - "@bazel_gazelle//label:go_default_library", - "@bazel_gazelle//language:go_default_library", - "@bazel_gazelle//repo:go_default_library", - "@bazel_gazelle//resolve:go_default_library", - "@bazel_gazelle//rule:go_default_library", + "@bazel_gazelle//config", + "@bazel_gazelle//label", + "@bazel_gazelle//language", + "@bazel_gazelle//repo", + "@bazel_gazelle//resolve", + "@bazel_gazelle//rule", "@com_github_bazelbuild_buildtools//build", "@com_github_bmatcuk_doublestar_v4//:doublestar", - "@com_github_emirpasic_gods//lists/singlylinkedlist", - "@com_github_emirpasic_gods//sets/treeset", - "@com_github_emirpasic_gods//utils", - "@com_github_smacker_go_tree_sitter//:go-tree-sitter", - "@com_github_smacker_go_tree_sitter//python", + "@com_github_emirpasic_gods//lists/singlylinkedlist:go_default_library", + "@com_github_emirpasic_gods//sets/treeset:go_default_library", + "@com_github_emirpasic_gods//utils:go_default_library", + "@com_github_smacker_go_tree_sitter//:go_default_library", + "@com_github_smacker_go_tree_sitter//python:go_default_library", "@org_golang_x_sync//errgroup", ], ) @@ -65,6 +65,9 @@ copy_file( ) # gazelle:exclude testdata/ +# gazelle:exclude extensions.bzl +# Exclude test-only Starlark helper from Gazelle to avoid generating an unnecessary bzl_library +# gazelle:exclude gazelle_test.bzl gazelle_test( name = "python_test", diff --git a/gazelle/python/configure.go b/gazelle/python/configure.go index 13ba6477cd..1fe95a1683 100644 --- a/gazelle/python/configure.go +++ b/gazelle/python/configure.go @@ -70,9 +70,11 @@ func (py *Configurer) KnownDirectives() []string { pythonconfig.LabelConvention, pythonconfig.LabelNormalization, pythonconfig.GeneratePyiDeps, + pythonconfig.GeneratePyiSrcs, pythonconfig.ExperimentalAllowRelativeImports, pythonconfig.GenerateProto, pythonconfig.PythonResolveSiblingImports, + pythonconfig.PythonIncludeAncestorConftest, } } @@ -242,6 +244,12 @@ func (py *Configurer) Configure(c *config.Config, rel string, f *rule.File) { log.Fatal(err) } config.SetGeneratePyiDeps(v) + case pythonconfig.GeneratePyiSrcs: + v, err := strconv.ParseBool(strings.TrimSpace(d.Value)) + if err != nil { + log.Fatal(err) + } + config.SetGeneratePyiSrcs(v) case pythonconfig.GenerateProto: v, err := strconv.ParseBool(strings.TrimSpace(d.Value)) if err != nil { @@ -254,6 +262,12 @@ func (py *Configurer) Configure(c *config.Config, rel string, f *rule.File) { log.Fatal(err) } config.SetResolveSiblingImports(v) + case pythonconfig.PythonIncludeAncestorConftest: + v, err := strconv.ParseBool(strings.TrimSpace(d.Value)) + if err != nil { + log.Fatal(err) + } + config.SetIncludeAncestorConftest(v) } } diff --git a/gazelle/python/generate.go b/gazelle/python/generate.go index a180ec527d..90e06a1546 100644 --- a/gazelle/python/generate.go +++ b/gazelle/python/generate.go @@ -48,13 +48,21 @@ var ( buildFilenames = []string{"BUILD", "BUILD.bazel"} ) -func GetActualKindName(kind string, args language.GenerateArgs) string { - if kindOverride, ok := args.Config.KindMap[kind]; ok { - return kindOverride.KindName +// Returns the mapped kind, or kind if no mapping is configured with the map_kind directive. +func getMappedKind(c *config.Config, kind string) string { + if mapped, ok := c.KindMap[kind]; ok { + return mapped.KindName } return kind } +// kindMatches returns whether r matches the canonical Python rule kind `expected`, respecting `# gazelle:map_kind` and +// `# gazelle:alias_kind` directives in the config.Config c. +func kindMatches(c *config.Config, r *rule.Rule, expected string) bool { + kind := r.Kind() + return kind == getMappedKind(c, expected) || c.AliasMap[kind] == expected +} + func matchesAnyGlob(s string, globs []string) bool { // This function assumes that the globs have already been validated. If a glob is // invalid, it's considered a non-match and we move on to the next pattern. @@ -66,6 +74,30 @@ func matchesAnyGlob(s string, globs []string) bool { return false } +// findConftestPaths returns package paths containing conftest.py, from currentPkg +// up through ancestors, stopping at module root. +func findConftestPaths(repoRoot, currentPkg, pythonProjectRoot string, includeAncestorConftest bool) []string { + var result []string + for pkg := currentPkg; ; pkg = filepath.Dir(pkg) { + if pkg == "." { + pkg = "" + } + if _, err := os.Stat(filepath.Join(repoRoot, pkg, conftestFilename)); err == nil { + result = append(result, pkg) + } + // We traverse up the tree to find conftest files and we start in + // the current package. Thus if we find one in the current package + // and do not want ancestors, we break early. + if !includeAncestorConftest { + break + } + if pkg == "" { + break + } + } + return result +} + // GenerateRules extracts build metadata from source files in a directory. // GenerateRules is called in each directory where an update is requested // in depth-first post-order. @@ -88,10 +120,6 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes } } - actualPyBinaryKind := GetActualKindName(pyBinaryKind, args) - actualPyLibraryKind := GetActualKindName(pyLibraryKind, args) - actualPyTestKind := GetActualKindName(pyTestKind, args) - pythonProjectRoot := cfg.PythonProjectRoot() packageName := filepath.Base(args.Dir) @@ -231,9 +259,23 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes } collisionErrors := singlylinkedlist.New() + // Create a validFilesMap of mainModules to validate if python macros have valid srcs. + validFilesMap := make(map[string]struct{}) + + // Determine whether we have an __init__.py file in this package and whether we'll implicitly include it in srcs. + var hasPopulatedInit bool + var autoIncludeInit bool + if cfg.PerFileGeneration() { + var hasInit bool + hasInit, hasPopulatedInit = hasLibraryEntrypointFile(args.Dir) + autoIncludeInit = cfg.PerFileGenerationIncludeInit() && hasInit && hasPopulatedInit + } appendPyLibrary := func(srcs *treeset.Set, pyLibraryTargetName string) { allDeps, mainModules, annotations, err := parser.parse(srcs) + for name := range mainModules { + validFilesMap[name] = struct{}{} + } if err != nil { log.Fatalf("ERROR: %v\n", err) } @@ -248,25 +290,41 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes // that we don't also generate a py_library target for it. if cfg.PerFileGeneration() { srcs.Remove(name) + // Also remove the __init__.py that was added earlier. + if autoIncludeInit { + srcs.Remove(pyLibraryEntrypointFilename) + } } } + sort.Strings(mainFileNames) for _, filename := range mainFileNames { pyBinaryTargetName := strings.TrimSuffix(filepath.Base(filename), ".py") - if err := ensureNoCollision(args.File, pyBinaryTargetName, actualPyBinaryKind); err != nil { + if err := ensureNoCollision(args.Config, args.File, pyBinaryTargetName, pyBinaryKind); err != nil { fqTarget := label.New("", args.Rel, pyBinaryTargetName) log.Printf("failed to generate target %q of kind %q: %v", - fqTarget.String(), actualPyBinaryKind, err) + fqTarget.String(), getMappedKind(args.Config, pyBinaryKind), err) continue } - pyBinary := newTargetBuilder(pyBinaryKind, pyBinaryTargetName, pythonProjectRoot, args.Rel, pyFileNames, cfg.ResolveSiblingImports()). + + // Add any sibling .pyi files to pyi_srcs + filenames := treeset.NewWith(godsutils.StringComparator, filename) + pyiSrcs, _ := getPyiFilenames(filenames, cfg.GeneratePyiSrcs(), args.Dir) + + pyBinaryBuilder := newTargetBuilder(pyBinaryKind, pyBinaryTargetName, pythonProjectRoot, args.Rel, pyFileNames, cfg.ResolveSiblingImports()). addVisibility(visibility). addSrc(filename). + addPyiSrcs(pyiSrcs). addModuleDependencies(mainModules[filename]). addResolvedDependencies(annotations.includeDeps). generateImportsAttribute(). - setAnnotations(*annotations). - build() + setAnnotations(*annotations) + + if autoIncludeInit { + pyBinaryBuilder.addSrc(pyLibraryEntrypointFilename) + } + + pyBinary := pyBinaryBuilder.build() result.Gen = append(result.Gen, pyBinary) result.Imports = append(result.Imports, pyBinary.PrivateAttr(config.GazelleImportsKey)) } @@ -280,7 +338,7 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes } generateEmptyLibrary := false for _, r := range args.File.Rules { - if r.Kind() == actualPyLibraryKind && r.Name() == pyLibraryTargetName { + if r.Name() == pyLibraryTargetName && kindMatches(args.Config, r, pyLibraryKind) { generateEmptyLibrary = true } } @@ -289,21 +347,25 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes } } + // Add any sibling .pyi files to pyi_srcs + pyiSrcs, _ := getPyiFilenames(srcs, cfg.GeneratePyiSrcs(), args.Dir) + // Check if a target with the same name we are generating already // exists, and if it is of a different kind from the one we are // generating. If so, we have to throw an error since Gazelle won't // generate it correctly. - if err := ensureNoCollision(args.File, pyLibraryTargetName, actualPyLibraryKind); err != nil { + if err := ensureNoCollision(args.Config, args.File, pyLibraryTargetName, pyLibraryKind); err != nil { fqTarget := label.New("", args.Rel, pyLibraryTargetName) err := fmt.Errorf("failed to generate target %q of kind %q: %w. "+ "Use the '# gazelle:%s' directive to change the naming convention.", - fqTarget.String(), actualPyLibraryKind, err, pythonconfig.LibraryNamingConvention) + fqTarget.String(), getMappedKind(args.Config, pyLibraryKind), err, pythonconfig.LibraryNamingConvention) collisionErrors.Add(err) } pyLibrary := newTargetBuilder(pyLibraryKind, pyLibraryTargetName, pythonProjectRoot, args.Rel, pyFileNames, cfg.ResolveSiblingImports()). addVisibility(visibility). addSrcs(srcs). + addPyiSrcs(pyiSrcs). addModuleDependencies(allDeps). addResolvedDependencies(annotations.includeDeps). generateImportsAttribute(). @@ -317,15 +379,15 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes result.Imports = append(result.Imports, pyLibrary.PrivateAttr(config.GazelleImportsKey)) } } + if cfg.PerFileGeneration() { - hasInit, nonEmptyInit := hasLibraryEntrypointFile(args.Dir) pyLibraryFilenames.Each(func(index int, filename interface{}) { pyLibraryTargetName := strings.TrimSuffix(filepath.Base(filename.(string)), ".py") - if filename == pyLibraryEntrypointFilename && !nonEmptyInit { + if filename == pyLibraryEntrypointFilename && !hasPopulatedInit { return // ignore empty __init__.py. } srcs := treeset.NewWith(godsutils.StringComparator, filename) - if cfg.PerFileGenerationIncludeInit() && hasInit && nonEmptyInit { + if autoIncludeInit { srcs.Add(pyLibraryEntrypointFilename) } appendPyLibrary(srcs, pyLibraryTargetName) @@ -346,18 +408,23 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes // exists, and if it is of a different kind from the one we are // generating. If so, we have to throw an error since Gazelle won't // generate it correctly. - if err := ensureNoCollision(args.File, pyBinaryTargetName, actualPyBinaryKind); err != nil { + if err := ensureNoCollision(args.Config, args.File, pyBinaryTargetName, pyBinaryKind); err != nil { fqTarget := label.New("", args.Rel, pyBinaryTargetName) err := fmt.Errorf("failed to generate target %q of kind %q: %w. "+ "Use the '# gazelle:%s' directive to change the naming convention.", - fqTarget.String(), actualPyBinaryKind, err, pythonconfig.BinaryNamingConvention) + fqTarget.String(), getMappedKind(args.Config, pyBinaryKind), err, pythonconfig.BinaryNamingConvention) collisionErrors.Add(err) } + // Add any sibling .pyi files to pyi_srcs + filenames := treeset.NewWith(godsutils.StringComparator, pyBinaryEntrypointFilename) + pyiSrcs, _ := getPyiFilenames(filenames, cfg.GeneratePyiSrcs(), args.Dir) + pyBinaryTarget := newTargetBuilder(pyBinaryKind, pyBinaryTargetName, pythonProjectRoot, args.Rel, pyFileNames, cfg.ResolveSiblingImports()). setMain(pyBinaryEntrypointFilename). addVisibility(visibility). addSrc(pyBinaryEntrypointFilename). + addPyiSrcs(pyiSrcs). addModuleDependencies(deps). addResolvedDependencies(annotations.includeDeps). setAnnotations(*annotations). @@ -380,15 +447,20 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes // exists, and if it is of a different kind from the one we are // generating. If so, we have to throw an error since Gazelle won't // generate it correctly. - if err := ensureNoCollision(args.File, conftestTargetname, actualPyLibraryKind); err != nil { + if err := ensureNoCollision(args.Config, args.File, conftestTargetname, pyLibraryKind); err != nil { fqTarget := label.New("", args.Rel, conftestTargetname) err := fmt.Errorf("failed to generate target %q of kind %q: %w. ", - fqTarget.String(), actualPyLibraryKind, err) + fqTarget.String(), getMappedKind(args.Config, pyLibraryKind), err) collisionErrors.Add(err) } + // Add any sibling .pyi files to pyi_srcs + filenames := treeset.NewWith(godsutils.StringComparator, conftestFilename) + pyiSrcs, _ := getPyiFilenames(filenames, cfg.GeneratePyiSrcs(), args.Dir) + conftestTarget := newTargetBuilder(pyLibraryKind, conftestTargetname, pythonProjectRoot, args.Rel, pyFileNames, cfg.ResolveSiblingImports()). addSrc(conftestFilename). + addPyiSrcs(pyiSrcs). addModuleDependencies(deps). addResolvedDependencies(annotations.includeDeps). setAnnotations(*annotations). @@ -412,15 +484,20 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes // exists, and if it is of a different kind from the one we are // generating. If so, we have to throw an error since Gazelle won't // generate it correctly. - if err := ensureNoCollision(args.File, pyTestTargetName, actualPyTestKind); err != nil { + if err := ensureNoCollision(args.Config, args.File, pyTestTargetName, pyTestKind); err != nil { fqTarget := label.New("", args.Rel, pyTestTargetName) err := fmt.Errorf("failed to generate target %q of kind %q: %w. "+ "Use the '# gazelle:%s' directive to change the naming convention.", - fqTarget.String(), actualPyTestKind, err, pythonconfig.TestNamingConvention) + fqTarget.String(), getMappedKind(args.Config, pyTestKind), err, pythonconfig.TestNamingConvention) collisionErrors.Add(err) } + + // Add any sibling .pyi files to pyi_srcs + pyiSrcs, _ := getPyiFilenames(srcs, cfg.GeneratePyiSrcs(), args.Dir) + return newTargetBuilder(pyTestKind, pyTestTargetName, pythonProjectRoot, args.Rel, pyFileNames, cfg.ResolveSiblingImports()). addSrcs(srcs). + addPyiSrcs(pyiSrcs). addModuleDependencies(deps). addResolvedDependencies(annotations.includeDeps). setAnnotations(*annotations). @@ -475,14 +552,17 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes } for _, pyTestTarget := range pyTestTargets { - if conftest != nil { - conftestModule := Module{Name: importSpecFromSrc(pythonProjectRoot, args.Rel, conftestFilename).Imp} - if pyTestTarget.annotations.includePytestConftest == nil { - // unset; default behavior - pyTestTarget.addModuleDependency(conftestModule) - } else if *pyTestTarget.annotations.includePytestConftest { - // set; add if true, do not add if false - pyTestTarget.addModuleDependency(conftestModule) + shouldAddConftest := pyTestTarget.annotations.includePytestConftest == nil || + *pyTestTarget.annotations.includePytestConftest + + if shouldAddConftest { + for _, conftestPkg := range findConftestPaths(args.Config.RepoRoot, args.Rel, pythonProjectRoot, cfg.IncludeAncestorConftest()) { + pyTestTarget.addModuleDependency( + Module{ + Name: importSpecFromSrc(pythonProjectRoot, conftestPkg, conftestFilename).Imp, + Filepath: filepath.Join(conftestPkg, conftestFilename), + }, + ) } } pyTest := pyTestTarget.build() @@ -490,7 +570,8 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes result.Gen = append(result.Gen, pyTest) result.Imports = append(result.Imports, pyTest.PrivateAttr(config.GazelleImportsKey)) } - + emptyRules := py.getRulesWithInvalidSrcs(args, validFilesMap) + result.Empty = append(result.Empty, emptyRules...) if !collisionErrors.Empty() { it := collisionErrors.Iterator() for it.Next() { @@ -502,6 +583,68 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes return result } +// getRulesWithInvalidSrcs checks existing Python rules in the BUILD file and return the rules with invalid source files. +// Invalid source files are files that do not exist or not a target. +func (py *Python) getRulesWithInvalidSrcs(args language.GenerateArgs, validFilesMap map[string]struct{}) (invalidRules []*rule.Rule) { + if args.File == nil { + return + } + for _, file := range args.GenFiles { + validFilesMap[file] = struct{}{} + } + + // allFilesMap extends validFilesMap with all regular files on disk. + // py_binary uses validFilesMap (main modules + generated files), while py_library + // and py_test use allFilesMap since any file is a valid src for them. + allFilesMap := make(map[string]struct{}, len(validFilesMap)+len(args.RegularFiles)) + for file := range validFilesMap { + allFilesMap[file] = struct{}{} + } + for _, file := range args.RegularFiles { + allFilesMap[file] = struct{}{} + } + + isTarget := func(src string) bool { + return strings.HasPrefix(src, "@") || strings.HasPrefix(src, "//") || strings.HasPrefix(src, ":") + } + for _, existingRule := range args.File.Rules { + var matchedKind string + var filesMap map[string]struct{} + if kindMatches(args.Config, existingRule, pyBinaryKind) { + matchedKind = pyBinaryKind + filesMap = validFilesMap + } else if kindMatches(args.Config, existingRule, pyLibraryKind) { + matchedKind = pyLibraryKind + filesMap = allFilesMap + } else if kindMatches(args.Config, existingRule, pyTestKind) { + matchedKind = pyTestKind + filesMap = allFilesMap + } else { + continue + } + + srcs := existingRule.AttrStrings("srcs") + if len(srcs) == 0 { + continue + } + var hasValidSrcs bool + for _, src := range srcs { + if isTarget(src) { + hasValidSrcs = true + break + } + if _, ok := filesMap[src]; ok { + hasValidSrcs = true + break + } + } + if !hasValidSrcs { + invalidRules = append(invalidRules, newTargetBuilder(matchedKind, existingRule.Name(), "", "", nil, false).build()) + } + } + return invalidRules +} + // isBazelPackage determines if the directory is a Bazel package by probing for // the existence of a known BUILD file name. func isBazelPackage(dir string) bool { @@ -557,12 +700,12 @@ func isEntrypointFile(path string) bool { } } -func ensureNoCollision(file *rule.File, targetName, kind string) error { +func ensureNoCollision(c *config.Config, file *rule.File, targetName, kind string) error { if file == nil { return nil } for _, t := range file.Rules { - if t.Name() == targetName && t.Kind() != kind { + if t.Name() == targetName && !kindMatches(c, t, kind) { return fmt.Errorf("a target of kind %q with the same name already exists", t.Kind()) } } @@ -585,7 +728,7 @@ func generateProtoLibraries(args language.GenerateArgs, cfg *pythonconfig.Config pyProtoRulesForProto := map[string]string{} if args.File != nil { for _, r := range args.File.Rules { - if r.Kind() == "py_proto_library" { + if kindMatches(args.Config, r, pyProtoLibraryKind) { pyProtoRules[r.Name()] = false protos := r.AttrStrings("deps") @@ -627,3 +770,25 @@ func generateProtoLibraries(args language.GenerateArgs, cfg *pythonconfig.Config } } + +// getPyiFilenames returns a set of existing .pyi source file names for a given set of source +// file names if GeneratePyiSrcs is set. Otherwise, returns an empty set. +func getPyiFilenames(filenames *treeset.Set, generatePyiSrcs bool, basePath string) (*treeset.Set, error) { + pyiSrcs := treeset.NewWith(godsutils.StringComparator) + if !generatePyiSrcs { + return pyiSrcs, nil + } + + it := filenames.Iterator() + for it.Next() { + pyiFilename := it.Value().(string) + "i" // foo.py --> foo.pyi + + _, err := os.Stat(filepath.Join(basePath, pyiFilename)) + // If the file DNE or there's some other error, there's nothing to do. + if err == nil { + // pyi file exists, add it + pyiSrcs.Add(pyiFilename) + } + } + return pyiSrcs, nil +} diff --git a/gazelle/python/kinds.go b/gazelle/python/kinds.go index a4ce572aaa..dac271bec6 100644 --- a/gazelle/python/kinds.go +++ b/gazelle/python/kinds.go @@ -46,10 +46,12 @@ var pyKinds = map[string]rule.KindInfo{ SubstituteAttrs: map[string]bool{}, MergeableAttrs: map[string]bool{ "srcs": true, + "imports": true, }, ResolveAttrs: map[string]bool{ "deps": true, "pyi_deps": true, + "pyi_srcs": true, }, }, pyLibraryKind: { @@ -67,6 +69,7 @@ var pyKinds = map[string]rule.KindInfo{ ResolveAttrs: map[string]bool{ "deps": true, "pyi_deps": true, + "pyi_srcs": true, }, }, pyProtoLibraryKind: { @@ -90,6 +93,7 @@ var pyKinds = map[string]rule.KindInfo{ ResolveAttrs: map[string]bool{ "deps": true, "pyi_deps": true, + "pyi_srcs": true, }, }, } diff --git a/gazelle/python/private/BUILD.bazel b/gazelle/python/private/BUILD.bazel index e69de29bb2..ccdc080664 100644 --- a/gazelle/python/private/BUILD.bazel +++ b/gazelle/python/private/BUILD.bazel @@ -0,0 +1 @@ +# gazelle:exclude extensions.bzl diff --git a/gazelle/python/target.go b/gazelle/python/target.go index 3fe5819e00..c7009a6a84 100644 --- a/gazelle/python/target.go +++ b/gazelle/python/target.go @@ -30,6 +30,7 @@ type targetBuilder struct { pythonProjectRoot string bzlPackage string srcs *treeset.Set + pyiSrcs *treeset.Set siblingSrcs *treeset.Set deps *treeset.Set resolvedDeps *treeset.Set @@ -49,6 +50,7 @@ func newTargetBuilder(kind, name, pythonProjectRoot, bzlPackage string, siblingS pythonProjectRoot: pythonProjectRoot, bzlPackage: bzlPackage, srcs: treeset.NewWith(godsutils.StringComparator), + pyiSrcs: treeset.NewWith(godsutils.StringComparator), siblingSrcs: siblingSrcs, deps: treeset.NewWith(moduleComparator), resolvedDeps: treeset.NewWith(godsutils.StringComparator), @@ -73,6 +75,21 @@ func (t *targetBuilder) addSrcs(srcs *treeset.Set) *targetBuilder { return t } +// addPyiSrc adds a single pyi_src to the target. +func (t *targetBuilder) addPyiSrc(pyiSrc string) *targetBuilder { + t.pyiSrcs.Add(pyiSrc) + return t +} + +// addPyiSrcs adds multiple pyi_srcs to the target. +func (t *targetBuilder) addPyiSrcs(pyiSrcs *treeset.Set) *targetBuilder { + it := pyiSrcs.Iterator() + for it.Next() { + t.pyiSrcs.Add(it.Value().(string)) + } + return t +} + // addModuleDependency adds a single module dep to the target. func (t *targetBuilder) addModuleDependency(dep Module) *targetBuilder { fileName := dep.Name + ".py" @@ -165,6 +182,9 @@ func (t *targetBuilder) build() *rule.Rule { if !t.srcs.Empty() { r.SetAttr("srcs", t.srcs.Values()) } + if !t.pyiSrcs.Empty() { + r.SetAttr("pyi_srcs", t.pyiSrcs.Values()) + } if !t.visibility.Empty() { r.SetAttr("visibility", t.visibility.Values()) } diff --git a/gazelle/python/testdata/directive_python_generate_pyi_srcs/BUILD.in b/gazelle/python/testdata/directive_python_generate_pyi_srcs/BUILD.in new file mode 100644 index 0000000000..a8459ad349 --- /dev/null +++ b/gazelle/python/testdata/directive_python_generate_pyi_srcs/BUILD.in @@ -0,0 +1 @@ +# gazelle:python_generate_pyi_srcs true diff --git a/gazelle/python/testdata/directive_python_generate_pyi_srcs/BUILD.out b/gazelle/python/testdata/directive_python_generate_pyi_srcs/BUILD.out new file mode 100644 index 0000000000..5beb703423 --- /dev/null +++ b/gazelle/python/testdata/directive_python_generate_pyi_srcs/BUILD.out @@ -0,0 +1,36 @@ +load("@rules_python//python:defs.bzl", "py_binary", "py_library", "py_test") + +# gazelle:python_generate_pyi_srcs true + +py_library( + name = "directive_python_generate_pyi_srcs", + srcs = [ + "__init__.py", + "bar.py", + "baz.py", + "foo.py", + ], + pyi_srcs = [ + "baz.pyi", + "foo.pyi", + ], + visibility = ["//:__subpackages__"], +) + +py_binary( + name = "directive_python_generate_pyi_srcs_bin", + srcs = ["__main__.py"], + main = "__main__.py", + pyi_srcs = ["__main__.pyi"], + visibility = ["//:__subpackages__"], +) + +py_test( + name = "directive_python_generate_pyi_srcs_test", + srcs = [ + "__test__.py", + "foo_test.py", + ], + main = "__test__.py", + pyi_srcs = ["foo_test.pyi"], +) diff --git a/gazelle/python/testdata/directive_python_generate_pyi_srcs/README.md b/gazelle/python/testdata/directive_python_generate_pyi_srcs/README.md new file mode 100644 index 0000000000..3ef10bab06 --- /dev/null +++ b/gazelle/python/testdata/directive_python_generate_pyi_srcs/README.md @@ -0,0 +1,13 @@ +# Directive: python_generate_pyi_srcs + +Test that the `python_generate_pyi_srcs` directive will add `pyi_srcs` to +generated targets and that it can be toggled on/off on a per-package basis. + +The root of the test case asserts that the default generation mode (package) +will compile multiple .pyi files into a single py_* target. + +The `per_file` directory asserts that the `file` generation mode will attach +a single .pyi file to a given target. + +Lastly, the `per_file/turn_off` directory asserts that we can turn off the +directive for subpackages. It continues with per-file generation mode. diff --git a/examples/bzlmod_build_file_generation/other_module/WORKSPACE b/gazelle/python/testdata/directive_python_generate_pyi_srcs/WORKSPACE similarity index 100% rename from examples/bzlmod_build_file_generation/other_module/WORKSPACE rename to gazelle/python/testdata/directive_python_generate_pyi_srcs/WORKSPACE diff --git a/sphinxdocs/src/sphinx_bzl/__init__.py b/gazelle/python/testdata/directive_python_generate_pyi_srcs/__init__.py similarity index 100% rename from sphinxdocs/src/sphinx_bzl/__init__.py rename to gazelle/python/testdata/directive_python_generate_pyi_srcs/__init__.py diff --git a/tests/integration/ignore_root_user_error/submodule/BUILD.bazel b/gazelle/python/testdata/directive_python_generate_pyi_srcs/__main__.py similarity index 100% rename from tests/integration/ignore_root_user_error/submodule/BUILD.bazel rename to gazelle/python/testdata/directive_python_generate_pyi_srcs/__main__.py diff --git a/tests/integration/ignore_root_user_error/submodule/WORKSPACE b/gazelle/python/testdata/directive_python_generate_pyi_srcs/__main__.pyi similarity index 100% rename from tests/integration/ignore_root_user_error/submodule/WORKSPACE rename to gazelle/python/testdata/directive_python_generate_pyi_srcs/__main__.pyi diff --git a/gazelle/python/testdata/directive_python_generate_pyi_srcs/__test__.py b/gazelle/python/testdata/directive_python_generate_pyi_srcs/__test__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/directive_python_generate_pyi_srcs/bar.py b/gazelle/python/testdata/directive_python_generate_pyi_srcs/bar.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/directive_python_generate_pyi_srcs/baz.py b/gazelle/python/testdata/directive_python_generate_pyi_srcs/baz.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/directive_python_generate_pyi_srcs/baz.pyi b/gazelle/python/testdata/directive_python_generate_pyi_srcs/baz.pyi new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/directive_python_generate_pyi_srcs/foo.py b/gazelle/python/testdata/directive_python_generate_pyi_srcs/foo.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/directive_python_generate_pyi_srcs/foo.pyi b/gazelle/python/testdata/directive_python_generate_pyi_srcs/foo.pyi new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/directive_python_generate_pyi_srcs/foo_test.py b/gazelle/python/testdata/directive_python_generate_pyi_srcs/foo_test.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/directive_python_generate_pyi_srcs/foo_test.pyi b/gazelle/python/testdata/directive_python_generate_pyi_srcs/foo_test.pyi new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/directive_python_generate_pyi_srcs/per_file/BUILD.in b/gazelle/python/testdata/directive_python_generate_pyi_srcs/per_file/BUILD.in new file mode 100644 index 0000000000..a7dbb7e4c1 --- /dev/null +++ b/gazelle/python/testdata/directive_python_generate_pyi_srcs/per_file/BUILD.in @@ -0,0 +1,2 @@ +# gazelle:python_generate_pyi_srcs true +# gazelle:python_generation_mode file diff --git a/gazelle/python/testdata/directive_python_generate_pyi_srcs/per_file/BUILD.out b/gazelle/python/testdata/directive_python_generate_pyi_srcs/per_file/BUILD.out new file mode 100644 index 0000000000..d0889fc46f --- /dev/null +++ b/gazelle/python/testdata/directive_python_generate_pyi_srcs/per_file/BUILD.out @@ -0,0 +1,30 @@ +load("@rules_python//python:defs.bzl", "py_binary", "py_library", "py_test") + +# gazelle:python_generate_pyi_srcs true +# gazelle:python_generation_mode file + +py_library( + name = "bar", + srcs = ["bar.py"], + visibility = ["//:__subpackages__"], +) + +py_library( + name = "foo", + srcs = ["foo.py"], + pyi_srcs = ["foo.pyi"], + visibility = ["//:__subpackages__"], +) + +py_binary( + name = "my_binary", + srcs = ["my_binary.py"], + pyi_srcs = ["my_binary.pyi"], + visibility = ["//:__subpackages__"], +) + +py_test( + name = "bar_test", + srcs = ["bar_test.py"], + pyi_srcs = ["bar_test.pyi"], +) diff --git a/gazelle/python/testdata/directive_python_generate_pyi_srcs/per_file/__init__.py b/gazelle/python/testdata/directive_python_generate_pyi_srcs/per_file/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/directive_python_generate_pyi_srcs/per_file/bar.py b/gazelle/python/testdata/directive_python_generate_pyi_srcs/per_file/bar.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/directive_python_generate_pyi_srcs/per_file/bar_test.py b/gazelle/python/testdata/directive_python_generate_pyi_srcs/per_file/bar_test.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/directive_python_generate_pyi_srcs/per_file/bar_test.pyi b/gazelle/python/testdata/directive_python_generate_pyi_srcs/per_file/bar_test.pyi new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/directive_python_generate_pyi_srcs/per_file/foo.py b/gazelle/python/testdata/directive_python_generate_pyi_srcs/per_file/foo.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/directive_python_generate_pyi_srcs/per_file/foo.pyi b/gazelle/python/testdata/directive_python_generate_pyi_srcs/per_file/foo.pyi new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/directive_python_generate_pyi_srcs/per_file/my_binary.py b/gazelle/python/testdata/directive_python_generate_pyi_srcs/per_file/my_binary.py new file mode 100644 index 0000000000..6a81382943 --- /dev/null +++ b/gazelle/python/testdata/directive_python_generate_pyi_srcs/per_file/my_binary.py @@ -0,0 +1,2 @@ +if __name__ == "__main__": + print("hey") diff --git a/gazelle/python/testdata/directive_python_generate_pyi_srcs/per_file/my_binary.pyi b/gazelle/python/testdata/directive_python_generate_pyi_srcs/per_file/my_binary.pyi new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/directive_python_generate_pyi_srcs/per_file/turn_off/BUILD.in b/gazelle/python/testdata/directive_python_generate_pyi_srcs/per_file/turn_off/BUILD.in new file mode 100644 index 0000000000..7a24055cd0 --- /dev/null +++ b/gazelle/python/testdata/directive_python_generate_pyi_srcs/per_file/turn_off/BUILD.in @@ -0,0 +1 @@ +# gazelle:python_generate_pyi_srcs false diff --git a/gazelle/python/testdata/directive_python_generate_pyi_srcs/per_file/turn_off/BUILD.out b/gazelle/python/testdata/directive_python_generate_pyi_srcs/per_file/turn_off/BUILD.out new file mode 100644 index 0000000000..25b508a7f0 --- /dev/null +++ b/gazelle/python/testdata/directive_python_generate_pyi_srcs/per_file/turn_off/BUILD.out @@ -0,0 +1,9 @@ +load("@rules_python//python:defs.bzl", "py_library") + +# gazelle:python_generate_pyi_srcs false + +py_library( + name = "foo", + srcs = ["foo.py"], + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/directive_python_generate_pyi_srcs/per_file/turn_off/foo.py b/gazelle/python/testdata/directive_python_generate_pyi_srcs/per_file/turn_off/foo.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/directive_python_generate_pyi_srcs/per_file/turn_off/foo.pyi b/gazelle/python/testdata/directive_python_generate_pyi_srcs/per_file/turn_off/foo.pyi new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/directive_python_generate_pyi_srcs/test.yaml b/gazelle/python/testdata/directive_python_generate_pyi_srcs/test.yaml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/directive_python_include_ancestor_conftest/BUILD.in b/gazelle/python/testdata/directive_python_include_ancestor_conftest/BUILD.in new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/directive_python_include_ancestor_conftest/BUILD.out b/gazelle/python/testdata/directive_python_include_ancestor_conftest/BUILD.out new file mode 100644 index 0000000000..c7adad8336 --- /dev/null +++ b/gazelle/python/testdata/directive_python_include_ancestor_conftest/BUILD.out @@ -0,0 +1,8 @@ +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "conftest", + testonly = True, + srcs = ["conftest.py"], + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/directive_python_include_ancestor_conftest/MODULE.bazel b/gazelle/python/testdata/directive_python_include_ancestor_conftest/MODULE.bazel new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/directive_python_include_ancestor_conftest/README.md b/gazelle/python/testdata/directive_python_include_ancestor_conftest/README.md new file mode 100644 index 0000000000..956e65155b --- /dev/null +++ b/gazelle/python/testdata/directive_python_include_ancestor_conftest/README.md @@ -0,0 +1,23 @@ +# Directive: `python_include_ancestor_conftest` + +This test case asserts that the `# gazelle:python_include_ancestor_conftest` +directive correctly includes or excludes ancestor `conftest` targets in +`py_test` target dependencies. + +The test also asserts that the directive can be applied at any level and that +child levels will inherit the value: + ++ The root level does not set the directive (it defaults to True). ++ The next level, `one/`, inherits that value. ++ The next level, `one/two/`, sets the directive to False; consequently the + `py_test` target only includes the sibling `:conftest` target. + + The `one/two/no_conftest/` directory does not contain a `conftest.py` file + thereby asserting that we correctly do not include any `conftest` targets + whatsoever. ++ The final level, `one/two/three/`, sets the directive back to True, meaning + the `py_test` target includes a total of 4 `conftest` targets. + + The `one/two/three/no_conftest/` directory does not contain a `conftest.py` + file and thus asserts that the code includes _only_ ancestor `conftest` + targets. + +See [Issue #3595](https://github.com/bazel-contrib/rules_python/issues/3595). diff --git a/gazelle/python/testdata/directive_python_include_ancestor_conftest/WORKSPACE b/gazelle/python/testdata/directive_python_include_ancestor_conftest/WORKSPACE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/directive_python_include_ancestor_conftest/conftest.py b/gazelle/python/testdata/directive_python_include_ancestor_conftest/conftest.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/BUILD.in b/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/BUILD.in new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/BUILD.out b/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/BUILD.out new file mode 100644 index 0000000000..9d0405e92f --- /dev/null +++ b/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/BUILD.out @@ -0,0 +1,17 @@ +load("@rules_python//python:defs.bzl", "py_library", "py_test") + +py_library( + name = "conftest", + testonly = True, + srcs = ["conftest.py"], + visibility = ["//:__subpackages__"], +) + +py_test( + name = "my_test", + srcs = ["my_test.py"], + deps = [ + ":conftest", + "//:conftest", + ], +) diff --git a/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/conftest.py b/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/conftest.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/my_test.py b/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/my_test.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/BUILD.in b/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/BUILD.in new file mode 100644 index 0000000000..3805e248e2 --- /dev/null +++ b/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/BUILD.in @@ -0,0 +1 @@ +# gazelle:python_include_ancestor_conftest false diff --git a/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/BUILD.out b/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/BUILD.out new file mode 100644 index 0000000000..edb91a38f5 --- /dev/null +++ b/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/BUILD.out @@ -0,0 +1,16 @@ +load("@rules_python//python:defs.bzl", "py_library", "py_test") + +# gazelle:python_include_ancestor_conftest false + +py_library( + name = "conftest", + testonly = True, + srcs = ["conftest.py"], + visibility = ["//:__subpackages__"], +) + +py_test( + name = "my_test", + srcs = ["my_test.py"], + deps = [":conftest"], +) diff --git a/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/conftest.py b/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/conftest.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/my_test.py b/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/my_test.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/no_conftest/BUILD.in b/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/no_conftest/BUILD.in new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/no_conftest/BUILD.out b/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/no_conftest/BUILD.out new file mode 100644 index 0000000000..764e2b4172 --- /dev/null +++ b/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/no_conftest/BUILD.out @@ -0,0 +1,6 @@ +load("@rules_python//python:defs.bzl", "py_test") + +py_test( + name = "my_test", + srcs = ["my_test.py"], +) diff --git a/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/no_conftest/my_test.py b/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/no_conftest/my_test.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/three/BUILD.in b/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/three/BUILD.in new file mode 100644 index 0000000000..987cd7dc20 --- /dev/null +++ b/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/three/BUILD.in @@ -0,0 +1 @@ +# gazelle:python_include_ancestor_conftest true diff --git a/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/three/BUILD.out b/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/three/BUILD.out new file mode 100644 index 0000000000..605aa00f88 --- /dev/null +++ b/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/three/BUILD.out @@ -0,0 +1,21 @@ +load("@rules_python//python:defs.bzl", "py_library", "py_test") + +# gazelle:python_include_ancestor_conftest true + +py_library( + name = "conftest", + testonly = True, + srcs = ["conftest.py"], + visibility = ["//:__subpackages__"], +) + +py_test( + name = "my_test", + srcs = ["my_test.py"], + deps = [ + ":conftest", + "//:conftest", + "//one:conftest", + "//one/two:conftest", + ], +) diff --git a/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/three/conftest.py b/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/three/conftest.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/three/my_test.py b/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/three/my_test.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/three/no_conftest/BUILD.in b/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/three/no_conftest/BUILD.in new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/three/no_conftest/BUILD.out b/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/three/no_conftest/BUILD.out new file mode 100644 index 0000000000..c1bddccc30 --- /dev/null +++ b/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/three/no_conftest/BUILD.out @@ -0,0 +1,12 @@ +load("@rules_python//python:defs.bzl", "py_test") + +py_test( + name = "my_test", + srcs = ["my_test.py"], + deps = [ + "//:conftest", + "//one:conftest", + "//one/two:conftest", + "//one/two/three:conftest", + ], +) diff --git a/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/three/no_conftest/my_test.py b/gazelle/python/testdata/directive_python_include_ancestor_conftest/one/two/three/no_conftest/my_test.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/directive_python_include_ancestor_conftest/test.yaml b/gazelle/python/testdata/directive_python_include_ancestor_conftest/test.yaml new file mode 100644 index 0000000000..36dd656b39 --- /dev/null +++ b/gazelle/python/testdata/directive_python_include_ancestor_conftest/test.yaml @@ -0,0 +1,3 @@ +--- +expect: + exit_code: 0 diff --git a/gazelle/python/testdata/naming_convention_mapped_fail/BUILD.in b/gazelle/python/testdata/naming_convention_mapped_fail/BUILD.in new file mode 100644 index 0000000000..63ca6b805f --- /dev/null +++ b/gazelle/python/testdata/naming_convention_mapped_fail/BUILD.in @@ -0,0 +1,9 @@ +# gazelle:map_kind py_library my_lib :mytest.bzl +# gazelle:map_kind py_binary my_bin :mytest.bzl +# gazelle:map_kind py_test my_test :mytest.bzl + +py_library(name = "naming_convention_mapped_fail") + +py_binary(name = "naming_convention_mapped_fail_bin") + +py_test(name = "naming_convention_mapped_fail_test") diff --git a/gazelle/python/testdata/naming_convention_mapped_fail/BUILD.out b/gazelle/python/testdata/naming_convention_mapped_fail/BUILD.out new file mode 100644 index 0000000000..63ca6b805f --- /dev/null +++ b/gazelle/python/testdata/naming_convention_mapped_fail/BUILD.out @@ -0,0 +1,9 @@ +# gazelle:map_kind py_library my_lib :mytest.bzl +# gazelle:map_kind py_binary my_bin :mytest.bzl +# gazelle:map_kind py_test my_test :mytest.bzl + +py_library(name = "naming_convention_mapped_fail") + +py_binary(name = "naming_convention_mapped_fail_bin") + +py_test(name = "naming_convention_mapped_fail_test") diff --git a/gazelle/python/testdata/naming_convention_mapped_fail/README.md b/gazelle/python/testdata/naming_convention_mapped_fail/README.md new file mode 100644 index 0000000000..85a561c575 --- /dev/null +++ b/gazelle/python/testdata/naming_convention_mapped_fail/README.md @@ -0,0 +1,4 @@ +# Naming convention fail with `map_kind` + +This test case asserts that collision error messages reference the mapped rule +kind over than the canonical Python kind when using `# gazelle:map_kind`. diff --git a/gazelle/python/testdata/remove_invalid_library/WORKSPACE b/gazelle/python/testdata/naming_convention_mapped_fail/WORKSPACE similarity index 100% rename from gazelle/python/testdata/remove_invalid_library/WORKSPACE rename to gazelle/python/testdata/naming_convention_mapped_fail/WORKSPACE diff --git a/gazelle/python/testdata/naming_convention_mapped_fail/__init__.py b/gazelle/python/testdata/naming_convention_mapped_fail/__init__.py new file mode 100644 index 0000000000..a75c47faaf --- /dev/null +++ b/gazelle/python/testdata/naming_convention_mapped_fail/__init__.py @@ -0,0 +1 @@ +# Empty test file. \ No newline at end of file diff --git a/gazelle/python/testdata/naming_convention_mapped_fail/__main__.py b/gazelle/python/testdata/naming_convention_mapped_fail/__main__.py new file mode 100644 index 0000000000..a75c47faaf --- /dev/null +++ b/gazelle/python/testdata/naming_convention_mapped_fail/__main__.py @@ -0,0 +1 @@ +# Empty test file. \ No newline at end of file diff --git a/gazelle/python/testdata/naming_convention_mapped_fail/__test__.py b/gazelle/python/testdata/naming_convention_mapped_fail/__test__.py new file mode 100644 index 0000000000..a75c47faaf --- /dev/null +++ b/gazelle/python/testdata/naming_convention_mapped_fail/__test__.py @@ -0,0 +1 @@ +# Empty test file. \ No newline at end of file diff --git a/gazelle/python/testdata/naming_convention_mapped_fail/test.yaml b/gazelle/python/testdata/naming_convention_mapped_fail/test.yaml new file mode 100644 index 0000000000..1bdf1acbc5 --- /dev/null +++ b/gazelle/python/testdata/naming_convention_mapped_fail/test.yaml @@ -0,0 +1,7 @@ +--- +expect: + exit_code: 1 + stderr: | + gazelle: ERROR: failed to generate target "//:naming_convention_mapped_fail" of kind "my_lib": a target of kind "py_library" with the same name already exists. Use the '# gazelle:python_library_naming_convention' directive to change the naming convention. + gazelle: ERROR: failed to generate target "//:naming_convention_mapped_fail_bin" of kind "my_bin": a target of kind "py_binary" with the same name already exists. Use the '# gazelle:python_binary_naming_convention' directive to change the naming convention. + gazelle: ERROR: failed to generate target "//:naming_convention_mapped_fail_test" of kind "my_test": a target of kind "py_test" with the same name already exists. Use the '# gazelle:python_test_naming_convention' directive to change the naming convention. diff --git a/gazelle/python/testdata/per_file_non_empty_init/BUILD.out b/gazelle/python/testdata/per_file_non_empty_init/BUILD.out index ee4a417966..38dcaa5ee2 100644 --- a/gazelle/python/testdata/per_file_non_empty_init/BUILD.out +++ b/gazelle/python/testdata/per_file_non_empty_init/BUILD.out @@ -1,4 +1,4 @@ -load("@rules_python//python:defs.bzl", "py_library") +load("@rules_python//python:defs.bzl", "py_binary", "py_library") # gazelle:python_generation_mode file # gazelle:python_generation_mode_per_file_include_init true @@ -18,3 +18,13 @@ py_library( ], visibility = ["//:__subpackages__"], ) + +py_binary( + name = "foobin", + srcs = [ + "__init__.py", + "foobin.py", + ], + visibility = ["//:__subpackages__"], + deps = [":foo"], +) diff --git a/gazelle/python/testdata/per_file_non_empty_init/README.md b/gazelle/python/testdata/per_file_non_empty_init/README.md index 6e6e9e245d..0fad0f3d43 100644 --- a/gazelle/python/testdata/per_file_non_empty_init/README.md +++ b/gazelle/python/testdata/per_file_non_empty_init/README.md @@ -1,3 +1,3 @@ # Per-file generation -This test case generates one `py_library` per file, including `__init__.py`. +This test case generates one `py_library` or `py_binary` per file, including `__init__.py`. diff --git a/gazelle/python/testdata/per_file_non_empty_init/foo.py b/gazelle/python/testdata/per_file_non_empty_init/foo.py index 730755995d..b70cc96b7e 100644 --- a/gazelle/python/testdata/per_file_non_empty_init/foo.py +++ b/gazelle/python/testdata/per_file_non_empty_init/foo.py @@ -13,3 +13,4 @@ # limitations under the License. # For test purposes only. +BAR = "baz" diff --git a/gazelle/python/testdata/per_file_non_empty_init/foobin.py b/gazelle/python/testdata/per_file_non_empty_init/foobin.py new file mode 100644 index 0000000000..6b493b531e --- /dev/null +++ b/gazelle/python/testdata/per_file_non_empty_init/foobin.py @@ -0,0 +1,4 @@ +from foo import BAR + +if __name__ == "__main__": + print(BAR) diff --git a/gazelle/python/testdata/remove_invalid_binary/BUILD.in b/gazelle/python/testdata/remove_invalid_binary/BUILD.in new file mode 100644 index 0000000000..f4bfd65c1b --- /dev/null +++ b/gazelle/python/testdata/remove_invalid_binary/BUILD.in @@ -0,0 +1,20 @@ +load("@rules_python//python:defs.bzl", "py_binary", "py_library") + +py_library( + name = "remove_invalid_binary", + srcs = ["__init__.py"], + deps = ["//keep_binary:foo"], +) + +py_binary( + name = "remove_invalid_binary_bin", + srcs = ["__main__.py"], + data = ["testdata/test.txt"], + visibility = ["//:__subpackages__"], +) + +py_binary( + name = "another_removed_binary", + srcs = ["foo.py"], # eg a now-deleted file that used to have `if __name__` block + imports = ["."], +) diff --git a/gazelle/python/testdata/remove_invalid_binary/BUILD.out b/gazelle/python/testdata/remove_invalid_binary/BUILD.out new file mode 100644 index 0000000000..a217b4bdaf --- /dev/null +++ b/gazelle/python/testdata/remove_invalid_binary/BUILD.out @@ -0,0 +1,7 @@ +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "remove_invalid_binary", + srcs = ["__init__.py"], + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/remove_invalid_binary/README.md b/gazelle/python/testdata/remove_invalid_binary/README.md new file mode 100644 index 0000000000..be8a894420 --- /dev/null +++ b/gazelle/python/testdata/remove_invalid_binary/README.md @@ -0,0 +1,3 @@ +# Remove invalid binary + +This test case asserts that `py_binary` should be deleted if invalid (no source files). diff --git a/gazelle/python/testdata/remove_invalid_binary/WORKSPACE b/gazelle/python/testdata/remove_invalid_binary/WORKSPACE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/remove_invalid_binary/__init__.py b/gazelle/python/testdata/remove_invalid_binary/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/remove_invalid_binary/keep_binary/BUILD.in b/gazelle/python/testdata/remove_invalid_binary/keep_binary/BUILD.in new file mode 100644 index 0000000000..0036c6797a --- /dev/null +++ b/gazelle/python/testdata/remove_invalid_binary/keep_binary/BUILD.in @@ -0,0 +1,13 @@ +load("@rules_python//python:defs.bzl", "py_binary", "py_library") + +py_binary( + name = "foo", + srcs = ["foo.py"], + visibility = ["//:__subpackages__"], +) + +py_library( + name = "keep_binary", + srcs = ["foo.py"], + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/remove_invalid_binary/keep_binary/BUILD.out b/gazelle/python/testdata/remove_invalid_binary/keep_binary/BUILD.out new file mode 100644 index 0000000000..0036c6797a --- /dev/null +++ b/gazelle/python/testdata/remove_invalid_binary/keep_binary/BUILD.out @@ -0,0 +1,13 @@ +load("@rules_python//python:defs.bzl", "py_binary", "py_library") + +py_binary( + name = "foo", + srcs = ["foo.py"], + visibility = ["//:__subpackages__"], +) + +py_library( + name = "keep_binary", + srcs = ["foo.py"], + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/remove_invalid_binary/keep_binary/foo.py b/gazelle/python/testdata/remove_invalid_binary/keep_binary/foo.py new file mode 100644 index 0000000000..d3b51eea8e --- /dev/null +++ b/gazelle/python/testdata/remove_invalid_binary/keep_binary/foo.py @@ -0,0 +1,2 @@ +if __name__ == "__main__": + print("foo") diff --git a/gazelle/python/testdata/remove_invalid_binary/test.yaml b/gazelle/python/testdata/remove_invalid_binary/test.yaml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/remove_invalid_library/BUILD.in b/gazelle/python/testdata/remove_invalid_library_package_mode/BUILD.in similarity index 68% rename from gazelle/python/testdata/remove_invalid_library/BUILD.in rename to gazelle/python/testdata/remove_invalid_library_package_mode/BUILD.in index 3f24c8df35..815e673ee1 100644 --- a/gazelle/python/testdata/remove_invalid_library/BUILD.in +++ b/gazelle/python/testdata/remove_invalid_library_package_mode/BUILD.in @@ -1,4 +1,4 @@ -load("@rules_python//python:defs.bzl", "py_library") +load("@rules_python//python:defs.bzl", "py_library", "py_test") py_library( name = "remove_invalid_library", @@ -14,3 +14,8 @@ py_library( "@pypi//foo", ], ) + +py_test( + name = "my_test", + srcs = ["my_test.py"], +) diff --git a/gazelle/python/testdata/remove_invalid_library/BUILD.out b/gazelle/python/testdata/remove_invalid_library_package_mode/BUILD.out similarity index 55% rename from gazelle/python/testdata/remove_invalid_library/BUILD.out rename to gazelle/python/testdata/remove_invalid_library_package_mode/BUILD.out index 4a6fffa183..80d47076c3 100644 --- a/gazelle/python/testdata/remove_invalid_library/BUILD.out +++ b/gazelle/python/testdata/remove_invalid_library_package_mode/BUILD.out @@ -1,4 +1,4 @@ -load("@rules_python//python:defs.bzl", "py_library") +load("@rules_python//python:defs.bzl", "py_library", "py_test") py_library( name = "deps_with_no_srcs_library", @@ -8,3 +8,8 @@ py_library( "@pypi//foo", ], ) + +py_test( + name = "my_test", + srcs = ["my_test.py"], +) diff --git a/gazelle/python/testdata/remove_invalid_library/README.md b/gazelle/python/testdata/remove_invalid_library_package_mode/README.md similarity index 100% rename from gazelle/python/testdata/remove_invalid_library/README.md rename to gazelle/python/testdata/remove_invalid_library_package_mode/README.md diff --git a/gazelle/python/testdata/remove_invalid_library_package_mode/WORKSPACE b/gazelle/python/testdata/remove_invalid_library_package_mode/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/remove_invalid_library_package_mode/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/remove_invalid_library_package_mode/my_test.py b/gazelle/python/testdata/remove_invalid_library_package_mode/my_test.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/remove_invalid_library/others/BUILD.in b/gazelle/python/testdata/remove_invalid_library_package_mode/others/BUILD.in similarity index 100% rename from gazelle/python/testdata/remove_invalid_library/others/BUILD.in rename to gazelle/python/testdata/remove_invalid_library_package_mode/others/BUILD.in diff --git a/gazelle/python/testdata/remove_invalid_library/others/BUILD.out b/gazelle/python/testdata/remove_invalid_library_package_mode/others/BUILD.out similarity index 100% rename from gazelle/python/testdata/remove_invalid_library/others/BUILD.out rename to gazelle/python/testdata/remove_invalid_library_package_mode/others/BUILD.out diff --git a/gazelle/python/testdata/remove_invalid_library/test.yaml b/gazelle/python/testdata/remove_invalid_library_package_mode/test.yaml similarity index 100% rename from gazelle/python/testdata/remove_invalid_library/test.yaml rename to gazelle/python/testdata/remove_invalid_library_package_mode/test.yaml diff --git a/gazelle/python/testdata/remove_invalid_per_file/BUILD.in b/gazelle/python/testdata/remove_invalid_per_file/BUILD.in new file mode 100644 index 0000000000..d3b15a7866 --- /dev/null +++ b/gazelle/python/testdata/remove_invalid_per_file/BUILD.in @@ -0,0 +1,45 @@ +load("@rules_python//python:defs.bzl", "py_binary", "py_library", "py_test") + +# gazelle:python_generation_mode file + +# Valid py_library — bar.py exists on disk, should be kept. +py_library( + name = "bar", + srcs = ["bar.py"], + visibility = ["//:__subpackages__"], +) + +# Stale py_library — deleted.py does not exist, should be removed. +py_library( + name = "deleted_lib", + srcs = ["deleted.py"], + visibility = ["//:__subpackages__"], +) + +# Valid py_binary — __main__.py exists on disk, should be kept. +py_binary( + name = "remove_invalid_per_file_bin", + srcs = ["__main__.py"], + visibility = ["//:__subpackages__"], +) + +# Stale py_binary — deleted_bin.py does not exist, should be removed. +py_binary( + name = "deleted_bin", + srcs = ["deleted_bin.py"], + visibility = ["//:__subpackages__"], +) + +# Valid py_test — bar_test.py exists on disk, should be kept. +py_test( + name = "bar_test", + srcs = ["bar_test.py"], + visibility = ["//:__subpackages__"], +) + +# Stale py_test — deleted_test.py does not exist, should be removed. +py_test( + name = "deleted_test", + srcs = ["deleted_test.py"], + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/remove_invalid_per_file/BUILD.out b/gazelle/python/testdata/remove_invalid_per_file/BUILD.out new file mode 100644 index 0000000000..376c14c0e1 --- /dev/null +++ b/gazelle/python/testdata/remove_invalid_per_file/BUILD.out @@ -0,0 +1,24 @@ +load("@rules_python//python:defs.bzl", "py_binary", "py_library", "py_test") + +# gazelle:python_generation_mode file + +# Valid py_library — bar.py exists on disk, should be kept. +py_library( + name = "bar", + srcs = ["bar.py"], + visibility = ["//:__subpackages__"], +) + +# Valid py_test — bar_test.py exists on disk, should be kept. +py_test( + name = "bar_test", + srcs = ["bar_test.py"], + visibility = ["//:__subpackages__"], +) + +py_binary( + name = "remove_invalid_per_file_bin", + srcs = ["__main__.py"], + main = "__main__.py", + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/remove_invalid_per_file/WORKSPACE b/gazelle/python/testdata/remove_invalid_per_file/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/remove_invalid_per_file/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/remove_invalid_per_file/__main__.py b/gazelle/python/testdata/remove_invalid_per_file/__main__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/remove_invalid_per_file/alias_kind/BUILD.in b/gazelle/python/testdata/remove_invalid_per_file/alias_kind/BUILD.in new file mode 100644 index 0000000000..e56ea1617a --- /dev/null +++ b/gazelle/python/testdata/remove_invalid_per_file/alias_kind/BUILD.in @@ -0,0 +1,37 @@ +load(":mylib.bzl", "my_py_library", "my_py_test") + +# gazelle:python_generation_mode file +# gazelle:alias_kind my_py_library py_library +# gazelle:alias_kind my_py_test py_test + +# Valid aliased py_library — bar.py exists on disk, should be kept. +my_py_library( + name = "bar", + srcs = ["bar.py"], + visibility = ["//:__subpackages__"], +) + +# Stale aliased py_library — deleted.py does not exist, should be removed. +# TODO: Known limitation: not fully deleted until gazelle is bumped to include +# https://github.com/bazel-contrib/bazel-gazelle/pull/2362 +my_py_library( + name = "deleted_lib", + srcs = ["deleted.py"], + visibility = ["//:__subpackages__"], +) + +# Valid aliased py_test — bar_test.py exists on disk, should be kept. +my_py_test( + name = "bar_test", + srcs = ["bar_test.py"], + visibility = ["//:__subpackages__"], +) + +# Stale aliased py_test — deleted_test.py does not exist, should be removed. +# TODO: Known limitation: not fully deleted until gazelle is bumped to include +# https://github.com/bazel-contrib/bazel-gazelle/pull/2362 +my_py_test( + name = "deleted_test", + srcs = ["deleted_test.py"], + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/remove_invalid_per_file/alias_kind/BUILD.out b/gazelle/python/testdata/remove_invalid_per_file/alias_kind/BUILD.out new file mode 100644 index 0000000000..19d6c01789 --- /dev/null +++ b/gazelle/python/testdata/remove_invalid_per_file/alias_kind/BUILD.out @@ -0,0 +1,35 @@ +load(":mylib.bzl", "my_py_library", "my_py_test") + +# gazelle:python_generation_mode file +# gazelle:alias_kind my_py_library py_library +# gazelle:alias_kind my_py_test py_test + +# Valid aliased py_library — bar.py exists on disk, should be kept. +my_py_library( + name = "bar", + srcs = ["bar.py"], + visibility = ["//:__subpackages__"], +) + +# Stale aliased py_library — deleted.py does not exist, should be removed. +# TODO: Known limitation: not fully deleted until gazelle is bumped to include +# https://github.com/bazel-contrib/bazel-gazelle/pull/2362 +my_py_library( + name = "deleted_lib", + visibility = ["//:__subpackages__"], +) + +# Valid aliased py_test — bar_test.py exists on disk, should be kept. +my_py_test( + name = "bar_test", + srcs = ["bar_test.py"], + visibility = ["//:__subpackages__"], +) + +# Stale aliased py_test — deleted_test.py does not exist, should be removed. +# TODO: Known limitation: not fully deleted until gazelle is bumped to include +# https://github.com/bazel-contrib/bazel-gazelle/pull/2362 +my_py_test( + name = "deleted_test", + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/remove_invalid_per_file/alias_kind/bar.py b/gazelle/python/testdata/remove_invalid_per_file/alias_kind/bar.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/remove_invalid_per_file/alias_kind/bar_test.py b/gazelle/python/testdata/remove_invalid_per_file/alias_kind/bar_test.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/remove_invalid_per_file/bar.py b/gazelle/python/testdata/remove_invalid_per_file/bar.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/remove_invalid_per_file/bar_test.py b/gazelle/python/testdata/remove_invalid_per_file/bar_test.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/remove_invalid_per_file/map_kind/BUILD.in b/gazelle/python/testdata/remove_invalid_per_file/map_kind/BUILD.in new file mode 100644 index 0000000000..00b9a60d6f --- /dev/null +++ b/gazelle/python/testdata/remove_invalid_per_file/map_kind/BUILD.in @@ -0,0 +1,37 @@ +load(":mylib.bzl", "my_py_binary", "my_py_test") + +# gazelle:python_generation_mode file +# gazelle:map_kind py_binary my_py_binary :mylib.bzl +# gazelle:map_kind py_test my_py_test :mylib.bzl + +# Valid py_library — bar.py exists on disk, should be kept. +py_library( + name = "bar", + srcs = ["bar.py"], + visibility = ["//:__subpackages__"], +) + +# Stale py_library — deleted.py does not exist, should be removed. +py_library( + name = "deleted_lib", + srcs = ["deleted.py"], + visibility = ["//:__subpackages__"], +) + +# Stale mapped py_binary — deleted_bin.py does not exist, should be removed. +my_py_binary( + name = "deleted_bin", + srcs = ["deleted_bin.py"], +) + +# Valid mapped py_test — bar_test.py exists on disk, should be kept. +my_py_test( + name = "bar_test", + srcs = ["bar_test.py"], +) + +# Stale mapped py_test — deleted_test.py does not exist, should be removed. +my_py_test( + name = "deleted_test", + srcs = ["deleted_test.py"], +) diff --git a/gazelle/python/testdata/remove_invalid_per_file/map_kind/BUILD.out b/gazelle/python/testdata/remove_invalid_per_file/map_kind/BUILD.out new file mode 100644 index 0000000000..84ca905031 --- /dev/null +++ b/gazelle/python/testdata/remove_invalid_per_file/map_kind/BUILD.out @@ -0,0 +1,19 @@ +load("@rules_python//python:defs.bzl", "py_library") +load(":mylib.bzl", "my_py_test") + +# gazelle:python_generation_mode file +# gazelle:map_kind py_binary my_py_binary :mylib.bzl +# gazelle:map_kind py_test my_py_test :mylib.bzl + +# Valid py_library — bar.py exists on disk, should be kept. +py_library( + name = "bar", + srcs = ["bar.py"], + visibility = ["//:__subpackages__"], +) + +# Valid mapped py_test — bar_test.py exists on disk, should be kept. +my_py_test( + name = "bar_test", + srcs = ["bar_test.py"], +) diff --git a/gazelle/python/testdata/remove_invalid_per_file/map_kind/bar.py b/gazelle/python/testdata/remove_invalid_per_file/map_kind/bar.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/remove_invalid_per_file/map_kind/bar_test.py b/gazelle/python/testdata/remove_invalid_per_file/map_kind/bar_test.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/remove_invalid_per_file/test.yaml b/gazelle/python/testdata/remove_invalid_per_file/test.yaml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/gazelle/python/testdata/remove_invalid_per_file/test.yaml @@ -0,0 +1 @@ +--- diff --git a/gazelle/python/testdata/respect_alias_and_map_kind/BUILD.in b/gazelle/python/testdata/respect_alias_and_map_kind/BUILD.in new file mode 100644 index 0000000000..06ebfbdfa3 --- /dev/null +++ b/gazelle/python/testdata/respect_alias_and_map_kind/BUILD.in @@ -0,0 +1,16 @@ +load(":mylib.bzl", "my_py_library") + +# gazelle:alias_kind my_py_library py_library +# gazelle:map_kind py_test my_test :mytest.bzl + +my_py_library( + name = "respect_alias_and_map_kind", + srcs = ["__init__.py"], +) + +my_test( + name = "respect_alias_and_map_kind_test", + srcs = ["__test__.py"], + main = "__test__.py", + deps = [], +) diff --git a/gazelle/python/testdata/respect_alias_and_map_kind/BUILD.out b/gazelle/python/testdata/respect_alias_and_map_kind/BUILD.out new file mode 100644 index 0000000000..6c7eee06e3 --- /dev/null +++ b/gazelle/python/testdata/respect_alias_and_map_kind/BUILD.out @@ -0,0 +1,21 @@ +load(":mylib.bzl", "my_py_library") +load(":mytest.bzl", "my_test") + +# gazelle:alias_kind my_py_library py_library +# gazelle:map_kind py_test my_test :mytest.bzl + +my_py_library( + name = "respect_alias_and_map_kind", + srcs = [ + "__init__.py", + "foo.py", + ], + visibility = ["//:__subpackages__"], +) + +my_test( + name = "respect_alias_and_map_kind_test", + srcs = ["__test__.py"], + main = "__test__.py", + deps = [":respect_alias_and_map_kind"], +) diff --git a/gazelle/python/testdata/respect_alias_and_map_kind/README.md b/gazelle/python/testdata/respect_alias_and_map_kind/README.md new file mode 100644 index 0000000000..5e5ca20d27 --- /dev/null +++ b/gazelle/python/testdata/respect_alias_and_map_kind/README.md @@ -0,0 +1,4 @@ +# Respect `alias_kind` and `map_kind` + +This test case asserts that `# gazelle:alias_kind` and `# gazelle:map_kind` are +both respected as the expected kind at once and do not collide. diff --git a/gazelle/python/testdata/respect_alias_and_map_kind/WORKSPACE b/gazelle/python/testdata/respect_alias_and_map_kind/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/respect_alias_and_map_kind/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/respect_alias_and_map_kind/__init__.py b/gazelle/python/testdata/respect_alias_and_map_kind/__init__.py new file mode 100644 index 0000000000..6a49193fe4 --- /dev/null +++ b/gazelle/python/testdata/respect_alias_and_map_kind/__init__.py @@ -0,0 +1,3 @@ +from foo import foo + +_ = foo diff --git a/gazelle/python/testdata/respect_alias_and_map_kind/__test__.py b/gazelle/python/testdata/respect_alias_and_map_kind/__test__.py new file mode 100644 index 0000000000..d6085a41b4 --- /dev/null +++ b/gazelle/python/testdata/respect_alias_and_map_kind/__test__.py @@ -0,0 +1,12 @@ +import unittest + +from __init__ import foo + + +class FooTest(unittest.TestCase): + def test_foo(self): + self.assertEqual("foo", foo()) + + +if __name__ == "__main__": + unittest.main() diff --git a/examples/pip_repository_annotations/data/copy_executable.py b/gazelle/python/testdata/respect_alias_and_map_kind/foo.py old mode 100755 new mode 100644 similarity index 86% rename from examples/pip_repository_annotations/data/copy_executable.py rename to gazelle/python/testdata/respect_alias_and_map_kind/foo.py index 5cb1af7fdb..3f049df738 --- a/examples/pip_repository_annotations/data/copy_executable.py +++ b/gazelle/python/testdata/respect_alias_and_map_kind/foo.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright 2023 The Bazel Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,5 +13,5 @@ # limitations under the License. -if __name__ == "__main__": - print("Hello world from copied executable") +def foo(): + return "foo" diff --git a/gazelle/python/testdata/respect_alias_and_map_kind/test.yaml b/gazelle/python/testdata/respect_alias_and_map_kind/test.yaml new file mode 100644 index 0000000000..36dd656b39 --- /dev/null +++ b/gazelle/python/testdata/respect_alias_and_map_kind/test.yaml @@ -0,0 +1,3 @@ +--- +expect: + exit_code: 0 diff --git a/gazelle/python/testdata/respect_alias_kind/BUILD.in b/gazelle/python/testdata/respect_alias_kind/BUILD.in new file mode 100644 index 0000000000..3dc5ca7151 --- /dev/null +++ b/gazelle/python/testdata/respect_alias_kind/BUILD.in @@ -0,0 +1,8 @@ +load(":mylib.bzl", "my_py_library") + +# gazelle:alias_kind my_py_library py_library + +my_py_library( + name = "respect_alias_kind", + srcs = ["__init__.py"], +) diff --git a/gazelle/python/testdata/respect_alias_kind/BUILD.out b/gazelle/python/testdata/respect_alias_kind/BUILD.out new file mode 100644 index 0000000000..9e65a95923 --- /dev/null +++ b/gazelle/python/testdata/respect_alias_kind/BUILD.out @@ -0,0 +1,12 @@ +load(":mylib.bzl", "my_py_library") + +# gazelle:alias_kind my_py_library py_library + +my_py_library( + name = "respect_alias_kind", + srcs = [ + "__init__.py", + "foo.py", + ], + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/respect_alias_kind/README.md b/gazelle/python/testdata/respect_alias_kind/README.md new file mode 100644 index 0000000000..d0d8702106 --- /dev/null +++ b/gazelle/python/testdata/respect_alias_kind/README.md @@ -0,0 +1,4 @@ +# Respect `alias_kind` + +This test case asserts that Gazelle updates the existing wrapper-macro rule +declared with `# gazelle:alias_kind` in place instead of reporting a collision. diff --git a/gazelle/python/testdata/respect_alias_kind/WORKSPACE b/gazelle/python/testdata/respect_alias_kind/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/respect_alias_kind/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/respect_alias_kind/__init__.py b/gazelle/python/testdata/respect_alias_kind/__init__.py new file mode 100644 index 0000000000..6a49193fe4 --- /dev/null +++ b/gazelle/python/testdata/respect_alias_kind/__init__.py @@ -0,0 +1,3 @@ +from foo import foo + +_ = foo diff --git a/gazelle/python/testdata/respect_alias_kind/foo.py b/gazelle/python/testdata/respect_alias_kind/foo.py new file mode 100644 index 0000000000..cf68624419 --- /dev/null +++ b/gazelle/python/testdata/respect_alias_kind/foo.py @@ -0,0 +1,2 @@ +def foo(): + return "foo" diff --git a/gazelle/python/testdata/respect_alias_kind/test.yaml b/gazelle/python/testdata/respect_alias_kind/test.yaml new file mode 100644 index 0000000000..36dd656b39 --- /dev/null +++ b/gazelle/python/testdata/respect_alias_kind/test.yaml @@ -0,0 +1,3 @@ +--- +expect: + exit_code: 0 diff --git a/gazelle/python/testdata/respect_kind_mapping/BUILD.in b/gazelle/python/testdata/respect_kind_mapping/BUILD.in index 6a06737623..3c6ec07014 100644 --- a/gazelle/python/testdata/respect_kind_mapping/BUILD.in +++ b/gazelle/python/testdata/respect_kind_mapping/BUILD.in @@ -1,6 +1,7 @@ load("@rules_python//python:defs.bzl", "py_library") # gazelle:map_kind py_test my_test :mytest.bzl +# gazelle:map_kind py_binary my_bin :mytest.bzl py_library( name = "respect_kind_mapping", @@ -13,3 +14,8 @@ my_test( main = "__test__.py", deps = [":respect_kind_mapping"], ) + +my_bin( + name = "my_bin_gets_removed", + srcs = ["__main__.py"], +) diff --git a/gazelle/python/testdata/respect_kind_mapping/BUILD.out b/gazelle/python/testdata/respect_kind_mapping/BUILD.out index fa06e2af12..eb6894f1c8 100644 --- a/gazelle/python/testdata/respect_kind_mapping/BUILD.out +++ b/gazelle/python/testdata/respect_kind_mapping/BUILD.out @@ -2,6 +2,7 @@ load("@rules_python//python:defs.bzl", "py_library") load(":mytest.bzl", "my_test") # gazelle:map_kind py_test my_test :mytest.bzl +# gazelle:map_kind py_binary my_bin :mytest.bzl py_library( name = "respect_kind_mapping", diff --git a/gazelle/python/testdata/simple_test_with_conftest/bar/BUILD.out b/gazelle/python/testdata/simple_test_with_conftest/bar/BUILD.out index 4a1204e989..9b500d4733 100644 --- a/gazelle/python/testdata/simple_test_with_conftest/bar/BUILD.out +++ b/gazelle/python/testdata/simple_test_with_conftest/bar/BUILD.out @@ -23,5 +23,6 @@ py_test( deps = [ ":bar", ":conftest", + "//:conftest", ], ) diff --git a/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/bar/BUILD.out b/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/bar/BUILD.out index ef8591f199..25a2d39672 100644 --- a/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/bar/BUILD.out +++ b/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/bar/BUILD.out @@ -22,6 +22,7 @@ py_test( main = "__test__.py", deps = [ ":conftest", + "//:conftest", "//:simple_test_with_conftest_sibling_imports_disabled", ], ) diff --git a/gazelle/pythonconfig/BUILD.bazel b/gazelle/pythonconfig/BUILD.bazel index 711bf2eb42..a82a7f19ca 100644 --- a/gazelle/pythonconfig/BUILD.bazel +++ b/gazelle/pythonconfig/BUILD.bazel @@ -10,8 +10,8 @@ go_library( visibility = ["//visibility:public"], deps = [ "//manifest", - "@bazel_gazelle//label:go_default_library", - "@com_github_emirpasic_gods//lists/singlylinkedlist", + "@bazel_gazelle//label", + "@com_github_emirpasic_gods//lists/singlylinkedlist:go_default_library", ], ) diff --git a/gazelle/pythonconfig/pythonconfig.go b/gazelle/pythonconfig/pythonconfig.go index ed9b914e82..c88d59abcf 100644 --- a/gazelle/pythonconfig/pythonconfig.go +++ b/gazelle/pythonconfig/pythonconfig.go @@ -102,8 +102,12 @@ const ( ExperimentalAllowRelativeImports = "python_experimental_allow_relative_imports" // GeneratePyiDeps represents the directive that controls whether to generate // separate pyi_deps attribute or merge type-checking dependencies into deps. - // Defaults to false for backward compatibility. + // Defaults to true. GeneratePyiDeps = "python_generate_pyi_deps" + // GeneratePyiSrcs represents the directive that controls whether to include + // a pyi_srcs attribute if a sibling .pyi file is found. + // Defaults to true. + GeneratePyiSrcs = "python_generate_pyi_srcs" // GenerateProto represents the directive that controls whether to generate // python_generate_proto targets. GenerateProto = "python_generate_proto" @@ -112,6 +116,15 @@ const ( // like "import a" can be resolved to sibling modules. When disabled, they // can only be resolved as an absolute import. PythonResolveSiblingImports = "python_resolve_sibling_imports" + // PythonIncludeAncestorConftest represents the directive that controls + // whether ancestor conftest.py files are added as dependencies to py_test + // targets. When enabled (the default), ancestor conftest.py files are + // included as deps. + // See also https://github.com/bazel-contrib/rules_python/pull/3498, which + // fixed previous behavior that was incorrectly _not_ adding the files and + // https://github.com/bazel-contrib/rules_python/issues/3595 which requested + // that the behavior be configurable. + PythonIncludeAncestorConftest = "python_include_ancestor_conftest" ) // GenerationModeType represents one of the generation modes for the Python @@ -202,8 +215,10 @@ type Config struct { labelNormalization LabelNormalizationType experimentalAllowRelativeImports bool generatePyiDeps bool + generatePyiSrcs bool generateProto bool resolveSiblingImports bool + includeAncestorConftest bool } type LabelNormalizationType int @@ -241,9 +256,11 @@ func New( labelConvention: DefaultLabelConvention, labelNormalization: DefaultLabelNormalizationType, experimentalAllowRelativeImports: false, - generatePyiDeps: false, + generatePyiDeps: true, + generatePyiSrcs: true, generateProto: false, resolveSiblingImports: false, + includeAncestorConftest: true, } } @@ -279,8 +296,10 @@ func (c *Config) NewChild() *Config { labelNormalization: c.labelNormalization, experimentalAllowRelativeImports: c.experimentalAllowRelativeImports, generatePyiDeps: c.generatePyiDeps, + generatePyiSrcs: c.generatePyiSrcs, generateProto: c.generateProto, resolveSiblingImports: c.resolveSiblingImports, + includeAncestorConftest: c.includeAncestorConftest, } } @@ -590,6 +609,18 @@ func (c *Config) GeneratePyiDeps() bool { return c.generatePyiDeps } +// SetGeneratePyiSrcs sets whether pyi_srcs attribute should be generated if a sibling +// .pyi file is found. +func (c *Config) SetGeneratePyiSrcs(generatePyiSrcs bool) { + c.generatePyiSrcs = generatePyiSrcs +} + +// GeneratePyiSrcs returns whether pyi_srcs attribute should be generated if a sibling +// .pyi file is found. +func (c *Config) GeneratePyiSrcs() bool { + return c.generatePyiSrcs +} + // SetGenerateProto sets whether py_proto_library should be generated for proto_library. func (c *Config) SetGenerateProto(generateProto bool) { c.generateProto = generateProto @@ -610,6 +641,16 @@ func (c *Config) ResolveSiblingImports() bool { return c.resolveSiblingImports } +// SetIncludeAncestorConftest sets whether ancestor conftest files are added to py_test targets. +func (c *Config) SetIncludeAncestorConftest(includeAncestorConftest bool) { + c.includeAncestorConftest = includeAncestorConftest +} + +// IncludeAncestorConftest returns whether ancestor conftest files are added to py_test targets. +func (c *Config) IncludeAncestorConftest() bool { + return c.includeAncestorConftest +} + // FormatThirdPartyDependency returns a label to a third-party dependency performing all formating and normalization. func (c *Config) FormatThirdPartyDependency(repositoryName string, distributionName string) label.Label { conventionalDistributionName := strings.ReplaceAll(c.labelConvention, distributionNameLabelConventionSubstitution, distributionName) diff --git a/internal_dev_deps.bzl b/internal_dev_deps.bzl index 91f5defd3e..7ab4717236 100644 --- a/internal_dev_deps.bzl +++ b/internal_dev_deps.bzl @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Dependencies that are needed for development and testing of rules_python itself.""" +"""Dependencies that are needed for development and testing of rules_python itself in WORKSPACE mode.""" load("@bazel_tools//tools/build_defs/repo:http.bzl", _http_archive = "http_archive", _http_file = "http_file") load("@bazel_tools//tools/build_defs/repo:local.bzl", "local_repository") @@ -34,7 +34,7 @@ def http_file(name, **kwargs): ) def rules_python_internal_deps(): - """Fetches all required dependencies for developing/testing rules_python itself. + """Fetches all required dependencies for developing/testing rules_python itself in WORKSPACE mode. Setup of these dependencies is done by `internal_dev_setup.bzl` @@ -48,6 +48,13 @@ def rules_python_internal_deps(): ], ) + # Sphinxdocs doesn't support workspace mode, but we have to define it + # so that load() passes. + local_repository( + name = "sphinxdocs", + path = "sphinxdocs", + ) + local_repository( name = "other", path = "tests/modules/other", @@ -60,10 +67,10 @@ def rules_python_internal_deps(): http_archive( name = "bazel_skylib", - sha256 = "bc283cdfcd526a52c3201279cda4bc298652efa898b10b4db0837dc51652756f", + sha256 = "6e78f0e57de26801f6f564fa7c4a48dc8b36873e416257a92bbb0937eeac8446", urls = [ - "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.7.1/bazel-skylib-1.7.1.tar.gz", - "https://github.com/bazelbuild/bazel-skylib/releases/download/1.7.1/bazel-skylib-1.7.1.tar.gz", + "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.8.2/bazel-skylib-1.8.2.tar.gz", + "https://github.com/bazelbuild/bazel-skylib/releases/download/1.8.2/bazel-skylib-1.8.2.tar.gz", ], ) diff --git a/internal_dev_setup.bzl b/internal_dev_setup.bzl index c37c59a5da..0bbcd97748 100644 --- a/internal_dev_setup.bzl +++ b/internal_dev_setup.bzl @@ -26,6 +26,8 @@ load("//python:versions.bzl", "MINOR_MAPPING", "TOOL_VERSIONS") load("//python/private:pythons_hub.bzl", "hub_repo") # buildifier: disable=bzl-visibility load("//python/private:runtime_env_repo.bzl", "runtime_env_repo") # buildifier: disable=bzl-visibility load("//python/private/pypi:deps.bzl", "pypi_deps") # buildifier: disable=bzl-visibility +load("//python/private/pypi:whl_library.bzl", "whl_library") # buildifier: disable=bzl-visibility +load("//tests/support/whl_from_dir:whl_from_dir_repo.bzl", "whl_from_dir_repo") # buildifier: disable=bzl-visibility def rules_python_internal_setup(): """Setup for development and testing of rules_python itself.""" @@ -59,3 +61,25 @@ def rules_python_internal_setup(): bazel_features_deps() rules_shell_dependencies() rules_shell_toolchains() + + whl_from_dir_repo( + name = "whl_with_data1_whl", + root = "//tests/repos/whl_with_data1:BUILD.bazel", + output = "whl_with_data1-1.0-any-none-any.whl", + ) + whl_library( + name = "whl_with_data1", + whl_file = "@whl_with_data1_whl//:whl_with_data1-1.0-any-none-any.whl", + requirement = "whl-with-data1", + ) + + whl_from_dir_repo( + name = "whl_with_data2_whl", + root = "//tests/repos/whl_with_data2:BUILD.bazel", + output = "whl_with_data2-1.0-any-none-any.whl", + ) + whl_library( + name = "whl_with_data2", + whl_file = "@whl_with_data2_whl//:whl_with_data2-1.0-any-none-any.whl", + requirement = "whl-with-data2", + ) diff --git a/news/.gitkeep b/news/.gitkeep new file mode 100644 index 0000000000..35e8e662aa --- /dev/null +++ b/news/.gitkeep @@ -0,0 +1 @@ +# Keep this directory even if empty so git tracks it. diff --git a/news/3514.added.md b/news/3514.added.md new file mode 100644 index 0000000000..bf2a119380 --- /dev/null +++ b/news/3514.added.md @@ -0,0 +1 @@ +(pip,python) Added `pyproject_toml` attribute to {obj}`pip.default`, {obj}`pip.parse` and {obj}`python.defaults` to read the default Python version from the `requires-python` field of `pyproject.toml`. diff --git a/news/3825.added.md b/news/3825.added.md new file mode 100644 index 0000000000..71b20aca91 --- /dev/null +++ b/news/3825.added.md @@ -0,0 +1,6 @@ +(py_test) Added an opt-in safeguard against `py_test` targets that silently +pass without running any tests. Set +{obj}`--@rules_python//python/config_settings:validate_test_main=enabled` to +fail the build when a test's main module only contains inert top-level +statements (definitions, imports, assignments) and never invokes a test +runner ([#3824](https://github.com/bazel-contrib/rules_python/issues/3824)). diff --git a/news/3858.fixed.md b/news/3858.fixed.md new file mode 100644 index 0000000000..dd991e4654 --- /dev/null +++ b/news/3858.fixed.md @@ -0,0 +1,3 @@ +(compile_pip_requirements) Add the explicit `data` attribute and forward it +directly to the generated `py_binary`, so files passed via `data` can be +referenced from `extra_args` using `$(location ...)`. diff --git a/news/3906.fixed.md b/news/3906.fixed.md new file mode 100644 index 0000000000..64a16fd9c8 --- /dev/null +++ b/news/3906.fixed.md @@ -0,0 +1,3 @@ +(pypi) correctly parse the `index_url` for each wheel so that the source registry is forwarded to +the {obj}`whl_library`. This is so that the `purl` for `package_metadata` can be correctly +constructed. diff --git a/news/3919.fixed.md b/news/3919.fixed.md new file mode 100644 index 0000000000..0e45d3ce9c --- /dev/null +++ b/news/3919.fixed.md @@ -0,0 +1,3 @@ +Fixed `py_binary_rule_builder()` / `py_test_rule_builder()` (from `python/api/executables.bzl`) +failing at analysis time with a visibility error when used to construct a custom rule from an +external module. diff --git a/news/BUILD.bazel b/news/BUILD.bazel new file mode 100644 index 0000000000..79b48675ca --- /dev/null +++ b/news/BUILD.bazel @@ -0,0 +1,6 @@ +package(default_visibility = ["//:__subpackages__"]) + +filegroup( + name = "news_files", + srcs = glob(["**"]), +) diff --git a/python/BUILD.bazel b/python/BUILD.bazel index 76fa5dde6e..a4dd98c572 100644 --- a/python/BUILD.bazel +++ b/python/BUILD.bazel @@ -24,6 +24,10 @@ In an ideal renaming, we'd move the packaging rules to a different package so that @rules_python//python is only concerned with the core rules. """ +# TODO: make current_py_toolchain.bzl private +# gazelle:resolve starlark @rules_python//python:current_py_toolchain.bzl //python:current_py_toolchain_bzl +# gazelle:resolve starlark //python:current_py_toolchain.bzl //python:current_py_toolchain_bzl + load("@bazel_skylib//:bzl_library.bzl", "bzl_library") load(":current_py_toolchain.bzl", "current_py_toolchain") @@ -47,207 +51,13 @@ filegroup( "//python/runfiles:distribution", "//python/runtime_env_toolchains:distribution", "//python/uv:distribution", + "//python/zipapp:distribution", ], visibility = ["//:__pkg__"], ) # ========= bzl_library targets end ========= -bzl_library( - name = "current_py_toolchain_bzl", - srcs = ["current_py_toolchain.bzl"], - deps = ["//python/private:toolchain_types_bzl"], -) - -bzl_library( - name = "defs_bzl", - srcs = [ - "defs.bzl", - ], - visibility = ["//visibility:public"], - deps = [ - ":current_py_toolchain_bzl", - ":py_binary_bzl", - ":py_import_bzl", - ":py_info_bzl", - ":py_library_bzl", - ":py_runtime_bzl", - ":py_runtime_info_bzl", - ":py_runtime_pair_bzl", - ":py_test_bzl", - ], -) - -bzl_library( - name = "features_bzl", - srcs = ["features.bzl"], - deps = [ - "@rules_python_internal//:rules_python_config_bzl", - ], -) - -bzl_library( - name = "packaging_bzl", - srcs = ["packaging.bzl"], - deps = [ - ":py_binary_bzl", - "//python/private:bzlmod_enabled_bzl", - "//python/private:py_package_bzl", - "//python/private:py_wheel_bzl", - "//python/private:util_bzl", - "@bazel_skylib//rules:native_binary", - ], -) - -bzl_library( - name = "pip_bzl", - srcs = ["pip.bzl"], - deps = [ - "//python/private:normalize_name_bzl", - "//python/private/pypi:multi_pip_parse_bzl", - "//python/private/pypi:package_annotation_bzl", - "//python/private/pypi:pip_compile_bzl", - "//python/private/pypi:pip_repository_bzl", - "//python/private/pypi:whl_library_alias_bzl", - "//python/private/whl_filegroup:whl_filegroup_bzl", - ], -) - -bzl_library( - name = "proto_bzl", - srcs = [ - "proto.bzl", - ], - visibility = ["//visibility:public"], - deps = [ - "@com_google_protobuf//bazel:py_proto_library_bzl", - ], -) - -bzl_library( - name = "py_binary_bzl", - srcs = ["py_binary.bzl"], - deps = [ - "//python/private:py_binary_macro_bzl", - "//python/private:register_extension_info_bzl", - "//python/private:util_bzl", - "@rules_python_internal//:rules_python_config_bzl", - ], -) - -bzl_library( - name = "py_cc_link_params_info_bzl", - srcs = ["py_cc_link_params_info.bzl"], - deps = [ - "//python/private:py_cc_link_params_info_bzl", - "@rules_python_internal//:rules_python_config_bzl", - ], -) - -bzl_library( - name = "py_exec_tools_info_bzl", - srcs = ["py_exec_tools_info.bzl"], - deps = ["//python/private:py_exec_tools_info_bzl"], -) - -bzl_library( - name = "py_exec_tools_toolchain_bzl", - srcs = ["py_exec_tools_toolchain.bzl"], - deps = ["//python/private:py_exec_tools_toolchain_bzl"], -) - -bzl_library( - name = "py_executable_info_bzl", - srcs = ["py_executable_info.bzl"], - deps = ["//python/private:py_executable_info_bzl"], -) - -bzl_library( - name = "py_import_bzl", - srcs = ["py_import.bzl"], - deps = [":py_info_bzl"], -) - -bzl_library( - name = "py_info_bzl", - srcs = ["py_info.bzl"], - deps = [ - "//python/private:py_info_bzl", - "//python/private:reexports_bzl", - "@rules_python_internal//:rules_python_config_bzl", - ], -) - -bzl_library( - name = "py_library_bzl", - srcs = ["py_library.bzl"], - deps = [ - "//python/private:py_library_macro_bzl", - "//python/private:register_extension_info_bzl", - "//python/private:util_bzl", - "@rules_python_internal//:rules_python_config_bzl", - ], -) - -bzl_library( - name = "py_runtime_bzl", - srcs = ["py_runtime.bzl"], - deps = [ - "//python/private:py_runtime_macro_bzl", - "//python/private:util_bzl", - ], -) - -bzl_library( - name = "py_runtime_pair_bzl", - srcs = ["py_runtime_pair.bzl"], - deps = [ - "//python/private:bazel_tools_bzl", - "//python/private:py_runtime_pair_macro_bzl", - "//python/private:util_bzl", - ], -) - -bzl_library( - name = "py_runtime_info_bzl", - srcs = ["py_runtime_info.bzl"], - deps = [ - "//python/private:py_runtime_info_bzl", - "//python/private:reexports_bzl", - "@rules_python_internal//:rules_python_config_bzl", - ], -) - -bzl_library( - name = "py_test_bzl", - srcs = ["py_test.bzl"], - deps = [ - "//python/private:py_test_macro_bzl", - "//python/private:register_extension_info_bzl", - "//python/private:util_bzl", - "@rules_python_internal//:rules_python_config_bzl", - ], -) - -bzl_library( - name = "repositories_bzl", - srcs = ["repositories.bzl"], - deps = [ - "//python/private:is_standalone_interpreter_bzl", - "//python/private:py_repositories_bzl", - "//python/private:python_register_multi_toolchains_bzl", - "//python/private:python_register_toolchains_bzl", - "//python/private:python_repository_bzl", - ], -) - -bzl_library( - name = "versions_bzl", - srcs = ["versions.bzl"], - visibility = ["//:__subpackages__"], - deps = ["//python/private:platform_info_bzl"], -) - # NOTE: Remember to add bzl_library targets to //tests:bzl_libraries # ========= bzl_library targets end ========= @@ -367,3 +177,286 @@ exports_files([ current_py_toolchain( name = "current_py_toolchain", ) + +# Keep this target because it is a public API and Gazelle might otherwise +# remove it if it is not loaded by other bzl_library targets. +# keep +bzl_library( + name = "current_py_toolchain_bzl", + srcs = ["current_py_toolchain.bzl"], + visibility = ["//visibility:public"], +) + +bzl_library( + name = "defs", + srcs = ["defs.bzl"], + deps = [ + ":py_binary", + ":py_import", + ":py_info", + ":py_library", + ":py_runtime", + ":py_runtime_info", + ":py_runtime_pair", + ":py_test", + "//python:current_py_toolchain_bzl", + ], +) + +alias( + name = "defs_bzl", + actual = ":defs", + deprecation = "Use //python:defs instead", +) + +alias( + name = "features_bzl", + actual = ":features", + deprecation = "Use //python:features instead", +) + +bzl_library( + name = "packaging", + srcs = ["packaging.bzl"], + deps = [ + ":py_binary", + "//python/private:bzlmod_enabled", + "//python/private:py_package", + "//python/private:py_wheel", + "//python/private:util", + "@bazel_skylib//rules:native_binary", + ], +) + +alias( + name = "packaging_bzl", + actual = ":packaging", + deprecation = "Use //python:packaging instead", +) + +bzl_library( + name = "pip", + srcs = ["pip.bzl"], + deps = [ + "//python/private:normalize_name", + "//python/private/pypi:multi_pip_parse", + "//python/private/pypi:package_annotation", + "//python/private/pypi:pip_compile", + "//python/private/pypi:pip_repository", + "//python/private/pypi:whl_library_alias", + "//python/private/whl_filegroup", + ], +) + +alias( + name = "pip_bzl", + actual = ":pip", + deprecation = "Use //python:pip instead", +) + +bzl_library( + name = "proto", + srcs = ["proto.bzl"], + deps = ["@com_google_protobuf//bazel:py_proto_library_bzl"], +) + +alias( + name = "proto_bzl", + actual = ":proto", + deprecation = "Use //python:proto instead", +) + +bzl_library( + name = "py_binary", + srcs = ["py_binary.bzl"], + deps = ["//python/private:py_binary_macro"], +) + +alias( + name = "py_binary_bzl", + actual = ":py_binary", + deprecation = "Use //python:py_binary instead", +) + +bzl_library( + name = "py_cc_link_params_info", + srcs = ["py_cc_link_params_info.bzl"], + deps = ["//python/private:py_cc_link_params_info"], +) + +alias( + name = "py_cc_link_params_info_bzl", + actual = ":py_cc_link_params_info", + deprecation = "Use //python:py_cc_link_params_info instead", +) + +bzl_library( + name = "py_exec_tools_info", + srcs = ["py_exec_tools_info.bzl"], + deps = ["//python/private:py_exec_tools_info"], +) + +alias( + name = "py_exec_tools_info_bzl", + actual = ":py_exec_tools_info", + deprecation = "Use //python:py_exec_tools_info instead", +) + +bzl_library( + name = "py_exec_tools_toolchain", + srcs = ["py_exec_tools_toolchain.bzl"], + deps = ["//python/private:py_exec_tools_toolchain"], +) + +alias( + name = "py_exec_tools_toolchain_bzl", + actual = ":py_exec_tools_toolchain", + deprecation = "Use //python:py_exec_tools_toolchain instead", +) + +bzl_library( + name = "py_executable_info", + srcs = ["py_executable_info.bzl"], + deps = ["//python/private:py_executable_info"], +) + +alias( + name = "py_executable_info_bzl", + actual = ":py_executable_info", + deprecation = "Use //python:py_executable_info instead", +) + +bzl_library( + name = "py_import", + srcs = ["py_import.bzl"], + deps = [":py_info"], +) + +alias( + name = "py_import_bzl", + actual = ":py_import", + deprecation = "Use //python:py_import instead", +) + +bzl_library( + name = "py_info", + srcs = ["py_info.bzl"], + deps = ["//python/private:py_info"], +) + +alias( + name = "py_info_bzl", + actual = ":py_info", + deprecation = "Use //python:py_info instead", +) + +bzl_library( + name = "py_library", + srcs = ["py_library.bzl"], + deps = ["//python/private:py_library_macro"], +) + +alias( + name = "py_library_bzl", + actual = ":py_library", + deprecation = "Use //python:py_library instead", +) + +bzl_library( + name = "py_runtime", + srcs = ["py_runtime.bzl"], + deps = ["//python/private:py_runtime_macro"], +) + +alias( + name = "py_runtime_bzl", + actual = ":py_runtime", + deprecation = "Use //python:py_runtime instead", +) + +bzl_library( + name = "py_runtime_info", + srcs = ["py_runtime_info.bzl"], + deps = ["//python/private:py_runtime_info"], +) + +alias( + name = "py_runtime_info_bzl", + actual = ":py_runtime_info", + deprecation = "Use //python:py_runtime_info instead", +) + +bzl_library( + name = "py_runtime_pair", + srcs = ["py_runtime_pair.bzl"], + deps = ["//python/private:py_runtime_pair_macro"], +) + +alias( + name = "py_runtime_pair_bzl", + actual = ":py_runtime_pair", + deprecation = "Use //python:py_runtime_pair instead", +) + +# keep +bzl_library( + name = "py_test", + srcs = ["py_test.bzl"], + deps = ["//python/private:py_test_macro"], +) + +alias( + name = "py_test_bzl", + actual = ":py_test", + deprecation = "Use //python:py_test instead", +) + +alias( + name = "python_bzl", + actual = ":python", + deprecation = "Use //python:python instead", +) + +bzl_library( + name = "repositories", + srcs = ["repositories.bzl"], + deps = [ + "//python/private:is_standalone_interpreter", + "//python/private:py_repositories", + "//python/private:python_register_multi_toolchains", + "//python/private:python_register_toolchains", + "//python/private:python_repository", + ], +) + +alias( + name = "repositories_bzl", + actual = ":repositories", + deprecation = "Use //python:repositories instead", +) + +bzl_library( + name = "versions", + srcs = ["versions.bzl"], + deps = [ + "//python/private:pbs_manifest", + "//python/private:platform_info", + "//python/private:runtimes_manifest_workspace", + ], +) + +alias( + name = "versions_bzl", + actual = ":versions", + deprecation = "Use //python:versions instead", +) + +bzl_library( + name = "features", + srcs = ["features.bzl"], +) + +bzl_library( + name = "python", + srcs = ["python.bzl"], +) diff --git a/python/api/BUILD.bazel b/python/api/BUILD.bazel index 11fee103cb..93c70f4c8b 100644 --- a/python/api/BUILD.bazel +++ b/python/api/BUILD.bazel @@ -18,46 +18,41 @@ package( default_visibility = ["//:__subpackages__"], ) +filegroup( + name = "distribution", + srcs = glob(["**"]), +) + bzl_library( - name = "api_bzl", + name = "api", srcs = ["api.bzl"], - visibility = ["//visibility:public"], - deps = ["//python/private/api:api_bzl"], + deps = ["//python/private/api"], ) bzl_library( - name = "attr_builders_bzl", + name = "attr_builders", srcs = ["attr_builders.bzl"], - deps = ["//python/private:attr_builders_bzl"], + deps = ["//python/private:attr_builders"], ) bzl_library( - name = "executables_bzl", + name = "executables", srcs = ["executables.bzl"], - visibility = ["//visibility:public"], deps = [ - "//python/private:py_binary_rule_bzl", - "//python/private:py_executable_bzl", - "//python/private:py_test_rule_bzl", + "//python/private:py_binary_rule", + "//python/private:py_executable", + "//python/private:py_test_rule", ], ) bzl_library( - name = "libraries_bzl", + name = "libraries", srcs = ["libraries.bzl"], - visibility = ["//visibility:public"], - deps = [ - "//python/private:py_library_bzl", - ], + deps = ["//python/private:py_library"], ) bzl_library( - name = "rule_builders_bzl", + name = "rule_builders", srcs = ["rule_builders.bzl"], - deps = ["//python/private:rule_builders_bzl"], -) - -filegroup( - name = "distribution", - srcs = glob(["**"]), + deps = ["//python/private:rule_builders"], ) diff --git a/python/bin/repl_stub.py b/python/bin/repl_stub.py index f5b7c0aa4f..858cf810b9 100644 --- a/python/bin/repl_stub.py +++ b/python/bin/repl_stub.py @@ -16,9 +16,9 @@ # Capture the globals from PYTHONSTARTUP so we can pass them on to the console. console_locals = globals().copy() -import code -import rlcompleter -import sys +import code # noqa: E402 +import rlcompleter # noqa: E402 +import sys # noqa: E402 class DynamicCompleter(rlcompleter.Completer): @@ -62,10 +62,7 @@ def complete(self, text, state): elif "GNU readline" in readline.__doc__: # type: ignore readline.parse_and_bind("tab: complete") else: - print( - "Could not enable tab completion: " - "unable to determine readline backend" - ) + print("Could not enable tab completion: unable to determine readline backend") except ImportError: print( "Could not enable tab completion: " diff --git a/python/cc/BUILD.bazel b/python/cc/BUILD.bazel index f7686c41f6..f17805d92f 100644 --- a/python/cc/BUILD.bazel +++ b/python/cc/BUILD.bazel @@ -47,21 +47,35 @@ toolchain_type( visibility = ["//visibility:public"], ) +filegroup( + name = "distribution", + srcs = glob(["**"]), +) + bzl_library( - name = "py_cc_toolchain_bzl", + name = "py_cc_toolchain", srcs = ["py_cc_toolchain.bzl"], visibility = ["//visibility:public"], - deps = ["//python/private:py_cc_toolchain_macro_bzl"], + deps = ["//python/private:py_cc_toolchain_macro"], ) bzl_library( - name = "py_cc_toolchain_info_bzl", + name = "py_cc_toolchain_info", srcs = ["py_cc_toolchain_info.bzl"], visibility = ["//visibility:public"], - deps = ["//python/private:py_cc_toolchain_info_bzl"], + deps = ["//python/private:py_cc_toolchain_info"], ) -filegroup( - name = "distribution", - srcs = glob(["**"]), +alias( + name = "py_cc_toolchain_bzl", + actual = ":py_cc_toolchain", + deprecation = "Use //python/cc:py_cc_toolchain instead", + visibility = ["//visibility:public"], +) + +alias( + name = "py_cc_toolchain_info_bzl", + actual = ":py_cc_toolchain_info", + deprecation = "Use //python/cc:py_cc_toolchain_info instead", + visibility = ["//visibility:public"], ) diff --git a/python/cc/py_extension.bzl b/python/cc/py_extension.bzl new file mode 100644 index 0000000000..72a9ce843c --- /dev/null +++ b/python/cc/py_extension.bzl @@ -0,0 +1,8 @@ +"""Public API for py_extension.""" + +load( + "//python/private/cc:py_extension_macro.bzl", + _py_extension = "py_extension", +) + +py_extension = _py_extension diff --git a/python/config_settings/BUILD.bazel b/python/config_settings/BUILD.bazel index cc5c472fe7..d92aa4261c 100644 --- a/python/config_settings/BUILD.bazel +++ b/python/config_settings/BUILD.bazel @@ -1,5 +1,6 @@ load("@bazel_skylib//rules:common_settings.bzl", "bool_flag", "string_flag") load("@pythons_hub//:versions.bzl", "DEFAULT_PYTHON_VERSION", "MINOR_MAPPING", "PYTHON_VERSIONS") +load("@rules_python_internal//:rules_python_config.bzl", "config") load( "//python/private:flags.bzl", "AddSrcsToRunfilesFlag", @@ -9,18 +10,20 @@ load( "LibcFlag", "PrecompileFlag", "PrecompileSourceRetentionFlag", + "ValidateTestMainFlag", "VenvsSitePackages", "VenvsUseDeclareSymlinkFlag", rp_string_flag = "string_flag", ) -load( - "//python/private/pypi:flags.bzl", - "UniversalWhlFlag", - "UseWhlFlag", - "define_pypi_internal_flags", -) +load("//python/private:visibility.bzl", "NOT_ACTUALLY_PUBLIC") # buildifier: disable=bzl-visibility +load("//python/private/pypi:flags.bzl", "define_pypi_internal_flags") load(":config_settings.bzl", "construct_config_settings") +# We don't generate bzl_library for these because they aren't public targets +# and should be moved +# gazelle:exclude config_settings.bzl +# gazelle:exclude transition.bzl + filegroup( name = "distribution", srcs = glob(["**"]) + [ @@ -33,10 +36,6 @@ construct_config_settings( name = "construct_config_settings", default_version = DEFAULT_PYTHON_VERSION, documented_flags = [ - ":pip_whl", - ":pip_whl_glibc_version", - ":pip_whl_muslc_version", - ":pip_whl_osx_arch", ":pip_whl_osx_version", ":py_freethreaded", ":py_linux_libc", @@ -50,7 +49,7 @@ string_flag( build_setting_default = AddSrcsToRunfilesFlag.AUTO, values = AddSrcsToRunfilesFlag.flag_values(), # NOTE: Only public because it is dependency of public rules. - visibility = ["//visibility:public"], + visibility = NOT_ACTUALLY_PUBLIC, ) string_flag( @@ -69,7 +68,7 @@ config_setting( }, # NOTE: Only public because it is used in py_toolchain_suite from toolchain # repositories - visibility = ["//visibility:public"], + visibility = NOT_ACTUALLY_PUBLIC, ) string_flag( @@ -77,7 +76,15 @@ string_flag( build_setting_default = PrecompileFlag.AUTO, values = sorted(PrecompileFlag.__members__.values()), # NOTE: Only public because it's an implicit dependency - visibility = ["//visibility:public"], + visibility = NOT_ACTUALLY_PUBLIC, +) + +string_flag( + name = "validate_test_main", + build_setting_default = ValidateTestMainFlag.AUTO, + values = ValidateTestMainFlag.flag_values(), + # NOTE: Only public because it's an implicit dependency of py_test. + visibility = NOT_ACTUALLY_PUBLIC, ) string_flag( @@ -85,7 +92,7 @@ string_flag( build_setting_default = PrecompileSourceRetentionFlag.AUTO, values = sorted(PrecompileSourceRetentionFlag.__members__.values()), # NOTE: Only public because it's an implicit dependency - visibility = ["//visibility:public"], + visibility = NOT_ACTUALLY_PUBLIC, ) rp_string_flag( @@ -98,6 +105,12 @@ rp_string_flag( }), values = sorted(BootstrapImplFlag.__members__.values()), # NOTE: Only public because it's an implicit dependency + visibility = NOT_ACTUALLY_PUBLIC, +) + +label_flag( + name = "debugger", + build_setting_default = "//python/private:empty", visibility = ["//visibility:public"], ) @@ -115,7 +128,7 @@ string_flag( build_setting_default = LibcFlag.GLIBC, values = LibcFlag.flag_values(), # NOTE: Only public because it is used in pip hub and toolchain repos. - visibility = ["//visibility:public"], + visibility = NOT_ACTUALLY_PUBLIC, ) string_flag( @@ -151,83 +164,33 @@ string_flag( # pip.parse related flags string_flag( - name = "pip_whl", - build_setting_default = UseWhlFlag.AUTO, - values = sorted(UseWhlFlag.__members__.values()), - # NOTE: Only public because it is used in pip hub repos. - visibility = ["//visibility:public"], -) - -config_setting( - name = "is_pip_whl_auto", - flag_values = { - ":pip_whl": UseWhlFlag.AUTO, - }, - # NOTE: Only public because it is used in pip hub repos. - visibility = ["//visibility:public"], -) - -config_setting( - name = "is_pip_whl_no", - flag_values = { - ":pip_whl": UseWhlFlag.NO, - }, - # NOTE: Only public because it is used in pip hub repos. - visibility = ["//visibility:public"], -) - -config_setting( - name = "is_pip_whl_only", - flag_values = { - ":pip_whl": UseWhlFlag.ONLY, - }, - # NOTE: Only public because it is used in pip hub repos. - visibility = ["//visibility:public"], -) - -string_flag( - name = "pip_whl_osx_arch", - build_setting_default = UniversalWhlFlag.ARCH, - values = sorted(UniversalWhlFlag.__members__.values()), - # NOTE: Only public because it is used in pip hub repos. - visibility = ["//visibility:public"], -) - -string_flag( - name = "pip_whl_glibc_version", - build_setting_default = "", - # NOTE: Only public because it is used in pip hub repos. - visibility = ["//visibility:public"], -) - -string_flag( - name = "pip_whl_muslc_version", - build_setting_default = "", - # NOTE: Only public because it is used in pip hub repos. - visibility = ["//visibility:public"], + name = "venv", + build_setting_default = "auto", + # NOTE: Only public because it is used in pip hub repos and executable transitions. + visibility = NOT_ACTUALLY_PUBLIC, ) string_flag( name = "pip_whl_osx_version", build_setting_default = "", # NOTE: Only public because it is used in pip hub repos. - visibility = ["//visibility:public"], + visibility = NOT_ACTUALLY_PUBLIC, ) string_flag( name = "venvs_site_packages", build_setting_default = VenvsSitePackages.NO, # NOTE: Only public because it is used in pip hub repos. - visibility = ["//visibility:public"], + visibility = NOT_ACTUALLY_PUBLIC, ) config_setting( - name = "is_venvs_site_packages", + name = "_is_venvs_site_packages_yes", flag_values = { ":venvs_site_packages": VenvsSitePackages.YES, }, # NOTE: Only public because it is used in whl_library repos. - visibility = ["//visibility:public"], + visibility = NOT_ACTUALLY_PUBLIC, ) define_pypi_internal_flags( @@ -238,11 +201,34 @@ label_flag( name = "pip_env_marker_config", build_setting_default = ":_pip_env_marker_default_config", # NOTE: Only public because it is used in pip hub repos. - visibility = ["//visibility:public"], + visibility = NOT_ACTUALLY_PUBLIC, ) bool_flag( name = "experimental_python_import_all_repositories", build_setting_default = True, + scope = "universal", + visibility = ["//visibility:public"], +) + +bool_flag( + name = "build_python_zip", + build_setting_default = config.build_python_zip_default, + help = "Build python executable zip. Defaults to on on Windows, off on other platforms", + scope = "universal", + visibility = ["//visibility:public"], +) + +bool_flag( + name = "incompatible_default_to_explicit_init_py", + build_setting_default = False, + scope = "universal", + visibility = ["//visibility:public"], +) + +string_flag( + name = "python_path", + build_setting_default = "python", + scope = "universal", visibility = ["//visibility:public"], ) diff --git a/python/config_settings/private/py_args.bzl b/python/config_settings/private/py_args.bzl deleted file mode 100644 index 09a26461b7..0000000000 --- a/python/config_settings/private/py_args.bzl +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright 2023 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. - -"""A helper to extract default args for the transition rule.""" - -def py_args(name, kwargs): - """A helper to extract common py_binary and py_test args - - See https://bazel.build/reference/be/python#py_binary and - https://bazel.build/reference/be/python#py_test for the list - that should be returned - - Args: - name: The name of the target. - kwargs: The kwargs to be extracted from; MODIFIED IN-PLACE. - - Returns: - A dict with the extracted arguments - """ - return dict( - args = kwargs.pop("args", None), - data = kwargs.pop("data", None), - env = kwargs.pop("env", None), - srcs = kwargs.pop("srcs", None), - deps = kwargs.pop("deps", None), - # See https://bazel.build/reference/be/python#py_binary.main - # for default logic. - # NOTE: This doesn't match the exact way a regular py_binary searches for - # it's main amongst the srcs, but is close enough for most cases. - main = kwargs.pop("main", name + ".py"), - ) diff --git a/python/current_py_toolchain.bzl b/python/current_py_toolchain.bzl index 0ca5c90ccc..4fb9eb9eed 100644 --- a/python/current_py_toolchain.bzl +++ b/python/current_py_toolchain.bzl @@ -28,12 +28,18 @@ def _current_py_toolchain_impl(ctx): transitive.append(toolchain.py3_runtime.files) vars["PYTHON3"] = toolchain.py3_runtime.interpreter.path vars["PYTHON3_ROOTPATH"] = toolchain.py3_runtime.interpreter.short_path + elif toolchain.py3_runtime and toolchain.py3_runtime.interpreter_path: + vars["PYTHON3"] = toolchain.py3_runtime.interpreter_path + vars["PYTHON3_ROOTPATH"] = toolchain.py3_runtime.interpreter_path if toolchain.py2_runtime and toolchain.py2_runtime.interpreter: direct.append(toolchain.py2_runtime.interpreter) transitive.append(toolchain.py2_runtime.files) vars["PYTHON2"] = toolchain.py2_runtime.interpreter.path vars["PYTHON2_ROOTPATH"] = toolchain.py2_runtime.interpreter.short_path + elif toolchain.py2_runtime and toolchain.py2_runtime.interpreter_path: + vars["PYTHON2"] = toolchain.py2_runtime.interpreter_path + vars["PYTHON2_ROOTPATH"] = toolchain.py2_runtime.interpreter_path files = depset(direct, transitive = transitive) return [ diff --git a/python/entry_points/BUILD.bazel b/python/entry_points/BUILD.bazel index 46dbd9298b..7cfd0cdf25 100644 --- a/python/entry_points/BUILD.bazel +++ b/python/entry_points/BUILD.bazel @@ -21,17 +21,22 @@ exports_files( visibility = ["//docs:__subpackages__"], ) -bzl_library( - name = "py_console_script_binary_bzl", - srcs = [":py_console_script_binary.bzl"], - visibility = ["//visibility:public"], - deps = [ - "//python/private:py_console_script_binary_bzl", - ], -) - filegroup( name = "distribution", srcs = glob(["**"]), visibility = ["//python:__subpackages__"], ) + +bzl_library( + name = "py_console_script_binary", + srcs = ["py_console_script_binary.bzl"], + visibility = ["//visibility:public"], + deps = ["//python/private:py_console_script_binary"], +) + +alias( + name = "py_console_script_binary_bzl", + actual = ":py_console_script_binary", + deprecation = "Use //python/entry_points:py_console_script_binary instead", + visibility = ["//visibility:public"], +) diff --git a/python/extensions/BUILD.bazel b/python/extensions/BUILD.bazel index 12c0f248fe..af18fa15b7 100644 --- a/python/extensions/BUILD.bazel +++ b/python/extensions/BUILD.bazel @@ -25,27 +25,47 @@ filegroup( ) bzl_library( - name = "pip_bzl", + name = "config", + srcs = ["config.bzl"], + visibility = ["//:__subpackages__"], + deps = [ + "//python/private:internal_config_repo", + "//python/private/pypi:deps", + "@bazel_features//:features", + ], +) + +bzl_library( + name = "pip", srcs = ["pip.bzl"], visibility = ["//:__subpackages__"], - deps = ["//python/private/pypi:pip_bzl"], + deps = ["//python/private/pypi:pip"], ) bzl_library( - name = "python_bzl", + name = "python", srcs = ["python.bzl"], visibility = ["//:__subpackages__"], - deps = [ - "//python/private:python_bzl", - ], + deps = ["//python/private:python"], ) -bzl_library( +alias( name = "config_bzl", - srcs = ["config.bzl"], + actual = ":config", + deprecation = "Use //python/extensions:config instead", + visibility = ["//:__subpackages__"], +) + +alias( + name = "pip_bzl", + actual = ":pip", + deprecation = "Use //python/extensions:pip instead", + visibility = ["//:__subpackages__"], +) + +alias( + name = "python_bzl", + actual = ":python", + deprecation = "Use //python/extensions:python instead", visibility = ["//:__subpackages__"], - deps = [ - "//python/private:internal_config_repo_bzl", - "//python/private/pypi:deps_bzl", - ], ) diff --git a/python/extensions/config.bzl b/python/extensions/config.bzl index 2667b2a4fb..f19a07aaef 100644 --- a/python/extensions/config.bzl +++ b/python/extensions/config.bzl @@ -1,5 +1,6 @@ """Extension for configuring global settings of rules_python.""" +load("@bazel_features//:features.bzl", "bazel_features") load("//python/private:internal_config_repo.bzl", "internal_config_repo") load("//python/private/pypi:deps.bzl", "pypi_deps") @@ -21,10 +22,10 @@ to repositories that are expensive to create or invalidate frequently. }, ) -def _config_impl(mctx): +def _config_impl(module_ctx): transition_setting_generators = {} transition_settings = [] - for mod in mctx.modules: + for mod in module_ctx.modules: for tag in mod.tags.add_transition_setting: setting = str(tag.setting) if setting not in transition_setting_generators: @@ -40,10 +41,15 @@ def _config_impl(mctx): pypi_deps() + if bazel_features.external_deps.extension_metadata_has_reproducible: + return module_ctx.extension_metadata(reproducible = True) + else: + return None + config = module_extension( doc = """Global settings for rules_python. -:::{versionadded} VERSION_NEXT_FEATURE +:::{versionadded} 1.7.0 ::: """, implementation = _config_impl, diff --git a/python/features.bzl b/python/features.bzl index 21ff588dca..33323b8b65 100644 --- a/python/features.bzl +++ b/python/features.bzl @@ -13,8 +13,6 @@ # limitations under the License. """Allows detecting of rules_python features that aren't easily detected.""" -load("@rules_python_internal//:rules_python_config.bzl", "config") - # This is a magic string expanded by `git archive`, as set by `.gitattributes` # See https://git-scm.com/docs/git-archive/2.29.0#Documentation/git-archive.txt-export-subst _VERSION_PRIVATE = "$Format:%(describe:tags=true)$" @@ -22,13 +20,23 @@ _VERSION_PRIVATE = "$Format:%(describe:tags=true)$" def _features_typedef(): """Information about features rules_python has implemented. + ::::{field} targets + :type: dict[str, bool] + + A map of public API targets available in rules_python for feature detection + purposes. + + :::{versionadded} 1.9.0 + ::: + :::: + ::::{field} headers_abi3 :type: bool True if the {obj}`@rules_python//python/cc:current_py_cc_headers_abi3` target is available. - :::{versionadded} VERSION_NEXT_FEATURE + :::{versionadded} 1.7.0 ::: :::: @@ -41,6 +49,15 @@ def _features_typedef(): ::: :::: + ::::{field} loadable_symbols + :type: dict[str, list[str]] + + A map of bzl paths to the list of public symbols they export. + + :::{versionadded} 2.2.0 + ::: + :::: + ::::{field} py_info_venv_symlinks True if the `PyInfo.venv_symlinks` field is available. @@ -65,14 +82,68 @@ def _features_typedef(): optional trailing `-rcN`. For unreleased versions, it is an empty string. :::{versionadded} 0.38.0 :::: + + ::::{field} zipapp_rules + :type: bool + + Whether the rules_python version has the `py_zipapp_*` rules + + :::{versionadded} 1.9.0 + :::: """ +_TARGETS = { + "//command_line_option:build_runfile_links": True, + "//command_line_option:enable_runfiles": True, + "//command_line_option:extra_toolchains": True, + "//python/api:api": True, + "//python/api:executables": True, + "//python/api:libraries": True, + "//python/cc:current_py_cc_headers_abi3": True, + "//python/cc:py_cc_toolchain": True, + "//python/cc:py_cc_toolchain_info": True, + "//python/config_settings:venv": True, + "//python/entry_points:py_console_script_binary": True, + "//python/local_toolchains:repos": True, + "//python:defs": True, + "//python:features": True, + "//python:packaging": True, + "//python:pip": True, + "//python:proto": True, + "//python:py_binary": True, + "//python:py_cc_link_params_info": True, + "//python:py_exec_tools_info": True, + "//python:py_exec_tools_toolchain": True, + "//python:py_executable_info": True, + "//python:py_import": True, + "//python:py_info": True, + "//python:py_library": True, + "//python:py_runtime": True, + "//python:py_runtime_info": True, + "//python:py_runtime_pair": True, + "//python:py_test": True, + "//python:repositories": True, + "//python:versions": True, +} + +_LOADABLE_SYMBOLS = { + "//python:py_info.bzl": [ + # keep sorted + "PyInfo", + "VenvSymlinkEntry", + "VenvSymlinkKind", + ], +} + features = struct( TYPEDEF = _features_typedef, # keep sorted headers_abi3 = True, + loadable_symbols = _LOADABLE_SYMBOLS, precompile = True, py_info_venv_symlinks = True, - uses_builtin_rules = not config.enable_pystar, + targets = _TARGETS, + uses_builtin_rules = False, version = _VERSION_PRIVATE if "$Format" not in _VERSION_PRIVATE else "", + zipapp_rules = True, ) diff --git a/python/local_toolchains/BUILD.bazel b/python/local_toolchains/BUILD.bazel index 211f3e21a7..256dffb337 100644 --- a/python/local_toolchains/BUILD.bazel +++ b/python/local_toolchains/BUILD.bazel @@ -2,17 +2,24 @@ load("@bazel_skylib//:bzl_library.bzl", "bzl_library") package(default_visibility = ["//:__subpackages__"]) +filegroup( + name = "distribution", + srcs = glob(["**"]), +) + bzl_library( - name = "repos_bzl", + name = "repos", srcs = ["repos.bzl"], visibility = ["//visibility:public"], deps = [ - "//python/private:local_runtime_repo_bzl", - "//python/private:local_runtime_toolchains_repo_bzl", + "//python/private:local_runtime_repo", + "//python/private:local_runtime_toolchains_repo", ], ) -filegroup( - name = "distribution", - srcs = glob(["**"]), +alias( + name = "repos_bzl", + actual = ":repos", + deprecation = "Use //python/local_toolchains:repos instead", + visibility = ["//visibility:public"], ) diff --git a/python/packaging.bzl b/python/packaging.bzl index 223aba142d..537aa6090f 100644 --- a/python/packaging.bzl +++ b/python/packaging.bzl @@ -216,19 +216,12 @@ def py_wheel( **copy_propagating_kwargs(kwargs) ) elif twine: - if not twine.endswith(":pkg"): - fail("twine label should look like @my_twine_repo//:pkg") - - twine_main = twine.replace(":pkg", ":rules_python_wheel_entry_point_twine.py") - py_binary( name = "{}.publish".format(name), - srcs = [twine_main], + deps = [twine], args = twine_args, data = [dist_target], - imports = ["."], - main = twine_main, - deps = [twine], + main_module = "twine", tags = manual_tags, visibility = kwargs.get("visibility"), **copy_propagating_kwargs(kwargs) diff --git a/python/pip_install/BUILD.bazel b/python/pip_install/BUILD.bazel index 09bc46eea7..b3c1ab1c03 100644 --- a/python/pip_install/BUILD.bazel +++ b/python/pip_install/BUILD.bazel @@ -18,23 +18,6 @@ package( default_visibility = ["//:__subpackages__"], ) -bzl_library( - name = "pip_repository_bzl", - srcs = ["pip_repository.bzl"], - deps = [ - "//python/private/pypi:group_library_bzl", - "//python/private/pypi:package_annotation_bzl", - "//python/private/pypi:pip_repository_bzl", - "//python/private/pypi:whl_library_bzl", - ], -) - -bzl_library( - name = "requirements_bzl", - srcs = ["requirements.bzl"], - deps = ["//python/private/pypi:pip_compile_bzl"], -) - filegroup( name = "distribution", srcs = glob(["**"]), @@ -51,3 +34,26 @@ exports_files( glob(["*.bzl"]), visibility = ["//docs:__pkg__"], ) + +bzl_library( + name = "pip_repository", + srcs = ["pip_repository.bzl"], + deps = [ + "//python/private/pypi:package_annotation", + "//python/private/pypi:pip_repository", + "//python/private/pypi:whl_config_repo", + "//python/private/pypi:whl_library", + ], +) + +bzl_library( + name = "requirements", + srcs = ["requirements.bzl"], + deps = ["//python/private/pypi:pip_compile"], +) + +bzl_library( + name = "requirements_parser", + srcs = ["requirements_parser.bzl"], + deps = ["//python/private/pypi:parse_requirements_txt"], +) diff --git a/python/pip_install/pip_repository.bzl b/python/pip_install/pip_repository.bzl index 18deee1993..f9c3c9fb56 100644 --- a/python/pip_install/pip_repository.bzl +++ b/python/pip_install/pip_repository.bzl @@ -14,13 +14,14 @@ "" -load("//python/private/pypi:group_library.bzl", _group_library = "group_library") load("//python/private/pypi:package_annotation.bzl", _package_annotation = "package_annotation") load("//python/private/pypi:pip_repository.bzl", _pip_repository = "pip_repository") +load("//python/private/pypi:whl_config_repo.bzl", _whl_config_repo = "whl_config_repo") load("//python/private/pypi:whl_library.bzl", _whl_library = "whl_library") # Re-exports for backwards compatibility -group_library = _group_library +group_library = _whl_config_repo pip_repository = _pip_repository whl_library = _whl_library +whl_config_repo = _whl_config_repo package_annotation = _package_annotation diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel index 0c8ccdea99..17d18fef6f 100644 --- a/python/private/BUILD.bazel +++ b/python/private/BUILD.bazel @@ -13,20 +13,31 @@ # limitations under the License. load("@bazel_skylib//:bzl_library.bzl", "bzl_library") -load("@bazel_skylib//rules:common_settings.bzl", "bool_setting") +load("@bazel_skylib//rules:common_settings.bzl", "bool_flag") load("//python:py_binary.bzl", "py_binary") load("//python:py_library.bzl", "py_library") -load(":print_toolchain_checksums.bzl", "print_toolchains_checksums") +load(":bazel_config_mode.bzl", "bazel_config_mode") load(":py_exec_tools_toolchain.bzl", "current_interpreter_executable") -load(":sentinel.bzl", "sentinel") -load(":stamp.bzl", "stamp_build_setting") +load(":py_interpreter_program.bzl", "py_interpreter_program") +load(":sentinel_impl.bzl", "sentinel") +load(":stamp_impl.bzl", "stamp_build_setting") +load(":uncachable_version_file.bzl", "define_uncachable_version_file") +load(":visibility.bzl", "NOT_ACTUALLY_PUBLIC") package( - default_visibility = ["//:__subpackages__"], + default_visibility = [ + "//:__subpackages__", + ], ) licenses(["notice"]) +exports_files([ + "runtime_env_toolchain_interpreter.sh", + "runtimes_manifest.txt", + "runtimes_manifest_workspace.bzl", +]) + filegroup( name = "distribution", srcs = glob(["**"]) + [ @@ -34,13 +45,14 @@ filegroup( "//python/private/cc:distribution", "//python/private/pypi:distribution", "//python/private/whl_filegroup:distribution", + "//python/private/zipapp:distribution", "//tools/build_defs/python/private:distribution", ], visibility = ["//python:__pkg__"], ) filegroup( - name = "coverage_deps", + name = "coverage_deps_filegroup", srcs = ["coverage_deps.bzl"], visibility = ["//tools/private/update_deps:__pkg__"], ) @@ -52,824 +64,974 @@ filegroup( visibility = ["//python:__pkg__"], ) +alias( + name = "build_data_writer", + actual = select({ + "@platforms//os:windows": ":build_data_writer.ps1", + "//conditions:default": ":build_data_writer.sh", + }), + # Not actually public. Only public because it's an implicit dependency of + # rules built via py_binary_rule_builder() / py_test_rule_builder(). + visibility = ["//visibility:public"], +) + +define_uncachable_version_file( + name = "uncachable_version_file", + # Not actually public. Only public because it's an implicit dependency of + # rules built via py_binary_rule_builder() / py_test_rule_builder(). + visibility = ["//visibility:public"], +) + +# Needed to define bzl_library targets for docgen. (We don't define the +# bzl_library target here because it'd give our users a transitive dependency +# on Skylib.) +exports_files( + [ + "coverage.patch", + "py_package.bzl", + "py_wheel.bzl", + "version.bzl", + "reexports.bzl", + "stamp_impl.bzl", + "util.bzl", + ], + visibility = ["//:__subpackages__"], +) + +exports_files( + ["python_bootstrap_template.txt"], + # Not actually public. Only public because it's an implicit dependency of + # py_runtime. + visibility = NOT_ACTUALLY_PUBLIC, +) + +filegroup( + name = "stage1_bootstrap_template", + srcs = ["stage1_bootstrap_template.sh"], + # Not actually public. Only public because it's an implicit dependency of + # py_runtime. + visibility = NOT_ACTUALLY_PUBLIC, +) + +filegroup( + name = "stage2_bootstrap_template", + srcs = ["stage2_bootstrap_template.py"], + # Not actually public. Only public because it's an implicit dependency of + # py_runtime. + visibility = NOT_ACTUALLY_PUBLIC, +) + +filegroup( + name = "site_init_template", + srcs = ["site_init_template.py"], + # Not actually public. Only public because it's an implicit dependency of + # py_runtime. + visibility = NOT_ACTUALLY_PUBLIC, +) + +# NOTE: Windows builds don't use this bootstrap. Instead, a native Windows +# program locates some Python exe and runs `python.exe foo.zip` which +# runs the __main__.py in the zip file. +alias( + name = "bootstrap_template", + actual = select({ + ":is_script_bootstrap_enabled": "stage1_bootstrap_template.sh", + "//conditions:default": "python_bootstrap_template.txt", + }), + # Not actually public. Only public because it's an implicit dependency of + # py_runtime. + visibility = NOT_ACTUALLY_PUBLIC, +) + +# Used to determine the use of `--stamp` in Starlark rules +stamp_build_setting(name = "stamp") + +config_setting( + name = "is_script_bootstrap_enabled", + flag_values = { + "//python/config_settings:bootstrap_impl": "script", + }, +) + +config_setting( + name = "is_bazel_config_mode_target", + flag_values = { + "//python/private:bazel_config_mode": "target", + }, +) + +alias( + name = "debugger_if_target_config", + actual = select({ + ":is_bazel_config_mode_target": "//python/config_settings:debugger", + "//conditions:default": "//python/private:empty", + }), + # Not actually public. Only public because it's an implicit dependency of + # rules built via py_binary_rule_builder() / py_test_rule_builder(). + visibility = ["//visibility:public"], +) + +bazel_config_mode(name = "bazel_config_mode") + +# This should only be set by analysis tests to expose additional metadata to +# aid testing, so a setting instead of a flag. +bool_flag( + name = "visible_for_testing", + build_setting_default = False, + # This is only because it is an implicit dependency by the toolchains. + visibility = NOT_ACTUALLY_PUBLIC, +) + +# Used for py_console_script_gen rule +py_binary( + name = "py_console_script_gen_py", + srcs = ["py_console_script_gen.py"], + main = "py_console_script_gen.py", + visibility = [ + "//visibility:public", + ], +) + +py_binary( + name = "py_wheel_dist", + srcs = ["py_wheel_dist.py"], + visibility = ["//visibility:public"], +) + +py_library( + name = "py_console_script_gen_lib", + srcs = ["py_console_script_gen.py"], + imports = ["../.."], + visibility = [ + "//tests/entry_points:__pkg__", + ], +) + +# The current toolchain's interpreter as an excutable, usable with +# executable=True attributes. +current_interpreter_executable( + name = "current_interpreter_executable", + # Not actually public. Only public because it's an implicit dependency of + # py_exec_tools_toolchain. + visibility = NOT_ACTUALLY_PUBLIC, +) + +py_library( + name = "empty", + # Not actually public. Only public because it's the resolved default of + # debugger_if_target_config, an implicit dependency of rules built via + # py_binary_rule_builder() / py_test_rule_builder(). + visibility = ["//visibility:public"], +) + +sentinel( + name = "sentinel", + # Not actually public. Only public because it's the resolved default of + # uncachable_version_file, an implicit dependency of rules built via + # py_binary_rule_builder() / py_test_rule_builder(). + visibility = ["//visibility:public"], +) + +py_binary( + name = "sync_runtimes_manifest_workspace", + srcs = ["tools/sync_runtimes_manifest_workspace.py"], + visibility = ["//:__subpackages__"], +) + +# Tool used by py_test's validation action to statically check that the main +# module actually runs tests. See the validate_test_main config setting. +py_interpreter_program( + name = "py_test_main_validator", + main = "py_test_main_validator.py", + # Not actually public. Only public because it's an implicit dependency of + # the py_test rule. + visibility = NOT_ACTUALLY_PUBLIC, +) + +py_library( + name = "py_test_main_validator_lib", + srcs = ["py_test_main_validator.py"], + imports = ["../.."], + visibility = [ + "//tests/validate_test_main:__pkg__", + ], +) + bzl_library( - name = "attr_builders_bzl", + name = "attr_builders", srcs = ["attr_builders.bzl"], deps = [ - ":builders_util_bzl", + ":builders_util", "@bazel_skylib//lib:types", ], ) bzl_library( - name = "attributes_bzl", + name = "attributes", srcs = ["attributes.bzl"], deps = [ - ":attr_builders_bzl", - ":common_bzl", - ":common_labels_bzl", - ":enum_bzl", - ":flags_bzl", - ":py_info_bzl", - ":py_internal_bzl", - ":reexports_bzl", - ":rules_cc_srcs_bzl", + ":attr_builders", + ":common_labels", + ":enum", + ":flags", + ":py_info", + ":reexports", + ":rule_builders", + "@bazel_skylib//lib:dicts", "@bazel_skylib//rules:common_settings", + "@rules_cc//cc/common", ], ) bzl_library( - name = "auth_bzl", + name = "auth", srcs = ["auth.bzl"], - deps = [":bazel_tools_bzl"], + deps = ["//python/private:bazel_tools"], ) +# @bazel_tools can't define bzl_library itself, so we just put a wrapper around it. +# Keep this target because Gazelle does not automatically generate +# bzl_library targets for external repositories like @bazel_tools. +# keep bzl_library( - name = "runtime_env_toolchain_bzl", - srcs = ["runtime_env_toolchain.bzl"], - deps = [ - ":config_settings_bzl", - ":py_exec_tools_toolchain_bzl", - ":toolchain_types_bzl", - "//python:py_runtime_bzl", - "//python:py_runtime_pair_bzl", + name = "bazel_tools", + srcs = [ + # This set of sources is overly broad, but it's the only public + # target available across Bazel versions that has all the necessary + # sources. + "@bazel_tools//tools:bzl_srcs", ], ) bzl_library( - name = "builders_bzl", + name = "builders", srcs = ["builders.bzl"], - deps = [ - "@bazel_skylib//lib:types", - ], + deps = ["@bazel_skylib//lib:types"], ) bzl_library( - name = "builders_util_bzl", + name = "builders_util", srcs = ["builders_util.bzl"], deps = [ - ":bzlmod_enabled_bzl", + ":bzlmod_enabled", "@bazel_skylib//lib:types", ], ) bzl_library( - name = "bzlmod_enabled_bzl", - srcs = ["bzlmod_enabled.bzl"], -) - -bzl_library( - name = "cc_helper_bzl", + name = "cc_helper", srcs = ["cc_helper.bzl"], - deps = [":py_internal_bzl"], + deps = [":py_internal"], ) bzl_library( - name = "common_bzl", + name = "common", srcs = ["common.bzl"], deps = [ - ":cc_helper_bzl", - ":py_cc_link_params_info_bzl", - ":py_info_bzl", - ":py_internal_bzl", - ":reexports_bzl", - ":rules_cc_srcs_bzl", + ":builders", + ":cc_helper", + ":py_cc_link_params_info", + ":py_info", + ":py_internal", + ":py_interpreter_program", + ":reexports", + ":toolchain_types", "@bazel_skylib//lib:paths", + "@rules_cc//cc/common", + "@rules_python_internal//:rules_python_config", ], ) bzl_library( - name = "common_labels_bzl", - srcs = ["common_labels.bzl"], -) - -bzl_library( - name = "config_settings_bzl", + name = "config_settings", srcs = ["config_settings.bzl"], deps = [ - ":version_bzl", + ":text_util", + ":version", + ":visibility", "@bazel_skylib//lib:selects", "@bazel_skylib//rules:common_settings", ], ) bzl_library( - name = "coverage_deps_bzl", + name = "coverage_deps", srcs = ["coverage_deps.bzl"], deps = [ - ":bazel_tools_bzl", - ":version_label_bzl", + ":repo_utils", + ":version_label", + "//python/private:bazel_tools", ], ) bzl_library( - name = "deprecation_bzl", - srcs = ["deprecation.bzl"], + name = "current_py_cc_headers", + srcs = ["current_py_cc_headers.bzl"], deps = [ - "@rules_python_internal//:rules_python_config_bzl", + ":toolchain_types", + "@rules_cc//cc/common", ], ) bzl_library( - name = "enum_bzl", - srcs = ["enum.bzl"], + name = "current_py_cc_libs", + srcs = ["current_py_cc_libs.bzl"], + deps = ["@rules_cc//cc/common"], ) bzl_library( - name = "envsubst_bzl", - srcs = ["envsubst.bzl"], + name = "deprecation", + srcs = ["deprecation.bzl"], + deps = ["@rules_python_internal//:rules_python_config"], ) bzl_library( - name = "flags_bzl", + name = "flags", srcs = ["flags.bzl"], deps = [ - ":enum_bzl", + ":enum", "@bazel_skylib//rules:common_settings", ], ) bzl_library( - name = "full_version_bzl", - srcs = ["full_version.bzl"], -) - -bzl_library( - name = "glob_excludes_bzl", - srcs = ["glob_excludes.bzl"], - deps = [":util_bzl"], -) - -bzl_library( - name = "internal_config_repo_bzl", - srcs = ["internal_config_repo.bzl"], - deps = [":bzlmod_enabled_bzl"], -) - -bzl_library( - name = "is_standalone_interpreter_bzl", - srcs = ["is_standalone_interpreter.bzl"], + name = "hermetic_runtime_repo_setup", + srcs = ["hermetic_runtime_repo_setup.bzl"], deps = [ - ":repo_utils_bzl", + ":py_exec_tools_toolchain", + ":version", + "//python:py_runtime", + "//python:py_runtime_pair", + "//python/cc:py_cc_toolchain", + "@rules_cc//cc:core_rules", ], ) bzl_library( - name = "local_runtime_repo_bzl", - srcs = ["local_runtime_repo.bzl"], + name = "internal_config_repo", + srcs = ["internal_config_repo.bzl"], deps = [ - ":enum_bzl", - ":repo_utils.bzl", + ":repo_utils", + ":text_util", ], ) bzl_library( - name = "local_runtime_toolchains_repo_bzl", - srcs = ["local_runtime_toolchains_repo.bzl"], + name = "interpreter", + srcs = ["interpreter.bzl"], deps = [ - ":repo_utils.bzl", - ":text_util_bzl", - ], -) - -bzl_library( - name = "normalize_name_bzl", - srcs = ["normalize_name.bzl"], -) - -bzl_library( - name = "precompile_bzl", - srcs = ["precompile.bzl"], - deps = [ - ":attributes_bzl", - ":py_internal_bzl", - ":py_interpreter_program_bzl", - ":toolchain_types_bzl", + ":common", + ":sentinel_impl", + ":toolchain_types", + "//python:py_runtime_info", "@bazel_skylib//lib:paths", ], ) bzl_library( - name = "platform_info_bzl", - srcs = ["platform_info.bzl"], -) - -bzl_library( - name = "python_bzl", - srcs = ["python.bzl"], - deps = [ - ":full_version_bzl", - ":platform_info_bzl", - ":python_register_toolchains_bzl", - ":pythons_hub_bzl", - ":repo_utils_bzl", - ":toolchains_repo_bzl", - ":util_bzl", - ":version_bzl", - "@bazel_features//:features", - ], + name = "is_standalone_interpreter", + srcs = ["is_standalone_interpreter.bzl"], + deps = [":repo_utils"], ) bzl_library( - name = "python_register_toolchains_bzl", - srcs = ["python_register_toolchains.bzl"], + name = "local_runtime_repo", + srcs = ["local_runtime_repo.bzl"], deps = [ - ":auth_bzl", - ":bazel_tools_bzl", - ":coverage_deps_bzl", - ":full_version_bzl", - ":internal_config_repo_bzl", - ":python_repository_bzl", - ":toolchains_repo_bzl", - "//python:versions_bzl", - "//python/private/pypi:deps_bzl", + ":enum", + ":repo_utils", ], ) bzl_library( - name = "python_repository_bzl", - srcs = ["python_repository.bzl"], + name = "local_runtime_repo_setup", + srcs = ["local_runtime_repo_setup.bzl"], deps = [ - ":auth_bzl", - ":repo_utils_bzl", - ":text_util_bzl", - "//python:versions_bzl", + "@bazel_skylib//lib:selects", + "@rules_cc//cc:core_rules", + "@rules_python//python:py_runtime", + "@rules_python//python:py_runtime_pair", + "@rules_python//python/cc:py_cc_toolchain", + "@rules_python//python/private:py_exec_tools_toolchain", ], ) bzl_library( - name = "python_register_multi_toolchains_bzl", - srcs = ["python_register_multi_toolchains.bzl"], + name = "local_runtime_toolchains_repo", + srcs = ["local_runtime_toolchains_repo.bzl"], deps = [ - ":python_register_toolchains_bzl", - ":toolchains_repo_bzl", - "//python:versions_bzl", + ":repo_utils", + ":text_util", ], ) bzl_library( - name = "pythons_hub_bzl", - srcs = ["pythons_hub.bzl"], + name = "precompile", + srcs = ["precompile.bzl"], deps = [ - ":py_toolchain_suite_bzl", - ":text_util_bzl", - "//python:versions_bzl", + ":attributes", + ":flags", + ":py_interpreter_program", + ":toolchain_types", + "@bazel_skylib//rules:common_settings", ], ) bzl_library( - name = "py_binary_macro_bzl", + name = "py_binary_macro", srcs = ["py_binary_macro.bzl"], deps = [ - ":py_binary_rule_bzl", - ":py_executable_bzl", + ":py_binary_rule", + ":py_executable", ], ) bzl_library( - name = "py_binary_rule_bzl", + name = "py_binary_rule", srcs = ["py_binary_rule.bzl"], deps = [ - ":attributes_bzl", - ":py_executable_bzl", - ":rule_builders_bzl", - "@bazel_skylib//lib:dicts", + ":attributes", + ":py_executable", ], ) bzl_library( - name = "py_cc_link_params_info_bzl", + name = "py_cc_link_params_info", srcs = ["py_cc_link_params_info.bzl"], - deps = [ - ":rules_cc_srcs_bzl", - ":util_bzl", - ], + deps = ["@rules_cc//cc/common"], ) bzl_library( - name = "py_cc_toolchain_macro_bzl", + name = "py_cc_toolchain_macro", srcs = ["py_cc_toolchain_macro.bzl"], deps = [ - ":py_cc_toolchain_rule_bzl", + ":py_cc_toolchain_rule", + ":util", ], ) bzl_library( - name = "py_cc_toolchain_rule_bzl", + name = "py_cc_toolchain_rule", srcs = ["py_cc_toolchain_rule.bzl"], deps = [ - ":common_labels.bzl", - ":py_cc_toolchain_info_bzl", - ":rules_cc_srcs_bzl", - ":sentinel_bzl", - ":util_bzl", + ":common_labels", + ":py_cc_toolchain_info", + ":sentinel_impl", "@bazel_skylib//rules:common_settings", + "@rules_cc//cc/common", ], ) bzl_library( - name = "py_cc_toolchain_info_bzl", - srcs = ["py_cc_toolchain_info.bzl"], -) - -bzl_library( - name = "py_console_script_binary_bzl", - srcs = [ - "py_console_script_binary.bzl", - "py_console_script_gen.bzl", - ], - visibility = ["//python/entry_points:__pkg__"], + name = "py_console_script_binary", + srcs = ["py_console_script_binary.bzl"], deps = [ - "//python:py_binary_bzl", + ":py_console_script_gen", + "//python:py_binary", ], ) bzl_library( - name = "py_exec_tools_info_bzl", - srcs = ["py_exec_tools_info.bzl"], -) - -bzl_library( - name = "py_exec_tools_toolchain_bzl", + name = "py_exec_tools_toolchain", srcs = ["py_exec_tools_toolchain.bzl"], deps = [ - ":common_bzl", - ":common_labels_bzl", - ":py_exec_tools_info_bzl", - ":sentinel_bzl", - ":toolchain_types_bzl", + ":common_labels", + ":py_exec_tools_info", + ":sentinel_impl", + ":toolchain_types", "@bazel_skylib//lib:paths", "@bazel_skylib//rules:common_settings", ], ) bzl_library( - name = "py_executable_bzl", + name = "py_executable", srcs = ["py_executable.bzl"], deps = [ - ":attributes_bzl", - ":cc_helper_bzl", - ":common_bzl", - ":common_labels_bzl", - ":flags_bzl", - ":precompile_bzl", - ":py_cc_link_params_info_bzl", - ":py_executable_info_bzl", - ":py_info_bzl", - ":py_internal_bzl", - ":py_runtime_info_bzl", - ":rules_cc_srcs_bzl", - ":toolchain_types_bzl", - ":transition_labels_bzl", - ":venv_runfiles_bzl", + ":attr_builders", + ":attributes", + ":builders", + ":cc_helper", + ":common", + ":common_labels", + ":flags", + ":precompile", + ":py_cc_link_params_info", + ":py_executable_info", + ":py_info", + ":py_internal", + ":py_runtime_info", + ":reexports", + ":rule_builders", + ":toolchain_types", + ":transition_labels", + ":venv_runfiles", "@bazel_skylib//lib:dicts", "@bazel_skylib//lib:paths", "@bazel_skylib//lib:structs", "@bazel_skylib//rules:common_settings", + "@rules_cc//cc/common", + "@rules_python_internal//:rules_python_config", ], ) bzl_library( - name = "py_executable_info_bzl", - srcs = ["py_executable_info.bzl"], -) - -bzl_library( - name = "py_info_bzl", + name = "py_info", srcs = ["py_info.bzl"], deps = [ - ":builders_bzl", - ":reexports_bzl", - ":util_bzl", - "@rules_python_internal//:rules_python_config_bzl", + ":builders", + ":reexports", ], ) bzl_library( - name = "py_internal_bzl", + name = "py_internal", srcs = ["py_internal.bzl"], - deps = ["@rules_python_internal//:py_internal_bzl"], + deps = ["//tools/build_defs/python/private:py_internal_renamed"], ) bzl_library( - name = "py_interpreter_program_bzl", + name = "py_interpreter_program", srcs = ["py_interpreter_program.bzl"], - deps = ["@bazel_skylib//rules:common_settings"], + deps = [ + ":sentinel_impl", + "@bazel_skylib//rules:common_settings", + ], ) bzl_library( - name = "py_library_bzl", + name = "py_library", srcs = ["py_library.bzl"], deps = [ - ":attributes_bzl", - ":common_bzl", - ":common_labels_bzl", - ":flags_bzl", - ":normalize_name_bzl", - ":precompile_bzl", - ":py_cc_link_params_info_bzl", - ":py_internal_bzl", - ":rule_builders_bzl", - ":toolchain_types_bzl", - ":venv_runfiles_bzl", - ":version_bzl", + ":attr_builders", + ":attributes", + ":builders", + ":common", + ":common_labels", + ":flags", + ":normalize_name", + ":precompile", + ":py_cc_link_params_info", + ":py_info", + ":reexports", + ":rule_builders", + ":toolchain_types", + ":venv_runfiles", + ":version", "@bazel_skylib//lib:dicts", + "@bazel_skylib//lib:paths", "@bazel_skylib//rules:common_settings", ], ) bzl_library( - name = "py_library_macro_bzl", + name = "py_library_macro", srcs = ["py_library_macro.bzl"], - deps = [":py_library_rule_bzl"], + deps = [":py_library_rule"], ) bzl_library( - name = "py_library_rule_bzl", + name = "py_library_rule", srcs = ["py_library_rule.bzl"], - deps = [ - ":common_labels_bzl", - ":py_library_bzl", - ], + deps = [":py_library"], ) bzl_library( - name = "py_package_bzl", + name = "py_package", srcs = ["py_package.bzl"], - visibility = ["//:__subpackages__"], deps = [ - ":builders_bzl", - ":py_info_bzl", + ":builders", + ":py_info", ], ) bzl_library( - name = "py_runtime_info_bzl", - srcs = ["py_runtime_info.bzl"], - deps = [":util_bzl"], -) - -bzl_library( - name = "py_repositories_bzl", + name = "py_repositories", srcs = ["py_repositories.bzl"], deps = [ - ":bazel_tools_bzl", - ":internal_config_repo_bzl", - ":pythons_hub_bzl", - "//python:versions_bzl", - "//python/private/pypi:deps_bzl", + ":internal_config_repo", + ":pythons_hub", + "//python:versions", + "//python/private:bazel_tools", + "//python/private/pypi:deps", ], ) bzl_library( - name = "py_runtime_macro_bzl", + name = "py_runtime_macro", srcs = ["py_runtime_macro.bzl"], - deps = [":py_runtime_rule_bzl"], + deps = [":py_runtime_rule"], ) bzl_library( - name = "py_runtime_rule_bzl", - srcs = ["py_runtime_rule.bzl"], - deps = [ - ":attributes_bzl", - ":common_labels_bzl", - ":flags_bzl", - ":py_internal_bzl", - ":py_runtime_info_bzl", - ":reexports_bzl", - ":rule_builders_bzl", - ":util_bzl", - "@bazel_skylib//lib:dicts", - "@bazel_skylib//lib:paths", - "@bazel_skylib//rules:common_settings", - ], + name = "py_runtime_pair_macro", + srcs = ["py_runtime_pair_macro.bzl"], + deps = [":py_runtime_pair_rule"], ) bzl_library( - name = "py_runtime_pair_macro_bzl", - srcs = ["py_runtime_pair_macro.bzl"], - visibility = ["//:__subpackages__"], - deps = [":py_runtime_pair_rule_bzl"], + name = "py_runtime_pair_rule", + srcs = ["py_runtime_pair_rule.bzl"], + deps = [ + ":common_labels", + ":reexports", + "//python:py_runtime_info", + "@bazel_skylib//rules:common_settings", + ], ) bzl_library( - name = "py_runtime_pair_rule_bzl", - srcs = ["py_runtime_pair_rule.bzl"], + name = "py_runtime_rule", + srcs = ["py_runtime_rule.bzl"], deps = [ - ":common_labels_bzl", - "//python:py_runtime_bzl", - "//python:py_runtime_info_bzl", + ":common_labels", + ":flags", + ":py_internal", + ":py_runtime_info", + ":reexports", + ":version", + "@bazel_skylib//lib:dicts", + "@bazel_skylib//lib:paths", "@bazel_skylib//rules:common_settings", ], ) bzl_library( - name = "py_test_macro_bzl", + name = "py_test_macro", srcs = ["py_test_macro.bzl"], deps = [ - ":py_executable_bzl", - ":py_test_rule_bzl", + ":py_executable", + ":py_test_rule", ], ) bzl_library( - name = "py_test_rule_bzl", + name = "py_test_rule", srcs = ["py_test_rule.bzl"], deps = [ - ":attributes_bzl", - ":common_bzl", - ":py_executable_bzl", - ":rule_builders_bzl", - "@bazel_skylib//lib:dicts", + ":attributes", + ":common", + ":py_executable", ], ) bzl_library( - name = "py_toolchain_suite_bzl", + name = "py_toolchain_suite", srcs = ["py_toolchain_suite.bzl"], deps = [ - ":config_settings_bzl", - ":text_util_bzl", - ":toolchain_types_bzl", + ":text_util", + ":toolchain_types", "@bazel_skylib//lib:selects", + "@platforms//host:constraints_lib", ], ) bzl_library( - name = "py_wheel_bzl", + name = "py_wheel", srcs = ["py_wheel.bzl"], - visibility = ["//:__subpackages__"], deps = [ - ":py_package_bzl", - ":stamp_bzl", - ":transition_labels_bzl", - ":version_bzl", + ":attributes", + ":py_info", + ":py_package", + ":rule_builders", + ":stamp_impl", + ":transition_labels", + ":version", ], ) bzl_library( - name = "reexports_bzl", - srcs = ["reexports.bzl"], - visibility = [ - "//:__subpackages__", + name = "python", + srcs = ["python.bzl"], + deps = [ + ":auth", + ":full_version", + ":pbs_manifest", + ":platform_info", + ":pyproject_utils", + ":python_register_toolchains", + ":pythons_hub", + ":repo_utils", + ":toolchains_repo", + ":version", + "//python:versions", + "@bazel_features//:features", ], +) + +bzl_library( + name = "python_register_multi_toolchains", + srcs = ["python_register_multi_toolchains.bzl"], deps = [ - ":bazel_tools_bzl", - "@rules_python_internal//:rules_python_config_bzl", + ":python_register_toolchains", + ":toolchains_repo", + "//python:versions", ], ) bzl_library( - name = "register_extension_info_bzl", - srcs = ["register_extension_info.bzl"], + name = "python_register_toolchains", + srcs = ["python_register_toolchains.bzl"], + deps = [ + ":coverage_deps", + ":full_version", + ":python_repository", + ":repo_utils", + ":toolchains_repo", + "//python:versions", + ], ) bzl_library( - name = "repo_utils_bzl", - srcs = ["repo_utils.bzl"], + name = "python_repository", + srcs = ["python_repository.bzl"], + deps = [ + ":auth", + ":repo_utils", + ":text_util", + "//python:versions", + ], +) + +bzl_library( + name = "pythons_hub", + srcs = ["pythons_hub.bzl"], + deps = [ + ":pbs_manifest", + ":text_util", + ":toolchains_repo", + "//python:versions", + ], ) bzl_library( - name = "rule_builders_bzl", + name = "reexports", + srcs = ["reexports.bzl"], + deps = ["@rules_python_internal//:rules_python_config"], +) + +bzl_library( + name = "repl", + srcs = ["repl.bzl"], + deps = ["//python:py_binary"], +) + +bzl_library( + name = "rule_builders", srcs = ["rule_builders.bzl"], deps = [ - ":builders_bzl", - ":builders_util_bzl", + ":builders_util", "@bazel_skylib//lib:types", ], ) +# Keep this target because Gazelle does not automatically generate or +# maintain composite bzl_library targets. We need this to group external +# dependency sources. +# keep bzl_library( - name = "sentinel_bzl", - srcs = ["sentinel.bzl"], + name = "rules_cc_srcs", + srcs = [ + # rules_cc 0.0.13 and earlier load cc_proto_libary (and thus protobuf@), + # but their bzl srcs targets don't transitively refer to protobuf. + "@com_google_protobuf//:bzl_srcs", + # NOTE: As of rules_cc 0.10, cc:bzl_srcs no longer contains + # everything and sub-targets must be used instead + "@rules_cc//cc:bzl_srcs", + "@rules_cc//cc/common", + "@rules_cc//cc/toolchains:toolchain_rules", + ], + deps = [ + ":bazel_tools", + "@rules_cc//cc/common", + ], ) bzl_library( - name = "stamp_bzl", - srcs = ["stamp.bzl"], - visibility = ["//:__subpackages__"], + name = "runtime_env_repo", + srcs = ["runtime_env_repo.bzl"], + deps = [":repo_utils"], ) bzl_library( - name = "text_util_bzl", - srcs = ["text_util.bzl"], + name = "runtime_env_toolchain", + srcs = ["runtime_env_toolchain.bzl"], + deps = [ + ":config_settings", + ":py_exec_tools_toolchain", + ":toolchain_types", + "//python:py_runtime", + "//python:py_runtime_pair", + "//python/cc:py_cc_toolchain", + "@rules_cc//cc:core_rules", + ], ) bzl_library( - name = "toolchains_repo_bzl", - srcs = ["toolchains_repo.bzl"], + name = "toolchain_aliases", + srcs = ["toolchain_aliases.bzl"], deps = [ - ":repo_utils_bzl", - ":text_util_bzl", - "//python:versions_bzl", + "//python:versions", + "@bazel_skylib//lib:selects", ], ) bzl_library( - name = "toolchain_types_bzl", - srcs = ["toolchain_types.bzl"], + name = "toolchains_repo", + srcs = ["toolchains_repo.bzl"], + deps = [ + ":repo_utils", + ":text_util", + "//python:versions", + ], ) bzl_library( - name = "transition_labels_bzl", + name = "transition_labels", srcs = ["transition_labels.bzl"], deps = [ - ":common_labels_bzl", + ":common_labels", "@bazel_skylib//lib:collections", - "@rules_python_internal//:extra_transition_settings_bzl", + "@rules_python_internal//:extra_transition_settings", ], ) bzl_library( - name = "util_bzl", + name = "util", srcs = ["util.bzl"], - visibility = [ - "//:__subpackages__", - ], deps = [ + ":py_internal", "@bazel_skylib//lib:types", - "@rules_python_internal//:rules_python_config_bzl", ], ) bzl_library( - name = "version_bzl", - srcs = ["version.bzl"], + name = "venv_runfiles", + srcs = ["venv_runfiles.bzl"], + deps = [ + ":common", + ":py_info", + ":util", + "@bazel_skylib//lib:paths", + ], ) bzl_library( - name = "version_label_bzl", - srcs = ["version_label.bzl"], + name = "pyproject_utils", + srcs = ["pyproject_utils.bzl"], + deps = [ + ":version", + "@toml.bzl//:toml", + ], ) -# @bazel_tools can't define bzl_library itself, so we just put a wrapper around it. bzl_library( - name = "bazel_tools_bzl", - srcs = [ - # This set of sources is overly broad, but it's the only public - # target available across Bazel versions that has all the necessary - # sources. - "@bazel_tools//tools:bzl_srcs", - ], + name = "stamp_impl", + srcs = ["stamp_impl.bzl"], + deps = [":visibility"], ) bzl_library( - name = "rules_cc_srcs_bzl", - srcs = [ - # rules_cc 0.0.13 and earlier load cc_proto_libary (and thus protobuf@), - # but their bzl srcs targets don't transitively refer to protobuf. - "@com_google_protobuf//:bzl_srcs", - # NOTE: As of rules_cc 0.10, cc:bzl_srcs no longer contains - # everything and sub-targets must be used instead - "@rules_cc//cc:bzl_srcs", - "@rules_cc//cc/common", - "@rules_cc//cc/toolchains:toolchain_rules", - ], - deps = [ - ":bazel_tools_bzl", - "@rules_cc//cc/common", - ], + name = "bzlmod_enabled", + srcs = ["bzlmod_enabled.bzl"], ) bzl_library( - name = "venv_runfiles_bzl", - srcs = ["venv_runfiles.bzl"], - deps = [ - ":common_bzl", - ":py_info.bzl", - "@bazel_skylib//lib:paths", - ], + name = "common_labels", + srcs = ["common_labels.bzl"], ) -# Needed to define bzl_library targets for docgen. (We don't define the -# bzl_library target here because it'd give our users a transitive dependency -# on Skylib.) -exports_files( - [ - "coverage.patch", - "repack_whl.py", - "py_package.bzl", - "py_wheel.bzl", - "version.bzl", - "reexports.bzl", - "stamp.bzl", - "util.bzl", - ], - visibility = ["//:__subpackages__"], +bzl_library( + name = "enum", + srcs = ["enum.bzl"], ) -exports_files( - ["python_bootstrap_template.txt"], - # Not actually public. Only public because it's an implicit dependency of - # py_runtime. - visibility = ["//visibility:public"], +bzl_library( + name = "envsubst", + srcs = ["envsubst.bzl"], ) -filegroup( - name = "stage1_bootstrap_template", - srcs = ["stage1_bootstrap_template.sh"], - # Not actually public. Only public because it's an implicit dependency of - # py_runtime. - visibility = ["//visibility:public"], +bzl_library( + name = "full_version", + srcs = ["full_version.bzl"], ) -filegroup( - name = "stage2_bootstrap_template", - srcs = ["stage2_bootstrap_template.py"], - # Not actually public. Only public because it's an implicit dependency of - # py_runtime. - visibility = ["//visibility:public"], +bzl_library( + name = "normalize_name", + srcs = ["normalize_name.bzl"], ) -filegroup( - name = "zip_main_template", - srcs = ["zip_main_template.py"], - # Not actually public. Only public because it's an implicit dependency of - # py_runtime. - visibility = ["//visibility:public"], +bzl_library( + name = "pbs_manifest", + srcs = ["pbs_manifest.bzl"], ) -filegroup( - name = "site_init_template", - srcs = ["site_init_template.py"], - # Not actually public. Only public because it's an implicit dependency of - # py_runtime. - visibility = ["//visibility:public"], +bzl_library( + name = "platform_info", + srcs = ["platform_info.bzl"], ) -# NOTE: Windows builds don't use this bootstrap. Instead, a native Windows -# program locates some Python exe and runs `python.exe foo.zip` which -# runs the __main__.py in the zip file. -alias( - name = "bootstrap_template", - actual = select({ - ":is_script_bootstrap_enabled": "stage1_bootstrap_template.sh", - "//conditions:default": "python_bootstrap_template.txt", - }), - # Not actually public. Only public because it's an implicit dependency of - # py_runtime. - visibility = ["//visibility:public"], +bzl_library( + name = "py_cc_toolchain_info", + srcs = ["py_cc_toolchain_info.bzl"], ) -# Used to determine the use of `--stamp` in Starlark rules -stamp_build_setting(name = "stamp") +bzl_library( + name = "py_console_script_gen", + srcs = ["py_console_script_gen.bzl"], +) -config_setting( - name = "is_script_bootstrap_enabled", - flag_values = { - "//python/config_settings:bootstrap_impl": "script", - }, +bzl_library( + name = "py_exec_tools_info", + srcs = ["py_exec_tools_info.bzl"], ) -# This should only be set by analysis tests to expose additional metadata to -# aid testing, so a setting instead of a flag. -bool_setting( - name = "visible_for_testing", - build_setting_default = False, - # This is only because it is an implicit dependency by the toolchains. - visibility = ["//visibility:public"], +bzl_library( + name = "py_executable_info", + srcs = ["py_executable_info.bzl"], ) -print_toolchains_checksums(name = "print_toolchains_checksums") +bzl_library( + name = "py_runtime_info", + srcs = ["py_runtime_info.bzl"], +) -# Used for py_console_script_gen rule -py_binary( - name = "py_console_script_gen_py", - srcs = ["py_console_script_gen.py"], - main = "py_console_script_gen.py", - visibility = [ - "//visibility:public", - ], +bzl_library( + name = "repo_utils", + srcs = ["repo_utils.bzl"], ) -py_binary( - name = "py_wheel_dist", - srcs = ["py_wheel_dist.py"], - visibility = ["//visibility:public"], +bzl_library( + name = "runtimes_manifest_workspace", + srcs = ["runtimes_manifest_workspace.bzl"], ) -py_library( - name = "py_console_script_gen_lib", - srcs = ["py_console_script_gen.py"], - imports = ["../.."], - visibility = [ - "//tests/entry_points:__pkg__", - ], +bzl_library( + name = "sentinel_impl", + srcs = ["sentinel_impl.bzl"], ) -# The current toolchain's interpreter as an excutable, usable with -# executable=True attributes. -current_interpreter_executable( - name = "current_interpreter_executable", - # Not actually public. Only public because it's an implicit dependency of - # py_exec_tools_toolchain. - visibility = ["//visibility:public"], +bzl_library( + name = "text_util", + srcs = ["text_util.bzl"], ) -py_library( - name = "empty", +bzl_library( + name = "toolchain_types", + srcs = ["toolchain_types.bzl"], ) -sentinel( - name = "sentinel", +bzl_library( + name = "version", + srcs = ["version.bzl"], +) + +bzl_library( + name = "version_label", + srcs = ["version_label.bzl"], +) + +bzl_library( + name = "visibility", + srcs = ["visibility.bzl"], ) diff --git a/python/private/api/BUILD.bazel b/python/private/api/BUILD.bazel index 0826b85d9b..cd7eda1bd2 100644 --- a/python/private/api/BUILD.bazel +++ b/python/private/api/BUILD.bazel @@ -13,6 +13,7 @@ # limitations under the License. load("@bazel_skylib//:bzl_library.bzl", "bzl_library") +load("//python/private:visibility.bzl", "NOT_ACTUALLY_PUBLIC") load(":py_common_api.bzl", "py_common_api") package( @@ -25,24 +26,21 @@ filegroup( ) py_common_api( - name = "py_common_api", + name = "py_common_api_impl", # NOTE: Not actually public. Implicit dependency of public rules. - visibility = ["//visibility:public"], + visibility = NOT_ACTUALLY_PUBLIC, ) bzl_library( - name = "api_bzl", - srcs = ["api.bzl"], + name = "py_common_api", + srcs = ["py_common_api.bzl"], deps = [ - "//python/private:py_info_bzl", + ":api", + "//python/private:py_info", ], ) bzl_library( - name = "py_common_api_bzl", - srcs = ["py_common_api.bzl"], - deps = [ - ":api_bzl", - "//python/private:py_info_bzl", - ], + name = "api", + srcs = ["api.bzl"], ) diff --git a/python/private/api/api.bzl b/python/private/api/api.bzl index 44f9ab4e77..2a1f79fdf5 100644 --- a/python/private/api/api.bzl +++ b/python/private/api/api.bzl @@ -13,7 +13,7 @@ # limitations under the License. """Implementation of py_api.""" -_PY_COMMON_API_LABEL = Label("//python/private/api:py_common_api") +_PY_COMMON_API_LABEL = Label("//python/private/api:py_common_api_impl") ApiImplInfo = provider( doc = "Provider to hold an API implementation", diff --git a/python/private/attr_builders.bzl b/python/private/attr_builders.bzl index ecfc570a2b..8e8a690b8f 100644 --- a/python/private/attr_builders.bzl +++ b/python/private/attr_builders.bzl @@ -592,7 +592,7 @@ def _Label_new(**kwargs): """Creates a builder for `attr.label`. Args: - **kwargs: The same as {obj}`attr.label()`. + **kwargs: The same as {obj}`attr.label`. Returns: {type}`Label` diff --git a/python/private/attributes.bzl b/python/private/attributes.bzl index 6d08c3d926..61b41d527c 100644 --- a/python/private/attributes.bzl +++ b/python/private/attributes.bzl @@ -44,12 +44,6 @@ REQUIRED_EXEC_GROUP_BUILDERS = { "py_precompile": lambda: ruleb.ExecGroup(), } -# Backwards compatibility symbol for Google. -REQUIRED_EXEC_GROUPS = { - k: v().build() - for k, v in REQUIRED_EXEC_GROUP_BUILDERS.items() -} - _STAMP_VALUES = [-1, 0, 1] def _precompile_attr_get_effective_value(ctx): @@ -195,7 +189,14 @@ List of import directories to be added to the PYTHONPATH. Subject to "Make variable" substitution. These import directories will be added for this rule and all rules that depend on it (note: not the rules this rule depends on. Each directory will be added to `PYTHONPATH` by `py_binary` rules -that depend on this rule. The strings are repo-runfiles-root relative, +that depend on this rule. + +The values are target-directory-relative runfiles-root paths. e.g. given target +`//foo/bar:baz`, `sys.path` will be affected as: +* `a/b` adds `$runfilesRoot/$repo/foo/bar/a/b` +* `../sibling` adds `$runfilesRoot/$repo/foo/sibling` +* `../../` adds `$runfilesRoot/$repo` +(where `$repo` is the name of the repository containing the target). Absolute paths (paths that start with `/`) and paths that references a path above the execution root are not allowed and will result in an error. @@ -391,7 +392,7 @@ obtained by calling `str(Label(...))`). Most `@rules_python//python/config_setting` settings can be used here, which allows, for example, making only a certain `py_binary` use -{obj}`--boostrap_impl=script`. +{obj}`--bootstrap_impl=script`. Additional or custom config settings can be registered using the {obj}`add_transition_setting` API. This allows, for example, forcing a @@ -399,6 +400,23 @@ particular CPU, or defining a custom setting that `select()` uses elsewhere to pick between `pip.parse` hubs. See the [How to guide on multiple versions of a library] for a more concrete example. +:::{important} +Labels with package `command_line_option` are handled specially: they are treated +as the Bazel-builtin `//command_line_option:` psuedo-targets. + +e.g. `@foo//command_line_option:NAME` will attempt to transition +the Bazel-builtin `//command_line_option:NAME` setting. + +See the {obj}`@rules_python//command_line_option` package for some predefined +special targets, or define your own by putting them in your own `command_line_option` +directory. +::: + +:::{seealso} +* {obj}`//command_line_option:build_runfile_links` +* {obj}`//command_line_option:enable_runfiles` +::: + :::{note} These values are transitioned on, so will affect the analysis graph and the associated memory overhead. The more unique configurations in your overall @@ -408,7 +426,7 @@ https://bazel.build/extending/config#memory-performance-considerations for more information about risks and considerations. ::: -:::{versionadded} VERSION_NEXT_FEATURE +:::{versionadded} 1.7.0 ::: """, ), @@ -425,7 +443,19 @@ def apply_config_settings_attr(settings, attr): {type}`dict[str, object]` the input `settings` value. """ for key, value in attr.config_settings.items(): - settings[str(key)] = value + if key.package == "command_line_option": + if value == "INHERIT": + continue + str_key = "//command_line_option:" + key.name + if key.name == "extra_toolchains": + if value == "": + value = [] + else: + value = [v.strip() for v in value.split(",") if v.strip()] + else: + str_key = str(key) + + settings[str_key] = value return settings AGNOSTIC_EXECUTABLE_ATTRS = dicts.add( @@ -456,8 +486,20 @@ Whether to encode build information into the binary. Possible values: Stamped binaries are not rebuilt unless their dependencies change. -WARNING: Stamping can harm build performance by reducing cache hits and should +Stamped build information can accessed using the `bazel_binary_info` module. +See the [Accessing build information docs] for more information. + +:::{warning} +Stamping can harm build performance by reducing cache hits and should be avoided if possible. + +In addition, this transitions the {flag}`--stamp ` flag, which can additional +config state overhead. +::: + +:::{note} +Stamping of build data output is always disabled for the exec config. +::: """, default = -1, ), @@ -493,6 +535,14 @@ environment when the test is executed by bazel test. "@platforms//os:watchos", ], ), + "_validate_test_main": lambda: attrb.Label( + default = "//python/private:py_test_main_validator", + cfg = "exec", + ), + "_validate_test_main_flag": lambda: attrb.Label( + default = labels.VALIDATE_TEST_MAIN, + providers = [BuildSettingInfo], + ), }) # Attributes specific to Python test-equivalent executable rules. Such rules may @@ -505,6 +555,14 @@ AGNOSTIC_TEST_ATTRS = _init_agnostic_test_attrs() # but still accept Python source-agnostic settings. AGNOSTIC_BINARY_ATTRS = dicts.add(AGNOSTIC_EXECUTABLE_ATTRS) +WINDOWS_CONSTRAINTS_ATTRS = { + "_windows_constraints": lambda: attrb.LabelList( + default = [ + "@platforms//os:windows", + ], + ), +} + # Attribute names common to all Python rules COMMON_ATTR_NAMES = [ "compatible_with", diff --git a/python/private/auth.bzl b/python/private/auth.bzl index 6b612678c8..2bab92ef30 100644 --- a/python/private/auth.bzl +++ b/python/private/auth.bzl @@ -95,11 +95,7 @@ def get_auth(ctx, urls, ctx_attr = None): if ctx_attr.netrc: netrc = read_netrc(ctx, ctx_attr.netrc) elif "NETRC" in ctx.os.environ: - # This can be used on newer bazel versions - if hasattr(ctx, "getenv"): - netrc = read_netrc(ctx, ctx.getenv("NETRC")) - else: - netrc = read_netrc(ctx, ctx.os.environ["NETRC"]) + netrc = read_netrc(ctx, ctx.getenv("NETRC")) else: netrc = read_user_netrc(ctx) diff --git a/python/private/bazel_config_mode.bzl b/python/private/bazel_config_mode.bzl new file mode 100644 index 0000000000..ec6be5c83b --- /dev/null +++ b/python/private/bazel_config_mode.bzl @@ -0,0 +1,12 @@ +"""Flag to tell if exec or target mode is active.""" + +load(":py_internal.bzl", "py_internal") + +def _bazel_config_mode_impl(ctx): + return [config_common.FeatureFlagInfo( + value = "exec" if py_internal.is_tool_configuration(ctx) else "target", + )] + +bazel_config_mode = rule( + implementation = _bazel_config_mode_impl, +) diff --git a/python/private/build_data_writer.ps1 b/python/private/build_data_writer.ps1 new file mode 100644 index 0000000000..05c49fef9c --- /dev/null +++ b/python/private/build_data_writer.ps1 @@ -0,0 +1,37 @@ +$OutputPath = $env:OUTPUT +$Lines = @( + "TARGET $env:TARGET", + "STAMPED $env:STAMPED" +) + +$VersionFilePath = $env:VERSION_FILE +if (-not [string]::IsNullOrEmpty($VersionFilePath) -and (Test-Path $VersionFilePath)) { + $Lines += Get-Content -Path $VersionFilePath +} + +$InfoFilePath = $env:INFO_FILE +if (-not [string]::IsNullOrEmpty($InfoFilePath) -and (Test-Path $InfoFilePath)) { + $Lines += Get-Content -Path $InfoFilePath +} + +# Use .NET to write file to avoid PowerShell encoding/locking quirks +# We use UTF8 without BOM for compatibility with how the bash script writes (and +# what consumers expect). +$Utf8NoBom = New-Object System.Text.UTF8Encoding $False +[System.IO.File]::WriteAllLines($OutputPath, $Lines, $Utf8NoBom) + +$Acl = Get-Acl $OutputPath +# We use WorldSid because the "Everyone" name is locale-dependent. +$EveryoneSid = New-Object System.Security.Principal.SecurityIdentifier( + [System.Security.Principal.WellKnownSidType]::WorldSid, + $null +) +$AccessRule = New-Object System.Security.AccessControl.FileSystemAccessRule( + $EveryoneSid, + "Read", + "Allow" +) +$Acl.SetAccessRule($AccessRule) +Set-Acl $OutputPath $Acl + +exit 0 diff --git a/python/private/build_data_writer.sh b/python/private/build_data_writer.sh new file mode 100755 index 0000000000..4af98c6269 --- /dev/null +++ b/python/private/build_data_writer.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +echo "TARGET $TARGET" >> $OUTPUT +echo "STAMPED $STAMPED" >> $OUTPUT +if [ -n "$VERSION_FILE" ]; then + cat "$VERSION_FILE" >> "$OUTPUT" +fi +if [ -n "$INFO_FILE" ]; then + cat "$INFO_FILE" >> "$OUTPUT" +fi +exit 0 diff --git a/python/private/cc/py_extension_macro.bzl b/python/private/cc/py_extension_macro.bzl new file mode 100644 index 0000000000..28027f4857 --- /dev/null +++ b/python/private/cc/py_extension_macro.bzl @@ -0,0 +1,107 @@ +"""Macro for creating Python extensions.""" + +load("@rules_cc//cc:cc_library.bzl", "cc_library") +load("@rules_cc//cc:cc_shared_library.bzl", "cc_shared_library") +load("//python/private:util.bzl", "add_tag", "copy_propagating_kwargs") +load(":py_extension_rule.bzl", "py_extension_wrapper") + +def py_extension( + name, + srcs = None, + hdrs = None, + copts = None, + defines = None, + deps = None, + dynamic_deps = None, + exports_filter = None, + user_link_flags = None, + visibility = None, + data = None, + **kwargs): + """Creates a Python extension module. + + By default, extensions are created within their workspace package directory + (e.g., `pkg/ext.so`) and imported using standard Python package paths + (e.g., `from pkg import ext`). + + To customize import path behavior: + - `imports`: Pass `imports = ["..."]` to append custom search directories to + `sys.path` (matching `py_library`). + - `module_name`: Pass `module_name = "custom_name"` to override the base module + filename. + + Args: + name: Target name. + srcs: Optional C/C++ source files to compile directly for this extension. + hdrs: Optional header files for the srcs. + copts: Optional compiler flags for srcs. + defines: Optional preprocessor defines for srcs. + deps: cc_library targets to statically link into the extension. + dynamic_deps: cc_shared_library targets to dynamically link. + exports_filter: Filter for exported symbols passed to cc_shared_library. + user_link_flags: Additional link flags passed to cc_shared_library. + visibility: Target visibility. + data: Optional list of files or targets needed by this extension at runtime. + **kwargs: Additional arguments passed to the underlying wrapper rule. + """ + add_tag(kwargs, "@rules_python//python/cc:py_extension") + + csl_deps = [] + + # 1. Handle user-supplied static deps + if deps: + csl_deps.extend(deps) + + # 2. If srcs or hdrs are specified, create an implicit cc_library for them + if srcs or hdrs: + impl_lib_name = "_" + name + "_impl" + cc_library( + name = impl_lib_name, + srcs = srcs, + hdrs = hdrs, + copts = (copts or []) + ["-fPIC"], + defines = defines, + deps = ["@rules_python//python/cc:current_py_cc_headers"], + visibility = ["//visibility:private"], + **copy_propagating_kwargs(kwargs) + ) + csl_deps.append(":" + impl_lib_name) + + # 3. If no static deps or sources were specified, use empty target for CSL requirement + if not csl_deps: + csl_deps.append("//python/private/cc:empty") + + # 4. Create the underlying cc_shared_library + csl_name = "_" + name + "_csl" + csl_kwargs = copy_propagating_kwargs(kwargs) + if exports_filter: + csl_kwargs["exports_filter"] = exports_filter + if user_link_flags: + csl_kwargs["user_link_flags"] = user_link_flags + + cc_shared_library( + name = csl_name, + deps = csl_deps, + dynamic_deps = dynamic_deps, + visibility = ["//visibility:private"], + **csl_kwargs + ) + + # 5. Select default libc constraint if not provided + if "libc" not in kwargs: + kwargs["libc"] = select({ + "@rules_python//python/config_settings:_is_py_linux_libc_glibc": "glibc", + "@rules_python//python/config_settings:_is_py_linux_libc_musl": "musl", + "//conditions:default": "glibc", + }) + + if data != None: + kwargs["data"] = data + + # 6. Wrap with py_extension_wrapper for PEP 3149 naming & PyInfo + py_extension_wrapper( + name = name, + src = ":" + csl_name, + visibility = visibility, + **kwargs + ) diff --git a/python/private/cc/py_extension_rule.bzl b/python/private/cc/py_extension_rule.bzl new file mode 100644 index 0000000000..e38f9288c3 --- /dev/null +++ b/python/private/cc/py_extension_rule.bzl @@ -0,0 +1,149 @@ +"""Implementation of the _py_extension_wrapper rule.""" + +load("@bazel_skylib//lib:dicts.bzl", "dicts") +load("@rules_cc//cc/common:cc_shared_library_info.bzl", "CcSharedLibraryInfo") +load("//python/private:attr_builders.bzl", "attrb") +load("//python/private:attributes.bzl", "COMMON_ATTRS", "IMPORTS_ATTRS") +load("//python/private:builders.bzl", "builders") +load("//python/private:py_info.bzl", "PyInfo") +load("//python/private:rule_builders.bzl", "ruleb") +load("//python/private:toolchain_types.bzl", "PY_CC_TOOLCHAIN_TYPE") + +def _py_extension_wrapper_impl(ctx): + module_name = ctx.attr.module_name or ctx.label.name + + cc_toolchain = ctx.toolchains["@bazel_tools//tools/cpp:toolchain_type"].cc + ext = _get_extension(cc_toolchain) + use_py_limited_api = bool(ctx.attr.py_limited_api) + if use_py_limited_api: + output_filename = "{module_name}.abi3.{ext}".format( + module_name = module_name, + ext = ext, + ) + else: + py_toolchain = ctx.toolchains[PY_CC_TOOLCHAIN_TYPE] + py_cc_toolchain = py_toolchain.py_cc_toolchain + platform_tag = _get_platform(ctx) + output_filename = "{module_name}.{abi_tag}-{platform}.{ext}".format( + module_name = module_name, + abi_tag = py_cc_toolchain.abi_tag, + platform = platform_tag, + ext = ext, + ) + + py_dso = ctx.actions.declare_file(output_filename) + + # Symlink the cc_shared_library output to the PEP 3149 / abi3 filename + csl_target = ctx.attr.src + csl_file = csl_target[DefaultInfo].files.to_list()[0] + ctx.actions.symlink( + output = py_dso, + target_file = csl_file, + ) + + runfiles_builder = builders.RunfilesBuilder() + runfiles_builder.add(py_dso) + runfiles_builder.add(ctx.files.data) + runfiles_builder.add_targets(ctx.attr.data) + runfiles_builder.add(csl_target[DefaultInfo].default_runfiles) + runfiles = runfiles_builder.build(ctx) + + # Resolve imports paths relative to the target package and repository: + # 1. Default (imports = []): No extra search paths are added to sys.path, + # enforcing clean package-qualified imports (e.g. `from foo.bar import ext`). + # 2. Relative paths (e.g. imports = ["."]): Resolved relative to `repo_name/package_name` + # so passing `imports = ["."]` adds the target's package directory to sys.path. + # 3. Absolute paths (starting with "/"): Stripped of leading "/" and resolved relative to runfiles root. + imports_list = [] + repo_name = ctx.label.workspace_name or ctx.workspace_name + for path in ctx.attr.imports: + if path.startswith("/"): + imports_list.append(path[1:]) + else: + pkg = ctx.label.package + full_path = "{}/{}".format(pkg, path) if pkg else path + if repo_name: + full_path = "{}/{}".format(repo_name, full_path) + imports_list.append(full_path) + + return [ + DefaultInfo( + files = depset([py_dso]), + runfiles = runfiles, + ), + PyInfo( + transitive_sources = depset([py_dso]), + imports = depset(imports_list), + ), + ] + +PY_EXTENSION_WRAPPER_ATTRS = dicts.add( + COMMON_ATTRS, + IMPORTS_ATTRS, + { + "libc": lambda: attrb.String(default = "glibc"), + "module_name": lambda: attrb.String(), + "py_limited_api": lambda: attrb.String( + default = "", + ), + "src": lambda: attrb.Label( + mandatory = True, + providers = [CcSharedLibraryInfo], + doc = "The cc_shared_library target to wrap.", + ), + }, +) + +def create_py_extension_wrapper_rule_builder(**kwargs): + """Create a rule builder for the wrapper.""" + builder = ruleb.Rule( + implementation = _py_extension_wrapper_impl, + attrs = PY_EXTENSION_WRAPPER_ATTRS, + provides = [PyInfo], + toolchains = [ + ruleb.ToolchainType(PY_CC_TOOLCHAIN_TYPE), + ruleb.ToolchainType("@bazel_tools//tools/cpp:toolchain_type"), + ], + fragments = ["cpp"], + **kwargs + ) + return builder + +py_extension_wrapper = create_py_extension_wrapper_rule_builder().build() + +def _get_extension(cc_toolchain): + """ + Derives the appropriate file extension from the C++ toolchain. + + Args: + cc_toolchain: The CcToolchainInfo provider (usually obtained via + ctx.toolchains["@bazel_tools//tools/cpp:toolchain_type"].cc) + + Returns: + The extension, e.g. "so" or "pyd" + """ + + # Windows uses .pyd; Unix (Linux/macOS) uses .so for Python modules + target_name = cc_toolchain.target_gnu_system_name + is_windows = "windows" in target_name or "mingw" in target_name or "msvc" in target_name + ext = "pyd" if is_windows else "so" + return ext + +def _get_platform(ctx): + """Derives the PEP 3149 platform tag from the active Python C++ toolchain. + + Args: + ctx: The rule context. + + Returns: + The platform tag, e.g. "x86_64-linux-gnu" or "win_amd64" + """ + py_toolchain = ctx.toolchains[PY_CC_TOOLCHAIN_TYPE] + py_cc_toolchain = py_toolchain.py_cc_toolchain + if hasattr(py_cc_toolchain, "platform_tag") and py_cc_toolchain.platform_tag: + return py_cc_toolchain.platform_tag + + fail( + "ERROR: Unable to resolve platform_tag from Python C++ toolchain for {self}. " + + "Please ensure the active py_cc_toolchain provides a non-empty platform_tag.", + ) diff --git a/python/private/common.bzl b/python/private/common.bzl index 33b175c247..7e8c6decf3 100644 --- a/python/private/common.bzl +++ b/python/private/common.bzl @@ -16,6 +16,10 @@ load("@bazel_skylib//lib:paths.bzl", "paths") load("@rules_cc//cc/common:cc_common.bzl", "cc_common") load("@rules_cc//cc/common:cc_info.bzl", "CcInfo") +load("@rules_python_internal//:rules_python_config.bzl", "config") +load("//python/private:py_interpreter_program.bzl", "PyInterpreterProgramInfo") +load("//python/private:toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE", "LAUNCHER_MAKER_TOOLCHAIN_TYPE") +load(":builders.bzl", "builders") load(":cc_helper.bzl", "cc_helper") load(":py_cc_link_params_info.bzl", "PyCcLinkParamsInfo") load(":py_info.bzl", "PyInfo", "PyInfoBuilder") @@ -36,120 +40,113 @@ PYTHON_FILE_EXTENSIONS = [ "dylib", # Python C modules, Mac specific "py", "pyc", + "pth", # import 'pth' files "pyi", "so", # Python C modules, usually Linux ] +BUILTIN_BUILD_PYTHON_ZIP = [] if config.bazel_10_or_later else [ + "//command_line_option:build_python_zip", +] + +# Not an actual provider. Provider used for memory efficiency. +# buildifier: disable=name-conventions +ExplicitSymlink = provider( + doc = """ +A runfile that should be created as a symlink pointing to a specific location. + +This is only needed on Windows, where Bazel doesn't preserve declare_symlink +with relative paths. This is basically manually captures what using +declare_symlink(), symlink() and runfiles like so would capture: + +``` +link = declare_symlink(...) +link_to_path = relative_path(from=link, to=target) +symlink(output=link, target_path=link_to_path) +runfiles.add([link, target]) +``` +""", + fields = { + "files": "depset[File] of files that should be included if this symlink is used", + "link_to_path": "Path the symlink should point to", + "runfiles_path": "runfiles-root-relative path for the symlink", + "venv_path": "venv-root-relative path for the symlink", + }, +) + +def maybe_builtin_build_python_zip(value, settings = None): + settings = settings or {} + if not config.bazel_10_or_later: + settings["//command_line_option:build_python_zip"] = value + + return settings + +def _find_launcher_maker(ctx): + if config.bazel_9_or_later: + return (ctx.toolchains[LAUNCHER_MAKER_TOOLCHAIN_TYPE].binary, LAUNCHER_MAKER_TOOLCHAIN_TYPE) + return (ctx.executable._windows_launcher_maker, None) + +def create_windows_exe_launcher( + ctx, + *, + output, + python_binary_path, + use_zip_file): + """Creates a Windows exe launcher. + + Args: + ctx: The rule context. + output: The output file for the launcher. + python_binary_path: The path to the Python binary. + use_zip_file: Whether to use a zip file. + """ + launch_info = ctx.actions.args() + launch_info.use_param_file("%s", use_always = True) + launch_info.set_param_file_format("multiline") + launch_info.add("binary_type=Python") + launch_info.add(ctx.workspace_name, format = "workspace_name=%s") + launch_info.add( + "1" if py_internal.runfiles_enabled(ctx) else "0", + format = "symlink_runfiles_enabled=%s", + ) + launch_info.add(python_binary_path, format = "python_bin_path=%s") + launch_info.add("1" if use_zip_file else "0", format = "use_zip_file=%s") + + launcher = ctx.attr._launcher[DefaultInfo].files_to_run.executable + executable, toolchain = _find_launcher_maker(ctx) + ctx.actions.run( + executable = executable, + arguments = [launcher.path, launch_info, output.path], + inputs = [launcher], + outputs = [output], + mnemonic = "PyBuildLauncher", + progress_message = "Creating launcher for %{label}", + # Needed to inherit PATH when using non-MSVC compilers like MinGW + use_default_shell_env = True, + toolchain = toolchain, + ) + def create_binary_semantics_struct( *, - create_executable, - get_cc_details_for_binary, - get_central_uncachable_version_file, - get_coverage_deps, - get_debugger_deps, - get_extra_common_runfiles_for_binary, - get_extra_providers, - get_extra_write_build_data_env, - get_interpreter_path, - get_imports, get_native_deps_dso_name, - get_native_deps_user_link_flags, - get_stamp_flag, - maybe_precompile, - should_build_native_deps_dso, - should_create_init_files, - should_include_build_data): + should_build_native_deps_dso): """Helper to ensure a semantics struct has all necessary fields. Call this instead of a raw call to `struct(...)`; it'll help ensure all the necessary functions are being correctly provided. Args: - create_executable: Callable; creates a binary's executable output. See - py_executable.bzl#py_executable_base_impl for details. - get_cc_details_for_binary: Callable that returns a `CcDetails` struct; see - `create_cc_detail_struct`. - get_central_uncachable_version_file: Callable that returns an optional - Artifact; this artifact is special: it is never cached and is a copy - of `ctx.version_file`; see py_builtins.copy_without_caching - get_coverage_deps: Callable that returns a list of Targets for making - coverage work; only called if coverage is enabled. - get_debugger_deps: Callable that returns a list of Targets that provide - custom debugger support; only called for target-configuration. - get_extra_common_runfiles_for_binary: Callable that returns a runfiles - object of extra runfiles a binary should include. - get_extra_providers: Callable that returns extra providers; see - py_executable.bzl#_create_providers for details. - get_extra_write_build_data_env: Callable that returns a dict[str, str] - of additional environment variable to pass to build data generation. - get_interpreter_path: Callable that returns an optional string, which is - the path to the Python interpreter to use for running the binary. - get_imports: Callable that returns a list of the target's import - paths (from the `imports` attribute, so just the target's own import - path strings, not from dependencies). get_native_deps_dso_name: Callable that returns a string, which is the basename (with extension) of the native deps DSO library. - get_native_deps_user_link_flags: Callable that returns a list of strings, - which are any extra linker flags to pass onto the native deps DSO - linking action. - get_stamp_flag: Callable that returns bool of if the --stamp flag was - enabled or not. - maybe_precompile: Callable that may optional precompile the input `.py` - sources and returns the full set of desired outputs derived from - the source files (e.g., both py and pyc, only one of them, etc). should_build_native_deps_dso: Callable that returns bool; True if building a native deps DSO is supported, False if not. - should_create_init_files: Callable that returns bool; True if - `__init__.py` files should be generated, False if not. - should_include_build_data: Callable that returns bool; True if - build data should be generated, False if not. Returns: A "BinarySemantics" struct. """ return struct( # keep-sorted - create_executable = create_executable, - get_cc_details_for_binary = get_cc_details_for_binary, - get_central_uncachable_version_file = get_central_uncachable_version_file, - get_coverage_deps = get_coverage_deps, - get_debugger_deps = get_debugger_deps, - get_extra_common_runfiles_for_binary = get_extra_common_runfiles_for_binary, - get_extra_providers = get_extra_providers, - get_extra_write_build_data_env = get_extra_write_build_data_env, - get_imports = get_imports, - get_interpreter_path = get_interpreter_path, get_native_deps_dso_name = get_native_deps_dso_name, - get_native_deps_user_link_flags = get_native_deps_user_link_flags, - get_stamp_flag = get_stamp_flag, - maybe_precompile = maybe_precompile, should_build_native_deps_dso = should_build_native_deps_dso, - should_create_init_files = should_create_init_files, - should_include_build_data = should_include_build_data, - ) - -def create_library_semantics_struct( - *, - get_cc_info_for_library, - get_imports, - maybe_precompile): - """Create a `LibrarySemantics` struct. - - Call this instead of a raw call to `struct(...)`; it'll help ensure all - the necessary functions are being correctly provided. - - Args: - get_cc_info_for_library: Callable that returns a CcInfo for the library; - see py_library_impl for arg details. - get_imports: Callable; see create_binary_semantics_struct. - maybe_precompile: Callable; see create_binary_semantics_struct. - Returns: - a `LibrarySemantics` struct. - """ - return struct( - # keep sorted - get_cc_info_for_library = get_cc_info_for_library, - get_imports = get_imports, - maybe_precompile = maybe_precompile, ) def create_cc_details_struct( @@ -197,27 +194,6 @@ def create_cc_details_struct( **kwargs ) -def create_executable_result_struct(*, extra_files_to_build, output_groups, extra_runfiles = None): - """Creates a `CreateExecutableResult` struct. - - This is the return value type of the semantics create_executable function. - - Args: - extra_files_to_build: depset of File; additional files that should be - included as default outputs. - output_groups: dict[str, depset[File]]; additional output groups that - should be returned. - extra_runfiles: A runfiles object of additional runfiles to include. - - Returns: - A `CreateExecutableResult` struct. - """ - return struct( - extra_files_to_build = extra_files_to_build, - output_groups = output_groups, - extra_runfiles = extra_runfiles, - ) - def csv(values): """Convert a list of strings to comma separated value string.""" return ", ".join(sorted(values)) @@ -240,12 +216,8 @@ def collect_cc_info(ctx, extra_deps = []): Returns: CcInfo provider of merged information. """ - deps = ctx.attr.deps - if extra_deps: - deps = list(deps) - deps.extend(extra_deps) cc_infos = [] - for dep in deps: + for dep in collect_deps(ctx, extra_deps): if CcInfo in dep: cc_infos.append(dep[CcInfo]) @@ -254,29 +226,28 @@ def collect_cc_info(ctx, extra_deps = []): return cc_common.merge_cc_infos(cc_infos = cc_infos) -def collect_imports(ctx, semantics): +def collect_imports(ctx, extra_deps = []): """Collect the direct and transitive `imports` strings. Args: ctx: {type}`ctx` the current target ctx - semantics: semantics object for fetching direct imports. + extra_deps: list of Target to also collect imports from. Returns: {type}`depset[str]` of import paths """ + transitive = [] - for dep in ctx.attr.deps: + for dep in collect_deps(ctx, extra_deps): if PyInfo in dep: transitive.append(dep[PyInfo].imports) if BuiltinPyInfo != None and BuiltinPyInfo in dep: transitive.append(dep[BuiltinPyInfo].imports) - return depset(direct = semantics.get_imports(ctx), transitive = transitive) + return depset(direct = get_imports(ctx), transitive = transitive) def get_imports(ctx): """Gets the imports from a rule's `imports` attribute. - See create_binary_semantics_struct for details about this function. - Args: ctx: Rule ctx. @@ -514,6 +485,17 @@ def target_platform_has_any_constraint(ctx, constraints): return True return False +def is_windows_platform(ctx): + """Check if target platform is windows. + + Args: + ctx: rule context. + + Returns: + True if target platform is windows. + """ + return target_platform_has_any_constraint(ctx, ctx.attr._windows_constraints) + def relative_path(from_, to): """Compute a relative path from one path to another. @@ -561,3 +543,152 @@ def runfiles_root_path(ctx, short_path): return short_path[3:] else: return "{}/{}".format(ctx.workspace_name, short_path) + +def collect_deps(ctx, extra_deps = []): + """Collect the dependencies from the rule's context. + + Args: + ctx: rule ctx + extra_deps: list of Target to also collect dependencies from. + + Returns: + list of Target + """ + deps = ctx.attr.deps + if extra_deps: + deps = list(deps) + deps.extend(extra_deps) + return deps + +def maybe_create_repo_mapping(ctx, *, runfiles): + """Creates a repo mapping manifest if bzlmod is enabled. + + There isn't a way to reference the repo mapping Bazel implicitly + creates, so we have to manually create it ourselves. + + Args: + ctx: rule ctx. + runfiles: runfiles object to generate mapping for. + + Returns: + File object if the repo mapping manifest was created, None otherwise. + """ + if not py_internal.is_bzlmod_enabled(ctx): + return None + + # We have to add `.custom` because `{name}.repo_mapping` is used by Bazel + # internally. + repo_mapping_manifest = ctx.actions.declare_file(ctx.label.name + ".custom.repo_mapping") + py_internal.create_repo_mapping_manifest( + ctx = ctx, + runfiles = runfiles, + output = repo_mapping_manifest, + ) + return repo_mapping_manifest + +def actions_run( + ctx, + *, + executable, + toolchain = None, + **kwargs): + """Runs a tool as an action, supporting py_interpreter_program targets. + + This is wrapper around `ctx.actions.run()` that sets some useful defaults, + supports handling `py_interpreter_program` targets, and some other features + to let the target being run influence the action invocation. + + Args: + ctx: The rule context. The rule must have the + `//python:exec_tools_toolchain_type` toolchain available. + executable: The executable to run. This can be a target that provides + `PyInterpreterProgramInfo` or a regular executable target. If it + provides `testing.ExecutionInfo`, the requirements will be added to + the execution requirements. + toolchain: The toolchain type to use. Must be None or + `//python:exec_tools_toolchain_type`. + **kwargs: Additional arguments to pass to `ctx.actions.run()`. + `mnemonic` and `progress_message` are required. + """ + mnemonic = kwargs.pop("mnemonic", None) + if not mnemonic: + fail("actions_run: missing required argument 'mnemonic'") + + progress_message = kwargs.pop("progress_message", None) + if not progress_message: + fail("actions_run: missing required argument 'progress_message'") + + tools = kwargs.pop("tools", None) + tools = list(tools) if tools else [] + + action_arguments = [] + action_env = { + "PYTHONHASHSEED": "0", # Helps avoid non-deterministic behavior + "PYTHONNOUSERSITE": "1", # Helps avoid non-deterministic behavior + "PYTHONSAFEPATH": "1", # Helps avoid incorrect import issues + } + default_info = executable[DefaultInfo] + action_inputs = builders.DepsetBuilder() + action_inputs.add(kwargs.pop("inputs", None) or []) + if PyInterpreterProgramInfo in executable: + if toolchain and toolchain != EXEC_TOOLS_TOOLCHAIN_TYPE: + fail(("Action {}: tool {} provides PyInterpreterProgramInfo, which " + + "requires the `toolchain` arg be " + + "None or {}, got: {}").format( + mnemonic, + executable, + EXEC_TOOLS_TOOLCHAIN_TYPE, + toolchain, + )) + exec_runtime = ctx.toolchains[EXEC_TOOLS_TOOLCHAIN_TYPE].exec_tools.exec_runtime + if exec_runtime.interpreter: + action_exe = exec_runtime.interpreter + action_inputs.add(exec_runtime.files) + elif exec_runtime.interpreter_path: + action_exe = exec_runtime.interpreter_path + else: + fail(("Action {}: PyRuntimeInfo from exec tools toolchain is " + + "malformed: requires one of `interpreter` or " + + "`interpreter_path` set").format( + mnemonic, + )) + + program_info = executable[PyInterpreterProgramInfo] + + interpreter_args = ctx.actions.args() + interpreter_args.add_all(program_info.interpreter_args) + interpreter_args.add(default_info.files_to_run.executable) + action_arguments.append(interpreter_args) + + action_env.update(program_info.env) + + tools.append(default_info.files_to_run) + toolchain = EXEC_TOOLS_TOOLCHAIN_TYPE + else: + action_exe = executable[DefaultInfo].files_to_run + + execution_requirements = {} + if testing.ExecutionInfo in executable: + execution_requirements.update(executable[testing.ExecutionInfo].requirements) + + # Give precedence to caller's execution requirements. + execution_requirements.update(kwargs.pop("execution_requirements", None) or {}) + + # Give precedence to caller's env. + action_env.update(kwargs.pop("env", None) or {}) + + # Handle arguments=None + action_arguments.extend(list(kwargs.pop("arguments", None) or [])) + + ctx.actions.run( + executable = action_exe, + arguments = action_arguments, + tools = tools, + env = action_env, + execution_requirements = execution_requirements, + toolchain = toolchain, + mnemonic = mnemonic, + progress_message = progress_message, + inputs = action_inputs.build(), + **kwargs + ) diff --git a/python/private/common_labels.bzl b/python/private/common_labels.bzl index 4a6f6d3f0f..6cc42ecf8d 100644 --- a/python/private/common_labels.bzl +++ b/python/private/common_labels.bzl @@ -7,23 +7,30 @@ labels = struct( # keep sorted ADD_SRCS_TO_RUNFILES = str(Label("//python/config_settings:add_srcs_to_runfiles")), BOOTSTRAP_IMPL = str(Label("//python/config_settings:bootstrap_impl")), + BUILD_PYTHON_ZIP = str(Label("//python/config_settings:build_python_zip")), + # NOTE: Special target; see definition for details. + BUILD_RUNFILE_LINKS = str(Label("//command_line_option:build_runfile_links")), + DEBUGGER = str(Label("//python/config_settings:debugger")), + # NOTE: Special target; see definition for details. + ENABLE_RUNFILES = str(Label("//command_line_option:enable_runfiles")), EXEC_TOOLS_TOOLCHAIN = str(Label("//python/config_settings:exec_tools_toolchain")), - PIP_ENV_MARKER_CONFIG = str(Label("//python/config_settings:pip_env_marker_config")), + EXTRA_TOOLCHAINS = str(Label("//command_line_option:extra_toolchains")), NONE = str(Label("//python:none")), - PIP_WHL = str(Label("//python/config_settings:pip_whl")), - PIP_WHL_GLIBC_VERSION = str(Label("//python/config_settings:pip_whl_glibc_version")), - PIP_WHL_MUSLC_VERSION = str(Label("//python/config_settings:pip_whl_muslc_version")), - PIP_WHL_OSX_ARCH = str(Label("//python/config_settings:pip_whl_osx_arch")), + PIP_ENV_MARKER_CONFIG = str(Label("//python/config_settings:pip_env_marker_config")), PIP_WHL_OSX_VERSION = str(Label("//python/config_settings:pip_whl_osx_version")), + PLATFORMS_OS_WINDOWS = str(Label("@platforms//os:windows")), PRECOMPILE = str(Label("//python/config_settings:precompile")), PRECOMPILE_SOURCE_RETENTION = str(Label("//python/config_settings:precompile_source_retention")), PYC_COLLECTION = str(Label("//python/config_settings:pyc_collection")), + PYTHON_IMPORT_ALL_REPOSITORIES = str(Label("//python/config_settings:experimental_python_import_all_repositories")), PYTHON_SRC = str(Label("//python/bin:python_src")), PYTHON_VERSION = str(Label("//python/config_settings:python_version")), PYTHON_VERSION_MAJOR_MINOR = str(Label("//python/config_settings:python_version_major_minor")), PY_FREETHREADED = str(Label("//python/config_settings:py_freethreaded")), PY_LINUX_LIBC = str(Label("//python/config_settings:py_linux_libc")), REPL_DEP = str(Label("//python/bin:repl_dep")), + VALIDATE_TEST_MAIN = str(Label("//python/config_settings:validate_test_main")), + VENV = str(Label("//python/config_settings:venv")), VENVS_SITE_PACKAGES = str(Label("//python/config_settings:venvs_site_packages")), VENVS_USE_DECLARE_SYMLINK = str(Label("//python/config_settings:venvs_use_declare_symlink")), VISIBLE_FOR_TESTING = str(Label("//python/private:visible_for_testing")), diff --git a/python/private/config_settings.bzl b/python/private/config_settings.bzl index 3089b9c6cf..9cd729aff2 100644 --- a/python/private/config_settings.bzl +++ b/python/private/config_settings.bzl @@ -17,8 +17,9 @@ load("@bazel_skylib//lib:selects.bzl", "selects") load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") -load("//python/private:text_util.bzl", "render") +load(":text_util.bzl", "render") load(":version.bzl", "version") +load(":visibility.bzl", "NOT_ACTUALLY_PUBLIC") _PYTHON_VERSION_FLAG = Label("//python/config_settings:python_version") _PYTHON_VERSION_MAJOR_MINOR_FLAG = Label("//python/config_settings:python_version_major_minor") @@ -31,11 +32,14 @@ If the value is missing, then the default value is being used, see documentation {docs_url}/python/config_settings """ -# Indicates something needs public visibility so that other generated code can -# access it, but it's not intended for general public usage. -_NOT_ACTUALLY_PUBLIC = ["//visibility:public"] - -def construct_config_settings(*, name, default_version, versions, minor_mapping, documented_flags): # buildifier: disable=function-docstring +def construct_config_settings( + *, + name, + default_version, + versions, + minor_mapping, + compat_lowest_version = "3.8", + documented_flags): # buildifier: disable=function-docstring """Create a 'python_version' config flag and construct all config settings used in rules_python. This mainly includes the targets that are used in the toolchain and pip hub @@ -46,6 +50,8 @@ def construct_config_settings(*, name, default_version, versions, minor_mapping, default_version: {type}`str` the default value for the `python_version` flag. versions: {type}`list[str]` A list of versions to build constraint settings for. minor_mapping: {type}`dict[str, str]` A mapping from `X.Y` to `X.Y.Z` python versions. + compat_lowest_version: {type}`str` The version that we should use as the lowest available + version for `is_python_3.X` flags. documented_flags: {type}`list[str]` The labels of the documented settings that affect build configuration. """ @@ -69,22 +75,22 @@ def construct_config_settings(*, name, default_version, versions, minor_mapping, ) _reverse_minor_mapping = {full: minor for minor, full in minor_mapping.items()} - for version in versions: - minor_version = _reverse_minor_mapping.get(version) + for ver in versions: + minor_version = _reverse_minor_mapping.get(ver) if not minor_version: native.config_setting( - name = "is_python_{}".format(version), - flag_values = {":python_version": version}, + name = "is_python_{}".format(ver), + flag_values = {":python_version": ver}, visibility = ["//visibility:public"], ) continue # Also need to match the minor version when using - name = "is_python_{}".format(version) + name = "is_python_{}".format(ver) native.config_setting( name = "_" + name, - flag_values = {":python_version": version}, - visibility = ["//visibility:public"], + flag_values = {":python_version": ver}, + visibility = NOT_ACTUALLY_PUBLIC, ) # An alias pointing to an underscore-prefixed config_setting_group @@ -94,7 +100,7 @@ def construct_config_settings(*, name, default_version, versions, minor_mapping, selects.config_setting_group( name = "_{}_group".format(name), match_any = [ - ":_is_python_{}".format(version), + ":_is_python_{}".format(ver), ":is_python_{}".format(minor_version), ], visibility = ["//visibility:private"], @@ -102,20 +108,35 @@ def construct_config_settings(*, name, default_version, versions, minor_mapping, native.alias( name = name, actual = "_{}_group".format(name), - visibility = ["//visibility:public"], + visibility = NOT_ACTUALLY_PUBLIC, ) # This matches the raw flag value, e.g. --//python/config_settings:python_version=3.8 # It's private because matching the concept of e.g. "3.8" value is done # using the `is_python_X.Y` config setting group, which is aware of the # minor versions that could match instead. + first_minor = None for minor in minor_mapping.keys(): + ver = version.parse(minor) + if first_minor == None or version.is_lt(ver, first_minor): + first_minor = ver + native.config_setting( name = "is_python_{}".format(minor), flag_values = {_PYTHON_VERSION_MAJOR_MINOR_FLAG: minor}, visibility = ["//visibility:public"], ) + # This is a compatibility layer to ensure that `select` statements don't break out right + # when the toolchains for EOL minor versions are no longer registered. + compat_lowest_version = version.parse(compat_lowest_version) + for minor in range(compat_lowest_version.release[-1], first_minor.release[-1]): + native.alias( + name = "is_python_3.{}".format(minor), + actual = "@platforms//:incompatible", + visibility = ["//visibility:public"], + ) + _current_config( name = "current_config", build_setting_default = "", @@ -132,30 +153,30 @@ def construct_config_settings(*, name, default_version, versions, minor_mapping, # `whl_library` in the hub repo created by `pip.parse`. flag_values = {"current_config": "will-never-match"}, # Only public so that PyPI hub repo can access it - visibility = _NOT_ACTUALLY_PUBLIC, + visibility = NOT_ACTUALLY_PUBLIC, ) libc = Label("//python/config_settings:py_linux_libc") native.config_setting( name = "_is_py_linux_libc_glibc", flag_values = {libc: "glibc"}, - visibility = _NOT_ACTUALLY_PUBLIC, + visibility = NOT_ACTUALLY_PUBLIC, ) native.config_setting( name = "_is_py_linux_libc_musl", flag_values = {libc: "musl"}, - visibility = _NOT_ACTUALLY_PUBLIC, + visibility = NOT_ACTUALLY_PUBLIC, ) freethreaded = Label("//python/config_settings:py_freethreaded") native.config_setting( name = "_is_py_freethreaded_yes", flag_values = {freethreaded: "yes"}, - visibility = _NOT_ACTUALLY_PUBLIC, + visibility = NOT_ACTUALLY_PUBLIC, ) native.config_setting( name = "_is_py_freethreaded_no", flag_values = {freethreaded: "no"}, - visibility = _NOT_ACTUALLY_PUBLIC, + visibility = NOT_ACTUALLY_PUBLIC, ) def _python_version_flag_impl(ctx): diff --git a/python/private/coverage.patch b/python/private/coverage.patch index 051f7fc543..9b3830332c 100644 --- a/python/private/coverage.patch +++ b/python/private/coverage.patch @@ -8,10 +8,6 @@ diff --git a/coverage/__main__.py b/coverage/__main__.py index ce2d8db..7d7d0a0 100644 --- a/coverage/__main__.py +++ b/coverage/__main__.py -@@ -6,5 +6,6 @@ - from __future__ import annotations - +@@ -8,1 +8,2 @@ import sys +sys.path.append(sys.path.pop(0)) - from coverage.cmdline import main - sys.exit(main()) diff --git a/python/private/coverage_deps.bzl b/python/private/coverage_deps.bzl index e80e8ee910..a32b2e3f97 100644 --- a/python/private/coverage_deps.bzl +++ b/python/private/coverage_deps.bzl @@ -17,124 +17,149 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe") +load("//python/private:repo_utils.bzl", "repo_utils") load("//python/private:version_label.bzl", "version_label") # START: maintained by 'bazel run //tools/private/update_deps:update_coverage_deps ' _coverage_deps = { "cp310": { "aarch64-apple-darwin": ( - "https://files.pythonhosted.org/packages/7d/73/041928e434442bd3afde5584bdc3f932fb4562b1597629f537387cec6f3d/coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", - "cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36", + "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", + "e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", ), "aarch64-unknown-linux-gnu": ( - "https://files.pythonhosted.org/packages/c7/c8/6ca52b5147828e45ad0242388477fdb90df2c6cbb9a441701a12b3c71bc8/coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", - "e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02", + "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", + "6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", ), "x86_64-apple-darwin": ( - "https://files.pythonhosted.org/packages/7e/61/eb7ce5ed62bacf21beca4937a90fe32545c91a3c8a42a30c6616d48fc70d/coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", - "b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", + "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", + "fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", ), "x86_64-unknown-linux-gnu": ( - "https://files.pythonhosted.org/packages/53/23/9e2c114d0178abc42b6d8d5281f651a8e6519abfa0ef460a00a91f80879d/coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", - "8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23", + "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", + "8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", ), }, "cp311": { "aarch64-apple-darwin": ( - "https://files.pythonhosted.org/packages/e1/0e/e52332389e057daa2e03be1fbfef25bb4d626b37d12ed42ae6281d0a274c/coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", - "ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3", + "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", + "65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", ), "aarch64-unknown-linux-gnu": ( - "https://files.pythonhosted.org/packages/aa/cd/766b45fb6e090f20f8927d9c7cb34237d41c73a939358bc881883fd3a40d/coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", - "d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff", + "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", + "4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", ), "x86_64-apple-darwin": ( - "https://files.pythonhosted.org/packages/ad/5f/67af7d60d7e8ce61a4e2ddcd1bd5fb787180c8d0ae0fbd073f903b3dd95d/coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", - "7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93", + "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", + "a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", ), "x86_64-unknown-linux-gnu": ( - "https://files.pythonhosted.org/packages/14/6f/8351b465febb4dbc1ca9929505202db909c5a635c6fdf33e089bbc3d7d85/coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", - "0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6", + "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", + "35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", ), }, "cp312": { "aarch64-apple-darwin": ( - "https://files.pythonhosted.org/packages/e1/ab/6bf00de5327ecb8db205f9ae596885417a31535eeda6e7b99463108782e1/coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", - "5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391", + "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", + "18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", ), "aarch64-unknown-linux-gnu": ( - "https://files.pythonhosted.org/packages/92/8f/2ead05e735022d1a7f3a0a683ac7f737de14850395a826192f0288703472/coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", - "260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8", + "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", + "c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", ), "x86_64-apple-darwin": ( - "https://files.pythonhosted.org/packages/7e/d4/300fc921dff243cd518c7db3a4c614b7e4b2431b0d1145c1e274fd99bd70/coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", - "95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778", + "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", + "7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", ), "x86_64-unknown-linux-gnu": ( - "https://files.pythonhosted.org/packages/1f/0f/c890339dd605f3ebc269543247bdd43b703cce6825b5ed42ff5f2d6122c7/coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", - "c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca", + "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", + "314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", ), }, "cp313": { "aarch64-apple-darwin": ( - "https://files.pythonhosted.org/packages/b9/67/e1413d5a8591622a46dd04ff80873b04c849268831ed5c304c16433e7e30/coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", - "a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", + "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", + "73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", ), "aarch64-apple-darwin-freethreaded": ( - "https://files.pythonhosted.org/packages/c4/ae/b5d58dff26cade02ada6ca612a76447acd69dccdbb3a478e9e088eb3d4b9/coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", - "502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", + "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", + "dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", ), "aarch64-unknown-linux-gnu": ( - "https://files.pythonhosted.org/packages/14/5b/9dec847b305e44a5634d0fb8498d135ab1d88330482b74065fcec0622224/coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", - "d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", + "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", + "c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", ), "aarch64-unknown-linux-gnu-freethreaded": ( - "https://files.pythonhosted.org/packages/b8/d7/62095e355ec0613b08dfb19206ce3033a0eedb6f4a67af5ed267a8800642/coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", - "6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", + "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", + "83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", + ), + "x86_64-apple-darwin": ( + "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", + "981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", + ), + "x86_64-apple-darwin-freethreaded": ( + "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", + "a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", ), "x86_64-unknown-linux-gnu": ( - "https://files.pythonhosted.org/packages/f7/95/d2fd31f1d638df806cae59d7daea5abf2b15b5234016a5ebb502c2f3f7ee/coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", - "78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", + "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", + "10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", ), "x86_64-unknown-linux-gnu-freethreaded": ( - "https://files.pythonhosted.org/packages/8b/61/a7a6a55dd266007ed3b1df7a3386a0d760d014542d72f7c2c6938483b7bd/coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", - "13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", + "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", + "1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", ), }, - "cp38": { + "cp314": { "aarch64-apple-darwin": ( - "https://files.pythonhosted.org/packages/38/ea/cab2dc248d9f45b2b7f9f1f596a4d75a435cb364437c61b51d2eb33ceb0e/coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", - "f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a", + "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", + "212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", + ), + "aarch64-apple-darwin-freethreaded": ( + "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", + "9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", ), "aarch64-unknown-linux-gnu": ( - "https://files.pythonhosted.org/packages/ca/6f/f82f9a500c7c5722368978a5390c418d2a4d083ef955309a8748ecaa8920/coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", - "a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b", + "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", + "813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", + ), + "aarch64-unknown-linux-gnu-freethreaded": ( + "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", + "6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", ), "x86_64-apple-darwin": ( - "https://files.pythonhosted.org/packages/81/d0/d9e3d554e38beea5a2e22178ddb16587dbcbe9a1ef3211f55733924bf7fa/coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", - "6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0", + "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", + "b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", + ), + "x86_64-apple-darwin-freethreaded": ( + "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", + "a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", ), "x86_64-unknown-linux-gnu": ( - "https://files.pythonhosted.org/packages/e4/6e/885bcd787d9dd674de4a7d8ec83faf729534c63d05d51d45d4fa168f7102/coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", - "8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de", + "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", + "bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", + ), + "x86_64-unknown-linux-gnu-freethreaded": ( + "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", + "affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", ), }, "cp39": { "aarch64-apple-darwin": ( - "https://files.pythonhosted.org/packages/a5/fe/137d5dca72e4a258b1bc17bb04f2e0196898fe495843402ce826a7419fe3/coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", - "547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8", + "https://files.pythonhosted.org/packages/52/2f/b9f9daa39b80ece0b9548bbb723381e29bc664822d9a12c2135f8922c22b/coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", + "bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c", ), "aarch64-unknown-linux-gnu": ( - "https://files.pythonhosted.org/packages/78/5b/a0a796983f3201ff5485323b225d7c8b74ce30c11f456017e23d8e8d1945/coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", - "645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2", + "https://files.pythonhosted.org/packages/6a/92/1c1c5a9e8677ce56d42b97bdaca337b2d4d9ebe703d8c174ede52dbabd5f/coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", + "c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594", ), "x86_64-apple-darwin": ( - "https://files.pythonhosted.org/packages/19/d3/d54c5aa83268779d54c86deb39c1c4566e5d45c155369ca152765f8db413/coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", - "abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255", + "https://files.pythonhosted.org/packages/a3/ad/d1c25053764b4c42eb294aae92ab617d2e4f803397f9c7c8295caa77a260/coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", + "fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3", ), "x86_64-unknown-linux-gnu": ( - "https://files.pythonhosted.org/packages/9a/6f/eef79b779a540326fee9520e5542a8b428cc3bfa8b7c8f1022c1ee4fc66c/coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", - "609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc", + "https://files.pythonhosted.org/packages/b0/49/8a070782ce7e6b94ff6a0b6d7c65ba6bc3091d92a92cef4cd4eb0767965c/coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", + "2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40", ), }, } @@ -142,7 +167,7 @@ _coverage_deps = { _coverage_patch = Label("//python/private:coverage.patch") -def coverage_dep(name, python_version, platform, visibility): +def coverage_dep(name, python_version, platform, visibility, logger = None): """Register a single coverage dependency based on the python version and platform. Args: @@ -150,10 +175,19 @@ def coverage_dep(name, python_version, platform, visibility): python_version: The full python version. platform: The platform, which can be found in //python:versions.bzl PLATFORMS dict. visibility: The visibility of the coverage tool. + logger: {type}`repo_utils.logger | None` Optional logger used to emit a + warning when no wheel is available for the (python_version, + platform) pair. If not supplied, a default logger is constructed. Returns: The label of the coverage tool if the platform is supported, otherwise - None. """ + if logger == None: + logger = repo_utils.logger( + struct(getenv = lambda _: None), + name = "coverage_dep", + ) + if "windows" in platform: # NOTE @aignas 2023-01-19: currently we do not support windows as the # upstream coverage wrapper is written in shell. Do not log any warning @@ -164,7 +198,14 @@ def coverage_dep(name, python_version, platform, visibility): url, sha256 = _coverage_deps.get(abi, {}).get(platform, (None, "")) if url == None: - # Some wheels are not present for some builds, so let's silently ignore those. + logger.warn(lambda: ( + "rules_python's bundled coverage tool has no wheel for " + + "python_version={}, platform={}. `bazel coverage` will produce " + + "empty lcov for py_test targets in this configuration. Either " + + "pin python_version to a version in the bundled set (see " + + "python/private/coverage_deps.bzl), or configure coverage " + + "manually via py_runtime.coverage_tool. See docs/coverage.md." + ).format(python_version, platform)) return None maybe( diff --git a/python/private/current_py_cc_headers.bzl b/python/private/current_py_cc_headers.bzl index ef646317a6..f7fcd8d738 100644 --- a/python/private/current_py_cc_headers.bzl +++ b/python/private/current_py_cc_headers.bzl @@ -76,7 +76,7 @@ cc_library( ) ``` -:::{versionadded} VERSION_NEXT_FEATURE +:::{versionadded} 1.7.0 ::: """, ) diff --git a/python/private/envsubst.bzl b/python/private/envsubst.bzl index b2fdb99e1e..98dc72a818 100644 --- a/python/private/envsubst.bzl +++ b/python/private/envsubst.bzl @@ -20,9 +20,7 @@ def envsubst(template_string, varnames, getenv): Supports `$VARNAME`, `${VARNAME}` and `${VARNAME:-default}` syntaxes in the `template_string`, looking up each `VARNAME` listed in the `varnames` list in the environment defined by the - `getenv` function. Typically called with `getenv = rctx.getenv` - (if it is available) or `getenv = rctx.os.environ.get` (on e.g. - Bazel 6 or Bazel 7, which don't have `rctx.getenv` yet). + `getenv` function. Typically called with `getenv = rctx.getenv`. Limitations: Unlike the shell, we don't support `${VARNAME}` and `${VARNAME:-default}` in the default expression for a different diff --git a/python/private/flags.bzl b/python/private/flags.bzl index 82ec83294b..32769e4084 100644 --- a/python/private/flags.bzl +++ b/python/private/flags.bzl @@ -39,7 +39,6 @@ load(":enum.bzl", "FlagEnum", "enum") _POSSIBLY_NATIVE_FLAGS = { "build_python_zip": (lambda ctx: ctx.fragments.py.build_python_zip, "native"), "default_to_explicit_init_py": (lambda ctx: ctx.fragments.py.default_to_explicit_init_py, "native"), - "disable_py2": (lambda ctx: ctx.fragments.py.disable_py2, "native"), "python_import_all_repositories": (lambda ctx: ctx.fragments.bazel_py.python_import_all_repositories, "native"), "python_path": (lambda ctx: ctx.fragments.bazel_py.python_path, "native"), } @@ -72,7 +71,7 @@ def read_possibly_native_flag(ctx, flag_name): return _POSSIBLY_NATIVE_FLAGS[flag_name][0](ctx) else: # Starlark definition of "--foo" is assumed to be a label dependency named "_foo". - return getattr(ctx.attr, "_" + flag_name)[BuildSettingInfo].value + return getattr(ctx.attr, "_" + flag_name + "_flag")[BuildSettingInfo].value def _AddSrcsToRunfilesFlag_is_enabled(ctx): value = ctx.attr._add_srcs_to_runfiles_flag[BuildSettingInfo].value @@ -88,6 +87,27 @@ AddSrcsToRunfilesFlag = FlagEnum( is_enabled = _AddSrcsToRunfilesFlag_is_enabled, ) +def _ValidateTestMainFlag_is_enabled(ctx): + value = ctx.attr._validate_test_main_flag[BuildSettingInfo].value + if value == ValidateTestMainFlag.AUTO: + # Default off; intended to be flipped to enabled in a future major + # version (e.g. rules_python 3.0). + value = ValidateTestMainFlag.DISABLED + return value == ValidateTestMainFlag.ENABLED + +# Determines if py_test runs a validation action that statically checks the +# main module actually runs tests (instead of silently passing). +# buildifier: disable=name-conventions +ValidateTestMainFlag = FlagEnum( + # Automatically decide the effective value; currently resolves to disabled. + AUTO = "auto", + # Run the validation action. + ENABLED = "enabled", + # Don't run the validation action. + DISABLED = "disabled", + is_enabled = _ValidateTestMainFlag_is_enabled, +) + def _string_flag_impl(ctx): if ctx.attr.override: value = ctx.attr.override diff --git a/python/private/full_version.bzl b/python/private/full_version.bzl index 0292d6c77d..0be5b44daf 100644 --- a/python/private/full_version.bzl +++ b/python/private/full_version.bzl @@ -14,12 +14,13 @@ """A small helper to ensure that we are working with full versions.""" -def full_version(*, version, minor_mapping): +def full_version(*, version, minor_mapping, fail_on_err = True): """Return a full version. Args: version: {type}`str` the version in `X.Y` or `X.Y.Z` format. minor_mapping: {type}`dict[str, str]` mapping between `X.Y` to `X.Y.Z` format. + fail_on_err: {type}`bool` whether to fail on error or return `None` instead. Returns: a full version given the version string. If the string is already a @@ -31,6 +32,8 @@ def full_version(*, version, minor_mapping): parts = version.split(".") if len(parts) == 3: return version + elif not fail_on_err: + return None elif len(parts) == 2: fail( "Unknown Python version '{}', available values are: {}".format( diff --git a/python/private/get_local_runtime_info.py b/python/private/get_local_runtime_info.py index d176b1a7c6..787fad5635 100644 --- a/python/private/get_local_runtime_info.py +++ b/python/private/get_local_runtime_info.py @@ -11,19 +11,29 @@ # 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. - """Returns information about the local Python runtime as JSON.""" +import glob import json import os import sys import sysconfig +from typing import Any _IS_WINDOWS = sys.platform == "win32" _IS_DARWIN = sys.platform == "darwin" -def _search_directories(get_config, base_executable): +def _get_abi_flags(get_config) -> str: + """Returns the ABI flags for the Python runtime.""" + # sys.abiflags may not exist, but it still may be set in the config. + abi_flags = getattr(sys, "abiflags", None) + if abi_flags is None: + abi_flags = get_config("ABIFLAGS") or get_config("abiflags") or "" + return abi_flags + + +def _search_directories(get_config, base_executable) -> list[str]: """Returns a list of library directories to search for shared libraries.""" # There's several types of libraries with different names and a plethora # of settings, and many different config variables to check: @@ -67,16 +77,36 @@ def _search_directories(get_config, base_executable): lib_dirs.append(os.path.join(exec_dir, "lib")) lib_dirs.append(os.path.join(exec_dir, "libs")) else: - # On most systems the executable is in a bin/ directory and the libraries - # are in a sibling lib/ directory. + # On most non-windows systems the executable is in a bin/ directory and + # the libraries are in a sibling lib/ directory. lib_dirs.append(os.path.join(os.path.dirname(exec_dir), "lib")) # Dedup and remove empty values, keeping the order. - lib_dirs = [v for v in lib_dirs if v] - return {k: None for k in lib_dirs}.keys() + return list(dict.fromkeys(d for d in lib_dirs if d)) -def _search_library_names(get_config): +def _default_library_names(version, abi_flags) -> tuple[str, ...]: + """Returns a list of default library files to search for shared libraries.""" + if _IS_WINDOWS: + return ( + f"python{version}{abi_flags}.dll", + f"python{version}.dll", + ) + elif _IS_DARWIN: + return ( + f"libpython{version}{abi_flags}.dylib", + f"libpython{version}.dylib", + ) + else: + return ( + f"libpython{version}{abi_flags}.so", + f"libpython{version}.so", + f"libpython{version}{abi_flags}.so.1.0", + f"libpython{version}.so.1.0", + ) + + +def _search_library_names(get_config, version, abi_flags) -> list[str]: """Returns a list of library files to search for shared libraries.""" # Quoting configure.ac in the cpython code base: # "INSTSONAME is the name of the shared library that will be use to install @@ -100,110 +130,132 @@ def _search_library_names(get_config): ) ] - # Set the prefix and suffix to construct the library name used for linking. - # The suffix and version are set here to the default values for the OS, - # since they are used below to construct "default" library names. - if _IS_DARWIN: - suffix = ".dylib" - prefix = "lib" - elif _IS_WINDOWS: - suffix = ".dll" - prefix = "" - else: - suffix = get_config("SHLIB_SUFFIX") - prefix = "lib" - if not suffix: - suffix = ".so" - - version = get_config("VERSION") + # Include the default libraries for the system. + lib_names.extend(_default_library_names(version, abi_flags)) - # Ensure that the pythonXY.dll files are included in the search. - lib_names.append(f"{prefix}python{version}{suffix}") + # Also include the abi3 libraries for the system. + lib_names.extend(_default_library_names(sys.version_info.major, abi_flags)) - # If there are ABIFLAGS, also add them to the python version lib search. - abiflags = get_config("ABIFLAGS") or get_config("abiflags") or "" - if abiflags: - lib_names.append(f"{prefix}python{version}{abiflags}{suffix}") - - # Dedup and remove empty values, keeping the order. - lib_names = [v for v in lib_names if v] - return {k: None for k in lib_names}.keys() + # Uniqify, preserving order. + return list(dict.fromkeys(k for k in lib_names if k)) -def _get_python_library_info(base_executable): +def _get_python_library_info(base_executable) -> dict[str, Any]: """Returns a dictionary with the static and dynamic python libraries.""" config_vars = sysconfig.get_config_vars() # VERSION is X.Y in Linux/macOS and XY in Windows. This is used to # construct library paths such as python3.12, so ensure it exists. - if not config_vars.get("VERSION"): - if sys.platform == "win32": - config_vars["VERSION"] = f"{sys.version_info.major}{sys.version_info.minor}" + version = config_vars.get("VERSION") + if not version: + if _IS_WINDOWS: + version = f"{sys.version_info.major}{sys.version_info.minor}" else: - config_vars["VERSION"] = ( - f"{sys.version_info.major}.{sys.version_info.minor}" - ) + version = f"{sys.version_info.major}.{sys.version_info.minor}" + + defines = [] + if config_vars.get("Py_GIL_DISABLED", "0") == "1": + defines.append("Py_GIL_DISABLED") + + # Avoid automatically linking the libraries on windows via pydefine.h + # pragma comment(lib ...) + if _IS_WINDOWS: + defines.append("Py_NO_LINK_LIB") + + # sys.abiflags may not exist, but it still may be set in the config. + abi_flags = _get_abi_flags(config_vars.get) search_directories = _search_directories(config_vars.get, base_executable) - search_libnames = _search_library_names(config_vars.get) + search_libnames = _search_library_names(config_vars.get, version, abi_flags) + + # Used to test whether the library is an abi3 library or a full api library. + abi3_libraries = _default_library_names(sys.version_info.major, abi_flags) - def _add_if_exists(target, path): - if os.path.exists(path) or os.path.isdir(path): - target[path] = None + # Found libraries + static_libraries: dict[str, None] = {} + dynamic_libraries: dict[str, None] = {} + interface_libraries: dict[str, None] = {} + abi_dynamic_libraries: dict[str, None] = {} + abi_interface_libraries: dict[str, None] = {} - interface_libraries = {} - dynamic_libraries = {} - static_libraries = {} for root_dir in search_directories: for libname in search_libnames: composed_path = os.path.join(root_dir, libname) - if libname.endswith(".a"): - _add_if_exists(static_libraries, composed_path) - continue + is_abi3_file = os.path.basename(composed_path) in abi3_libraries + + # Check whether the library exists and add it to the appropriate list. + if os.path.exists(composed_path) or os.path.isdir(composed_path): + if is_abi3_file: + if not libname.endswith(".a"): + abi_dynamic_libraries[composed_path] = None + elif libname.endswith(".a"): + static_libraries[composed_path] = None + else: + dynamic_libraries[composed_path] = None - _add_if_exists(dynamic_libraries, composed_path) + interface_path = None if libname.endswith(".dll"): - # On windows a .lib file may be an "import library" or a static library. - # The file could be inspected to determine which it is; typically python - # is used as a shared library. + # On windows a .lib file may be an "import library" or a static + # library. The file could be inspected to determine which it is; + # typically python is used as a shared library. # # On Windows, extensions should link with the pythonXY.lib interface # libraries. # # See: https://docs.python.org/3/extending/windows.html # https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-creation - _add_if_exists( - interface_libraries, os.path.join(root_dir, libname[:-3] + "lib") - ) + interface_path = os.path.join(root_dir, libname[:-3] + "lib") elif libname.endswith(".so"): # It's possible, though unlikely, that interface stubs (.ifso) exist. - _add_if_exists( - interface_libraries, os.path.join(root_dir, libname[:-2] + "ifso") - ) + interface_path = os.path.join(root_dir, libname[:-2] + "ifso") + + # Check whether an interface library exists. + if interface_path and os.path.exists(interface_path): + if is_abi3_file: + abi_interface_libraries[interface_path] = None + else: + interface_libraries[interface_path] = None + + # Additional DLLs are needed on Windows to link properly. + dlls = [] + if _IS_WINDOWS: + dlls.extend(glob.glob(os.path.join(os.path.dirname(base_executable), "*.dll"))) + dlls = [ + x + for x in dlls + if x not in dynamic_libraries and x not in abi_dynamic_libraries + ] + + def _unique_basenames(inputs: dict[str, None]) -> list[str]: + """Returns a list of paths, keeping only the first path for each basename.""" + result = [] + seen = set() + for k in inputs: + b = os.path.basename(k) + if b not in seen: + seen.add(b) + result.append(k) + return result # When no libraries are found it's likely that the python interpreter is not # configured to use shared or static libraries (minilinux). If this seems # suspicious try running `uv tool run find_libpython --list-all -v` return { - "dynamic_libraries": list(dynamic_libraries.keys()), - "static_libraries": list(static_libraries.keys()), - "interface_libraries": list(interface_libraries.keys()), + "dynamic_libraries": _unique_basenames(dynamic_libraries), + "static_libraries": _unique_basenames(static_libraries), + "interface_libraries": _unique_basenames(interface_libraries), + "abi_dynamic_libraries": _unique_basenames(abi_dynamic_libraries), + "abi_interface_libraries": _unique_basenames(abi_interface_libraries), + "abi_flags": abi_flags, + "shlib_suffix": ".dylib" if _IS_DARWIN else "", + "additional_dlls": dlls, + "defines": defines, } -def _get_base_executable(): +def _get_base_executable() -> str: """Returns the base executable path.""" - try: - if sys._base_executable: # pylint: disable=protected-access - return sys._base_executable # pylint: disable=protected-access - except AttributeError: - # Bug reports indicate sys._base_executable doesn't exist in some cases, - # but it's not clear why. - # See https://github.com/bazel-contrib/rules_python/issues/3172 - pass - # The normal sys.executable is the next-best guess if sys._base_executable - # is missing. - return sys.executable + return getattr(sys, "_base_executable", None) or sys.executable data = { diff --git a/python/private/glob_excludes.bzl b/python/private/glob_excludes.bzl deleted file mode 100644 index c98afe0ae2..0000000000 --- a/python/private/glob_excludes.bzl +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright 2024 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. - -"Utilities for glob exclusions." - -load(":util.bzl", "IS_BAZEL_7_4_OR_HIGHER") - -def _version_dependent_exclusions(): - """Returns glob exclusions that are sensitive to Bazel version. - - Returns: - a list of glob exclusion patterns - """ - if IS_BAZEL_7_4_OR_HIGHER: - return [] - else: - return ["**/* *"] - -glob_excludes = struct( - version_dependent_exclusions = _version_dependent_exclusions, -) diff --git a/python/private/hermetic_runtime_repo_setup.bzl b/python/private/hermetic_runtime_repo_setup.bzl index a35ce8ae7d..20b0324894 100644 --- a/python/private/hermetic_runtime_repo_setup.bzl +++ b/python/private/hermetic_runtime_repo_setup.bzl @@ -18,7 +18,6 @@ load("@rules_cc//cc:cc_library.bzl", "cc_library") load("//python:py_runtime.bzl", "py_runtime") load("//python:py_runtime_pair.bzl", "py_runtime_pair") load("//python/cc:py_cc_toolchain.bzl", "py_cc_toolchain") -load(":glob_excludes.bzl", "glob_excludes") load(":py_exec_tools_toolchain.bzl", "py_exec_tools_toolchain") load(":version.bzl", "version") @@ -59,30 +58,35 @@ def define_hermetic_runtime_toolchain_impl( "major": version_info.release[0], "minor": version_info.release[1], } + files_include = [ + "bin/**", + "extensions/**", + "include/**", + "libs/**", + "share/**", + ] + files_include += extra_files_glob_include + files_exclude = [ + # Unused shared libraries. `python` executable and the `:libpython` target + # depend on `libpython{python_version}.so.1.0`. + "lib/libpython{major}.{minor}*.so".format(**version_dict), + # static libraries + "lib/**/*.a", + # tests for the standard libraries. + "lib/python{major}.{minor}*/**/test/**".format(**version_dict), + "lib/python{major}.{minor}*/**/tests/**".format(**version_dict), + # During pyc creation, temp files named *.pyc.NNN are created + "**/__pycache__/*.pyc.*", + ] + files_exclude += extra_files_glob_exclude + native.filegroup( name = "files", srcs = native.glob( - include = [ - "bin/**", - "extensions/**", - "include/**", - "libs/**", - "share/**", - ] + extra_files_glob_include, + include = files_include, # Platform-agnostic filegroup can't match on all patterns. allow_empty = True, - exclude = [ - # Unused shared libraries. `python` executable and the `:libpython` target - # depend on `libpython{python_version}.so.1.0`. - "lib/libpython{major}.{minor}*.so".format(**version_dict), - # static libraries - "lib/**/*.a", - # tests for the standard libraries. - "lib/python{major}.{minor}*/**/test/**".format(**version_dict), - "lib/python{major}.{minor}*/**/tests/**".format(**version_dict), - # During pyc creation, temp files named *.pyc.NNN are created - "**/__pycache__/*.pyc.*", - ] + glob_excludes.version_dependent_exclusions() + extra_files_glob_exclude, + exclude = files_exclude, ), ) cc_import( @@ -127,6 +131,7 @@ def define_hermetic_runtime_toolchain_impl( ) cc_library( name = "python_headers", + hdrs = [":includes"], deps = [":python_headers_abi3"] + select({ "@bazel_tools//src/conditions:windows": [":interface"], "//conditions:default": [], @@ -180,10 +185,6 @@ def define_hermetic_runtime_toolchain_impl( "libs/python{major}{minor}t.lib".format(**version_dict), "libs/python3t.lib", ], - "@platforms//os:linux": [ - "lib/libpython{major}.{minor}.so".format(**version_dict), - "lib/libpython{major}.{minor}.so.1.0".format(**version_dict), - ], "@platforms//os:macos": ["lib/libpython{major}.{minor}.dylib".format(**version_dict)], "@platforms//os:windows": [ "python3.dll", @@ -191,6 +192,10 @@ def define_hermetic_runtime_toolchain_impl( "libs/python{major}{minor}.lib".format(**version_dict), "libs/python3.lib", ], + "//conditions:default": [ + "lib/libpython{major}.{minor}.so".format(**version_dict), + "lib/libpython{major}.{minor}.so.1.0".format(**version_dict), + ], }), ) @@ -235,6 +240,18 @@ def define_hermetic_runtime_toolchain_impl( _IS_FREETHREADED_YES: "cpython-{major}{minor}t".format(**version_dict), _IS_FREETHREADED_NO: "cpython-{major}{minor}".format(**version_dict), }), + # On Windows, a symlink-style venv requires supporting .dll files. + venv_bin_files = select({ + "@platforms//os:windows": native.glob( + include = [ + "*.dll", + ], + # This must be true because glob empty-ness is checked + # during loading phase, before select() filters it out. + allow_empty = True, + ), + "//conditions:default": [], + }), ) py_runtime_pair( diff --git a/python/private/internal_config_repo.bzl b/python/private/internal_config_repo.bzl index b57275b672..8ee6a2d017 100644 --- a/python/private/internal_config_repo.bzl +++ b/python/private/internal_config_repo.bzl @@ -21,31 +21,25 @@ settings for rules to later use. load("//python/private:text_util.bzl", "render") load(":repo_utils.bzl", "repo_utils") -_ENABLE_PIPSTAR_ENVVAR_NAME = "RULES_PYTHON_ENABLE_PIPSTAR" -_ENABLE_PIPSTAR_DEFAULT = "0" -_ENABLE_PYSTAR_ENVVAR_NAME = "RULES_PYTHON_ENABLE_PYSTAR" -_ENABLE_PYSTAR_DEFAULT = "1" _ENABLE_DEPRECATION_WARNINGS_ENVVAR_NAME = "RULES_PYTHON_DEPRECATION_WARNINGS" _ENABLE_DEPRECATION_WARNINGS_DEFAULT = "0" _CONFIG_TEMPLATE = """ config = struct( - enable_pystar = {enable_pystar}, - enable_pipstar = {enable_pipstar}, + build_python_zip_default = {build_python_zip_default}, + supports_whl_extraction = {supports_whl_extraction}, + enable_pystar = True, enable_deprecation_warnings = {enable_deprecation_warnings}, + extract_needs_chmod = {extract_needs_chmod}, + bazel_8_or_later = {bazel_8_or_later}, + bazel_9_or_later = {bazel_9_or_later}, + bazel_10_or_later = {bazel_10_or_later}, BuiltinPyInfo = getattr(getattr(native, "legacy_globals", None), "PyInfo", {builtin_py_info_symbol}), BuiltinPyRuntimeInfo = getattr(getattr(native, "legacy_globals", None), "PyRuntimeInfo", {builtin_py_runtime_info_symbol}), BuiltinPyCcLinkParamsProvider = getattr(getattr(native, "legacy_globals", None), "PyCcLinkParamsProvider", {builtin_py_cc_link_params_provider}), ) """ -# The py_internal symbol is only accessible from within @rules_python, so we have to -# load it from there and re-export it so that rules_python can later load it. -_PY_INTERNAL_SHIM = """ -load("@rules_python//tools/build_defs/python/private:py_internal_renamed.bzl", "py_internal_renamed") -py_internal_impl = py_internal_renamed -""" - ROOT_BUILD_TEMPLATE = """ load("@bazel_skylib//:bzl_library.bzl", "bzl_library") @@ -56,19 +50,13 @@ package( ) bzl_library( - name = "rules_python_config_bzl", - srcs = ["rules_python_config.bzl"] -) - -bzl_library( - name = "py_internal_bzl", - srcs = ["py_internal.bzl"], - deps = [{py_internal_dep}], + name = "extra_transition_settings", + srcs = ["extra_transition_settings.bzl"], ) bzl_library( - name = "extra_transition_settings_bzl", - srcs = ["extra_transition_settings.bzl"], + name = "rules_python_config", + srcs = ["rules_python_config.bzl"], ) """ @@ -88,16 +76,23 @@ _TRANSITION_SETTINGS_DEBUG_TEMPLATE = """ """ def _internal_config_repo_impl(rctx): - pystar_requested = _bool_from_environ(rctx, _ENABLE_PYSTAR_ENVVAR_NAME, _ENABLE_PYSTAR_DEFAULT) - - # Bazel 7+ (dev and later) has native.starlark_doc_extract, and thus the - # py_internal global, which are necessary for the pystar implementation. - if pystar_requested and hasattr(native, "starlark_doc_extract"): - enable_pystar = pystar_requested + # An empty version signifies a development build, which is treated as + # the latest version. + if native.bazel_version: + version_parts = native.bazel_version.split(".") + bazel_major_version = int(version_parts[0]) + bazel_minor_version = int(version_parts[1]) else: - enable_pystar = False + bazel_major_version = 99999 + bazel_minor_version = 99999 + + supports_whl_extraction = False + extract_needs_chmod = bazel_major_version < 8 or (bazel_major_version == 8 and bazel_minor_version < 6) + if bazel_major_version >= 8: + # Extracting .whl files requires Bazel 8.3.0 or later. + if bazel_major_version > 8 or bazel_minor_version >= 3: + supports_whl_extraction = True - if not native.bazel_version or int(native.bazel_version.split(".")[0]) >= 8: builtin_py_info_symbol = "None" builtin_py_runtime_info_symbol = "None" builtin_py_cc_link_params_provider = "None" @@ -107,33 +102,21 @@ def _internal_config_repo_impl(rctx): builtin_py_cc_link_params_provider = "PyCcLinkParamsProvider" rctx.file("rules_python_config.bzl", _CONFIG_TEMPLATE.format( - enable_pystar = enable_pystar, - enable_pipstar = _bool_from_environ(rctx, _ENABLE_PIPSTAR_ENVVAR_NAME, _ENABLE_PIPSTAR_DEFAULT), + build_python_zip_default = repo_utils.get_platforms_os_name(rctx) == "windows", enable_deprecation_warnings = _bool_from_environ(rctx, _ENABLE_DEPRECATION_WARNINGS_ENVVAR_NAME, _ENABLE_DEPRECATION_WARNINGS_DEFAULT), builtin_py_info_symbol = builtin_py_info_symbol, builtin_py_runtime_info_symbol = builtin_py_runtime_info_symbol, + supports_whl_extraction = str(supports_whl_extraction), + extract_needs_chmod = str(extract_needs_chmod), builtin_py_cc_link_params_provider = builtin_py_cc_link_params_provider, + bazel_8_or_later = str(bazel_major_version >= 8), + bazel_9_or_later = str(bazel_major_version >= 9), + bazel_10_or_later = str(bazel_major_version > 9), )) - if enable_pystar: - shim_content = _PY_INTERNAL_SHIM - py_internal_dep = '"@rules_python//tools/build_defs/python/private:py_internal_renamed_bzl"' - else: - shim_content = "py_internal_impl = None\n" - py_internal_dep = "" - - # Bazel 5 doesn't support repository visibility, so just use public - # as a stand-in - if native.bazel_version.startswith("5."): - visibility = "//visibility:public" - else: - visibility = "@rules_python//:__subpackages__" - rctx.file("BUILD", ROOT_BUILD_TEMPLATE.format( - py_internal_dep = py_internal_dep, - visibility = visibility, + visibility = "@rules_python//:__subpackages__", )) - rctx.file("py_internal.bzl", shim_content) rctx.file( "extra_transition_settings.bzl", @@ -155,7 +138,7 @@ def _internal_config_repo_impl(rctx): internal_config_repo = repository_rule( implementation = _internal_config_repo_impl, configure = True, - environ = [_ENABLE_PYSTAR_ENVVAR_NAME], + environ = [], attrs = { "transition_setting_generators": attr.string_list_dict(), "transition_settings": attr.string_list(), @@ -163,4 +146,4 @@ internal_config_repo = repository_rule( ) def _bool_from_environ(rctx, key, default): - return bool(int(repo_utils.getenv(rctx, key, default))) + return bool(int(rctx.getenv(key, default))) diff --git a/python/private/internal_dev_deps.bzl b/python/private/internal_dev_deps.bzl index d621a5d941..5bbee232f9 100644 --- a/python/private/internal_dev_deps.bzl +++ b/python/private/internal_dev_deps.bzl @@ -26,7 +26,7 @@ def _internal_dev_deps_impl(mctx): # otherwise refer to RBE docs. rbe_preconfig( name = "buildkite_config", - toolchain = "ubuntu2204", + toolchain = "ubuntu2404", ) runtime_env_repo(name = "rules_python_runtime_env_tc_info") @@ -67,6 +67,100 @@ def _internal_dev_deps_impl(mctx): enable_implicit_namespace_pkgs = False, ) + whl_from_dir_repo( + name = "pkgutil_nspkg1_whl", + root = "//tests/repos/pkgutil_nspkg1:BUILD.bazel", + output = "pkgutil_nspkg1-1.0-any-none-any.whl", + ) + whl_library( + name = "pkgutil_nspkg1", + whl_file = "@pkgutil_nspkg1_whl//:pkgutil_nspkg1-1.0-any-none-any.whl", + requirement = "pkgutil_nspkg1", + enable_implicit_namespace_pkgs = False, + ) + + whl_from_dir_repo( + name = "pkgutil_nspkg2_whl", + root = "//tests/repos/pkgutil_nspkg2:BUILD.bazel", + output = "pkgutil_nspkg2-1.0-any-none-any.whl", + ) + whl_library( + name = "pkgutil_nspkg2", + whl_file = "@pkgutil_nspkg2_whl//:pkgutil_nspkg2-1.0-any-none-any.whl", + requirement = "pkgutil_nspkg2", + enable_implicit_namespace_pkgs = False, + ) + + whl_from_dir_repo( + name = "whl_with_data1_whl", + root = "//tests/repos/whl_with_data1:BUILD.bazel", + output = "whl_with_data1-1.0-any-none-any.whl", + ) + whl_library( + name = "whl_with_data1", + whl_file = "@whl_with_data1_whl//:whl_with_data1-1.0-any-none-any.whl", + requirement = "whl-with-data1", + ) + + whl_from_dir_repo( + name = "whl_with_data2_whl", + root = "//tests/repos/whl_with_data2:BUILD.bazel", + output = "whl_with_data2-1.0-any-none-any.whl", + ) + whl_library( + name = "whl_with_data2", + whl_file = "@whl_with_data2_whl//:whl_with_data2-1.0-any-none-any.whl", + requirement = "whl-with-data2", + ) + + _whl_library_from_dir( + name = "whl_library_extras_direct_dep", + root = "//tests/pypi/whl_library/testdata/pkg:BUILD.bazel", + output = "pkg-1.0-any-none-any.whl", + requirement = "pkg[optional]", + # The following is necessary to enable pipstar and make tests faster + config_load = "@rules_python//tests/pypi/whl_library/testdata:packages.bzl", + dep_template = "@whl_library_extras_{name}//:{target}", + ) + _whl_library_from_dir( + name = "whl_library_extras_optional_dep", + root = "//tests/pypi/whl_library/testdata/optional_dep:BUILD.bazel", + output = "optional_dep-1.0-any-none-any.whl", + requirement = "optional_dep", + # The following is necessary to enable pipstar and make tests faster + config_load = "@rules_python//tests/pypi/whl_library/testdata:packages.bzl", + ) + + # Setup for //tests/pypi/patch_whl/patch_whl_patch_test.py + whl_from_dir_repo( + name = "patch_whl_pkg_whl", + root = "//tests/pypi/patch_whl/testdata/pkg:BUILD.bazel", + output = "pkg-1.0-any-none-any.whl", + ) + whl_library( + name = "patch_whl_pkg", + whl_file = "@patch_whl_pkg_whl//:pkg-1.0-any-none-any.whl", + requirement = "pkg", + whl_patches = { + "//tests/pypi/patch_whl/testdata/patches:modify_pkg.patch": json.encode({ + "patch_strip": 0, + "whls": ["pkg-1.0-any-none-any.whl"], + }), + }, + ) + +def _whl_library_from_dir(*, name, output, root, **kwargs): + whl_from_dir_repo( + name = "{}_whl".format(name), + root = root, + output = output, + ) + whl_library( + name = name, + whl_file = "@{}_whl//:{}".format(name, output), + **kwargs + ) + internal_dev_deps = module_extension( implementation = _internal_dev_deps_impl, doc = "This extension creates internal rules_python dev dependencies.", diff --git a/python/private/interpreter.bzl b/python/private/interpreter.bzl index c66d3dc21e..b281be9639 100644 --- a/python/private/interpreter.bzl +++ b/python/private/interpreter.bzl @@ -17,7 +17,7 @@ load("@bazel_skylib//lib:paths.bzl", "paths") load("//python:py_runtime_info.bzl", "PyRuntimeInfo") load(":common.bzl", "runfiles_root_path") -load(":sentinel.bzl", "SentinelInfo") +load(":sentinel_impl.bzl", "SentinelInfo") load(":toolchain_types.bzl", "TARGET_TOOLCHAIN_TYPE") def _interpreter_binary_impl(ctx): diff --git a/python/private/local_runtime_repo.bzl b/python/private/local_runtime_repo.bzl index c053a03508..37b7d2b130 100644 --- a/python/private/local_runtime_repo.bzl +++ b/python/private/local_runtime_repo.bzl @@ -34,14 +34,36 @@ define_local_runtime_toolchain_impl( major = "{major}", minor = "{minor}", micro = "{micro}", + abi_flags = "{abi_flags}", + os = "{os}", + implementation_name = "{implementation_name}", interpreter_path = "{interpreter_path}", interface_library = {interface_library}, libraries = {libraries}, - implementation_name = "{implementation_name}", - os = "{os}", + defines = {defines}, + abi3_interface_library = {abi3_interface_library}, + abi3_libraries = {abi3_libraries}, + additional_dlls = {additional_dlls}, ) """ +def _expand_incompatible_template(): + return _TOOLCHAIN_IMPL_TEMPLATE.format( + major = "0", + minor = "0", + micro = "0", + abi_flags = "", + os = "@platforms//:incompatible", + implementation_name = "incompatible", + interpreter_path = "/incompatible", + interface_library = "None", + libraries = "[]", + defines = "[]", + abi3_interface_library = "None", + abi3_libraries = "[]", + additional_dlls = "[]", + ) + def _norm_path(path): """Returns a path using '/' separators and no trailing slash.""" path = path.replace("\\", "/") @@ -49,33 +71,39 @@ def _norm_path(path): path = path[:-1] return path -def _symlink_first_library(rctx, logger, libraries): +def _symlink_libraries(rctx, logger, libraries, shlib_suffix): """Symlinks the shared libraries into the lib/ directory. + Individual files are symlinked instead of the whole directory because + shared_lib_dirs contains multiple search paths for the shared libraries, + and the python files may be missing from any of those directories, and + any of those directories may include non-python runtime libraries, + as would be the case if LIBDIR were, for example, /usr/lib. + Args: rctx: A repository_ctx object logger: A repo_utils.logger object - libraries: A list of static library paths to potentially symlink. + libraries: paths to libraries to attempt to symlink. + shlib_suffix: Optional. Ensure that the generated symlinks end with this suffix. Returns: - A single library path linked by the action. + A list of library paths (under lib/) linked by the action. """ - linked = None - for target in libraries: - origin = rctx.path(target) + result = [] + for source in libraries: + origin = rctx.path(source) if not origin.exists: # The reported names don't always exist; it depends on the particulars # of the runtime installation. continue - if target.endswith("/Python"): - linked = "lib/{}.dylib".format(origin.basename) + if shlib_suffix and not origin.basename.endswith(shlib_suffix): + target = "lib/{}{}".format(origin.basename, shlib_suffix) else: - linked = "lib/{}".format(origin.basename) - logger.debug("Symlinking {} to {}".format(origin, linked)) - repo_utils.watch(rctx, origin) - rctx.symlink(origin, linked) - break - - return linked + target = "lib/{}".format(origin.basename) + logger.debug(lambda: "Symlinking {} to {}".format(origin, target)) + rctx.watch(origin) + rctx.symlink(origin, target) + result.append(target) + return result def _local_runtime_repo_impl(rctx): logger = repo_utils.logger(rctx) @@ -142,37 +170,55 @@ def _local_runtime_repo_impl(rctx): # path is an error. Silently skip, since includes are only necessary # if C extensions are built. if include_path.exists and include_path.is_dir: - repo_utils.watch_tree(rctx, include_path) + rctx.watch_tree(include_path) else: pass # The cc_library.includes values have to be non-absolute paths, otherwise # the toolchain will give an error. Work around this error by making them # appear as part of this repo. + logger.debug(lambda: "Symlinking {} to include".format(include_path)) rctx.symlink(include_path, "include") rctx.report_progress("Symlinking external Python shared libraries") - interface_library = _symlink_first_library(rctx, logger, info["interface_libraries"]) - shared_library = _symlink_first_library(rctx, logger, info["dynamic_libraries"]) - static_library = _symlink_first_library(rctx, logger, info["static_libraries"]) - - libraries = [] - if shared_library: - libraries.append(shared_library) - elif static_library: - libraries.append(static_library) + + interface_library = None + if info["dynamic_libraries"]: + libraries = _symlink_libraries(rctx, logger, info["dynamic_libraries"][:1], info["shlib_suffix"]) + symlinked = _symlink_libraries(rctx, logger, info["interface_libraries"][:1], None) + if symlinked: + interface_library = symlinked[0] + else: + libraries = _symlink_libraries(rctx, logger, info["static_libraries"], None) + if not libraries: + logger.info("No python libraries found.") + + abi3_interface_library = None + if info["abi_dynamic_libraries"]: + abi3_libraries = _symlink_libraries(rctx, logger, info["abi_dynamic_libraries"][:1], info["shlib_suffix"]) + symlinked = _symlink_libraries(rctx, logger, info["abi_interface_libraries"][:1], None) + if symlinked: + abi3_interface_library = symlinked[0] else: - logger.warn("No external python libraries found.") + abi3_libraries = [] + logger.info("No abi3 python libraries found.") + + additional_dlls = _symlink_libraries(rctx, logger, info["additional_dlls"], None) build_bazel = _TOOLCHAIN_IMPL_TEMPLATE.format( major = info["major"], minor = info["minor"], micro = info["micro"], + abi_flags = info["abi_flags"], + os = "@platforms//os:{}".format(repo_utils.get_platforms_os_name(rctx)), + implementation_name = info["implementation_name"], interpreter_path = _norm_path(interpreter_path), interface_library = repr(interface_library), libraries = repr(libraries), - implementation_name = info["implementation_name"], - os = "@platforms//os:{}".format(repo_utils.get_platforms_os_name(rctx)), + defines = repr(info["defines"]), + abi3_interface_library = repr(abi3_interface_library), + abi3_libraries = repr(abi3_libraries), + additional_dlls = repr(additional_dlls), ) logger.debug(lambda: "BUILD.bazel\n{}".format(build_bazel)) @@ -200,13 +246,37 @@ a system having the necessary Python installed. doc = """ An absolute path or program name on the `PATH` env var. +*Mutually exclusive with `interpreter_target`.* + Values with slashes are assumed to be the path to a program. Otherwise, it is treated as something to search for on `PATH` Note that, when a plain program name is used, the path to the interpreter is resolved at repository evalution time, not runtime of any resulting binaries. + +If not set, defaults to `python3`. + +:::{seealso} +The {obj}`interpreter_target` attribute for getting the interpreter from +a label +::: +""", + default = "", + ), + "interpreter_target": attr.label( + doc = """ +A label to a Python interpreter executable. + +*Mutually exclusive with `interpreter_path`.* + +On Windows, if the path doesn't exist, various suffixes will be tried to +find a usable path. + +:::{seealso} +The {obj}`interpreter_path` attribute for getting the interpreter from +a path or PATH environment lookup. +::: """, - default = "python3", ), "on_failure": attr.string( default = _OnFailure.SKIP, @@ -235,17 +305,36 @@ How to handle errors when trying to automatically determine settings. environ = ["PATH", REPO_DEBUG_ENV_VAR, "DEVELOPER_DIR", "XCODE_VERSION"], ) -def _expand_incompatible_template(): - return _TOOLCHAIN_IMPL_TEMPLATE.format( - interpreter_path = "/incompatible", - implementation_name = "incompatible", - interface_library = "None", - libraries = "[]", - major = "0", - minor = "0", - micro = "0", - os = "@platforms//:incompatible", +def _find_python_exe_from_target(rctx): + base_path = rctx.path(rctx.attr.interpreter_target) + if base_path.exists: + return base_path, None + attempted_paths = [base_path] + + # Try to convert a unix-y path to a Windows path. On Linux/Mac, + # the path is usually `bin/python3`. On Windows, it's simply + # `python.exe`. + basename = base_path.basename.rstrip("3") + path = base_path.dirname.dirname.get_child(basename) + path = rctx.path("{}.exe".format(path)) + if path.exists: + return path, None + attempted_paths.append(path) + + # Try adding .exe to the base path + path = rctx.path("{}.exe".format(base_path)) + if path.exists: + return path, None + attempted_paths.append(path) + + describe_failure = lambda: ( + "Target '{target}' could not be resolved to a valid path. " + + "Attempted paths: {paths}" + ).format( + target = rctx.attr.interpreter_target, + paths = "\n".join([str(p) for p in attempted_paths]), ) + return None, describe_failure def _resolve_interpreter_path(rctx): """Find the absolute path for an interpreter. @@ -260,20 +349,27 @@ def _resolve_interpreter_path(rctx): returns a description of why it couldn't be resolved A path object or None. The path may not exist. """ - if "/" not in rctx.attr.interpreter_path and "\\" not in rctx.attr.interpreter_path: - # Provide a bit nicer integration with pyenv: recalculate the runtime if the - # user changes the python version using e.g. `pyenv shell` - repo_utils.getenv(rctx, "PYENV_VERSION") - result = repo_utils.which_unchecked(rctx, rctx.attr.interpreter_path) - resolved_path = result.binary - describe_failure = result.describe_failure + if rctx.attr.interpreter_path and rctx.attr.interpreter_target: + fail("interpreter_path and interpreter_target are mutually exclusive") + + if rctx.attr.interpreter_target: + resolved_path, describe_failure = _find_python_exe_from_target(rctx) else: - repo_utils.watch(rctx, rctx.attr.interpreter_path) - resolved_path = rctx.path(rctx.attr.interpreter_path) - if not resolved_path.exists: - describe_failure = lambda: "Path not found: {}".format(repr(rctx.attr.interpreter_path)) + interpreter_path = rctx.attr.interpreter_path or "python3" + if "/" not in interpreter_path and "\\" not in interpreter_path: + # Provide a bit nicer integration with pyenv: recalculate the runtime if the + # user changes the python version using e.g. `pyenv shell` + rctx.getenv("PYENV_VERSION") + result = repo_utils.which_unchecked(rctx, interpreter_path) + resolved_path = result.binary + describe_failure = result.describe_failure else: - describe_failure = None + rctx.watch(interpreter_path) + resolved_path = rctx.path(interpreter_path) + if not resolved_path.exists: + describe_failure = lambda: "Path not found: {}".format(repr(interpreter_path)) + else: + describe_failure = None return struct( resolved_path = resolved_path, diff --git a/python/private/local_runtime_repo_setup.bzl b/python/private/local_runtime_repo_setup.bzl index 6cff1aea43..5cb7bda200 100644 --- a/python/private/local_runtime_repo_setup.bzl +++ b/python/private/local_runtime_repo_setup.bzl @@ -11,7 +11,6 @@ # 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. - """Setup code called by the code generated by `local_runtime_repo`.""" load("@bazel_skylib//lib:selects.bzl", "selects") @@ -29,11 +28,16 @@ def define_local_runtime_toolchain_impl( major, minor, micro, + abi_flags, + os, + implementation_name, interpreter_path, interface_library, libraries, - implementation_name, - os): + defines, + abi3_interface_library, + abi3_libraries, + additional_dlls): """Defines a toolchain implementation for a local Python runtime. Generates public targets: @@ -50,15 +54,23 @@ def define_local_runtime_toolchain_impl( major: `str` The major Python version, e.g. `3` of `3.9.1`. minor: `str` The minor Python version, e.g. `9` of `3.9.1`. micro: `str` The micro Python version, e.g. "1" of `3.9.1`. + abi_flags: `str` The abi flags, as returned by `sys.abiflags`. + os: `str` A label to the OS constraint (e.g. `@platforms//os:linux`) for + this runtime. + implementation_name: `str` The implementation name, as returned by + `sys.implementation.name`. interpreter_path: `str` Absolute path to the interpreter. interface_library: `str` Path to the interface library. e.g. "lib/python312.lib" libraries: `list[str]` Path[s] to the python libraries. e.g. ["lib/python312.dll"] or ["lib/python312.so"] - implementation_name: `str` The implementation name, as returned by - `sys.implementation.name`. - os: `str` A label to the OS constraint (e.g. `@platforms//os:linux`) for - this runtime. + defines: `list[str]` List of additional defines. + abi3_interface_library: `str` Path to the interface library. + e.g. "lib/python3.lib" + abi3_libraries: `list[str]` Path[s] to the python libraries. + e.g. ["lib/python3.dll"] or ["lib/python3.so"] + additional_dlls: `list[str]` Path[s] to additional DLLs. + e.g. ["lib/msvcrt123.dll"] """ major_minor = "{}.{}".format(major, minor) major_minor_micro = "{}.{}".format(major_minor, micro) @@ -67,44 +79,70 @@ def define_local_runtime_toolchain_impl( # See https://docs.python.org/3/extending/windows.html # However not all python installations (such as manylinux) include shared or static libraries, # so only create the import library when interface_library is set. - full_abi_deps = [] - abi3_deps = [] if interface_library: cc_import( - name = "_python_interface_library", + name = "interface", interface_library = interface_library, - system_provided = 1, + system_provided = True, ) - if interface_library.endswith("{}.lib".format(major)): - abi3_deps = [":_python_interface_library"] - else: - full_abi_deps = [":_python_interface_library"] - cc_library( - name = "_python_headers_abi3", - # NOTE: Keep in sync with watch_tree() called in local_runtime_repo + if abi3_interface_library: + cc_import( + name = "abi3_interface", + interface_library = abi3_interface_library, + system_provided = True, + ) + + native.filegroup( + name = "includes", srcs = native.glob( include = ["include/**/*.h"], exclude = ["include/numpy/**"], # numpy headers are handled separately allow_empty = True, # A Python install may not have C headers ), - deps = abi3_deps, + ) + + # header libraries. + cc_library( + name = "python_headers_abi3", + hdrs = [":includes"], includes = ["include"], + defines = defines, # NOTE: Users should define Py_LIMITED_API=3 + deps = select({ + "@bazel_tools//src/conditions:windows": [":abi3_interface"], + "//conditions:default": [], + }), ) + + cc_library( + name = "python_headers", + hdrs = [":includes"], + includes = ["include"], + defines = defines, + deps = select({ + "@bazel_tools//src/conditions:windows": [":interface"], + "//conditions:default": [], + }), + ) + + # python libraries cc_library( - name = "_python_headers", - deps = [":_python_headers_abi3"] + full_abi_deps, + name = "libpython_abi3", + hdrs = [":includes"], + defines = defines, # NOTE: Users should define Py_LIMITED_API=3 + srcs = abi3_libraries + additional_dlls, ) cc_library( - name = "_libpython", - hdrs = [":_python_headers"], - srcs = libraries, - deps = [], + name = "libpython", + hdrs = [":includes"], + defines = defines, + srcs = libraries + additional_dlls, ) + # runtime configuration py_runtime( - name = "_py3_runtime", + name = "py3_runtime", interpreter_path = interpreter_path, python_version = "PY3", interpreter_version_info = { @@ -113,12 +151,27 @@ def define_local_runtime_toolchain_impl( "minor": minor, }, implementation_name = implementation_name, + abi_flags = abi_flags, + pyc_tag = "{}-{}{}{}".format(implementation_name, major, minor, abi_flags), + venv_bin_files = select({ + "@platforms//os:windows": native.glob( + include = [ + "lib/*.dll", + # The pdb files just provide debugging information + "lib/*.pdb", + ], + # This must be true because glob empty-ness is checked + # during loading phase, before select() filters it out. + allow_empty = True, + ), + "//conditions:default": [], + }), ) py_runtime_pair( name = "python_runtimes", py2_runtime = None, - py3_runtime = ":_py3_runtime", + py3_runtime = ":py3_runtime", visibility = ["//visibility:public"], ) @@ -130,9 +183,9 @@ def define_local_runtime_toolchain_impl( py_cc_toolchain( name = "py_cc_toolchain", - headers = ":_python_headers", - headers_abi3 = ":_python_headers_abi3", - libs = ":_libpython", + headers = ":python_headers", + headers_abi3 = ":python_headers_abi3", + libs = ":libpython", python_version = major_minor_micro, visibility = ["//visibility:public"], ) diff --git a/python/private/pbs_manifest.bzl b/python/private/pbs_manifest.bzl new file mode 100644 index 0000000000..86434dc0ea --- /dev/null +++ b/python/private/pbs_manifest.bzl @@ -0,0 +1,152 @@ +"""Helper functions to parse python-build-standalone manifests.""" + +def parse_filename(filename): + """Parses a python-build-standalone filename (or URL) into its components. + + See https://gregoryszorc.com/docs/python-build-standalone/main/running.html + + Example: cpython-3.10.20+20260414-x86_64_v2-unknown-linux-musl-lto-full.tar.zst + + Args: + filename: The filename or URL of the python-build-standalone release asset. + + Returns: + A dictionary of parsed components if parsed successfully, else None. + """ + basename = filename.rpartition("/")[-1] + if basename.endswith(".tar.zst"): + name = basename.removesuffix(".tar.zst") + elif basename.endswith(".tar.gz"): + name = basename.removesuffix(".tar.gz") + else: + return None + + if not name.startswith("cpython-"): + return None + name = name.removeprefix("cpython-") + + left, plus, tail = name.partition("+") + if plus: + python_version = left + build_version, sep, rest = tail.partition("-") + if not sep: + return None + else: + python_version, sep, rest = left.partition("-") + if not sep: + return None + build_version = "" + + arch, sep, rest = rest.partition("-") + if not sep: + return None + + microarch = "" + arch_base, sep_v, microarch_num = arch.partition("_v") + if sep_v: + arch = arch_base + microarch = "v" + microarch_num + + vendor, sep, rest = rest.partition("-") + if not sep: + return None + + os, sep, rest = rest.partition("-") + if not sep: + return None + + libc = "" + next_part, _, remaining = rest.partition("-") + if os == "linux" and next_part in ["gnu", "musl"]: + libc = next_part + flavor = remaining + elif os == "windows" and next_part == "msvc": + libc = next_part + flavor = remaining + else: + libc = "" + flavor = rest + + freethreaded = False + if flavor.startswith("freethreaded+"): + freethreaded = True + flavor = flavor.removeprefix("freethreaded+") + elif flavor.startswith("freethreaded-"): + freethreaded = True + flavor = flavor.removeprefix("freethreaded-") + elif flavor == "freethreaded": + freethreaded = True + flavor = "" + + archive_flavor = "" + if flavor.endswith("-full"): + archive_flavor = "full" + flavor = flavor.removesuffix("-full") + elif flavor == "full": + archive_flavor = "full" + flavor = "" + elif flavor.endswith("-install_only_stripped"): + archive_flavor = "install_only_stripped" + flavor = flavor.removesuffix("-install_only_stripped") + elif flavor == "install_only_stripped": + archive_flavor = "install_only_stripped" + flavor = "" + elif flavor.endswith("-install_only"): + archive_flavor = "install_only" + flavor = flavor.removesuffix("-install_only") + elif flavor == "install_only": + archive_flavor = "install_only" + flavor = "" + + return { + "arch": arch, + "archive_flavor": archive_flavor, + "build_flavor": flavor, + "build_version": build_version, + "freethreaded": freethreaded, + "libc": libc, + "location": filename, + "microarch": microarch, + "os": os, + "python_version": python_version, + "vendor": vendor, + } + +def parse_runtime_manifest(content): + """Parses the SHA256SUMS file content into a list of structs. + + Args: + content: The raw content of the manifest file. + + Returns: + A list of structs capturing the parsed components of each valid entry. + Each struct contains the following fields: + - arch: CPU architecture (e.g., "x86_64"). + - archive_flavor: Release asset archive type (e.g., "full", "install_only"). + - build_version: Standalone release date (e.g., "20260414"). + - location: Full package filename or URL (e.g., "cpython-3.11.15..." or "https://..."). + - build_flavor: Build configuration flavor (e.g., "debug", "pgo+lto"). + - freethreaded: Whether the build is free-threaded (boolean). + - libc: C library type (e.g., "gnu", "musl", "msvc", or ""). + - microarch: Microarchitecture level (e.g., "v2", "v3", or ""). + - os: Operating system (e.g., "linux", "darwin", "windows"). + - python_version: Python semver version (e.g., "3.11.15"). + - sha256: SHA256 integrity hash of the release asset. + - vendor: Platform vendor (e.g., "unknown", "apple"). + """ + results = [] + for line in content.split("\n"): + line = line.strip() + if not line or line.startswith("#"): + continue + parts = [p for p in line.split(" ") if p] + if len(parts) != 2: + continue + sha256, filename = parts + parsed = parse_filename(filename) + if parsed: + results.append(struct( + sha256 = sha256, + **parsed + )) + return results diff --git a/python/private/print_toolchain_checksums.bzl b/python/private/print_toolchain_checksums.bzl deleted file mode 100644 index b4fa400221..0000000000 --- a/python/private/print_toolchain_checksums.bzl +++ /dev/null @@ -1,89 +0,0 @@ -"""Print the toolchain versions. -""" - -load("//python:versions.bzl", "TOOL_VERSIONS", "get_release_info") -load("//python/private:text_util.bzl", "render") -load("//python/private:version.bzl", "version") - -def print_toolchains_checksums(name): - """A macro to print checksums for a particular Python interpreter version. - - Args: - name: {type}`str`: the name of the runnable target. - """ - by_version = {} - - for python_version, metadata in TOOL_VERSIONS.items(): - by_version[python_version] = _commands_for_version( - python_version = python_version, - metadata = metadata, - ) - - all_commands = sorted( - by_version.items(), - key = lambda x: version.key(version.parse(x[0], strict = True)), - ) - all_commands = [x[1] for x in all_commands] - - template = """\ -cat > "$@" <<'EOF' -#!/usr/bin/env bash -set -euo pipefail - -set -o errexit -o nounset -o pipefail - -echo "Fetching hashes..." - -{commands} -EOF - """ - - native.genrule( - name = name, - srcs = [], - outs = ["print_toolchains_checksums.sh"], - cmd = select({ - "//python/config_settings:is_python_{}".format(version_str): template.format( - commands = commands, - ) - for version_str, commands in by_version.items() - } | { - "//conditions:default": template.format(commands = "\n".join(all_commands)), - }), - executable = True, - ) - -def _commands_for_version(*, python_version, metadata): - lines = [] - first_platform = metadata["sha256"].keys()[0] - root, _, _ = get_release_info(first_platform, python_version)[1][0].rpartition("/") - sha_url = "{}/{}".format(root, "SHA256SUMS") - prefix = metadata["strip_prefix"] - prefix = render.indent( - render.dict(prefix) if type(prefix) == type({}) else repr(prefix), - indent = " " * 8, - ).lstrip() - - lines += [ - "sha256s=$$(curl --silent --show-error --location --fail {})".format(sha_url), - "cat <_entry_point.py` to + be compatible with symbolic macros. binary_rule: {type}`callable`, The rule/macro to use to instantiate the target. It's expected to behave like {obj}`py_binary`. Defaults to {obj}`py_binary`. @@ -73,13 +76,13 @@ def py_console_script_binary( Defaults to empty string. **kwargs: Extra parameters forwarded to `binary_rule`. """ - main = "rules_python_entry_point_{}.py".format(name) + main = main or name + "_entry_point.py" if kwargs.pop("srcs", None): fail("passing 'srcs' attribute to py_console_script_binary is unsupported") py_console_script_gen( - name = "_{}_gen".format(name), + name = name + "_gen__", entry_points_txt = entry_points_txt or _dist_info(pkg), out = main, console_script = script, diff --git a/python/private/py_console_script_gen.py b/python/private/py_console_script_gen.py index 4b4f2f6986..2be986f732 100644 --- a/python/private/py_console_script_gen.py +++ b/python/private/py_console_script_gen.py @@ -37,9 +37,6 @@ import argparse import configparser import pathlib -import re -import sys -import textwrap _ENTRY_POINTS_TXT = "entry_points.txt" @@ -62,7 +59,7 @@ raise if __name__ == "__main__": - sys.exit({entry_point}()) + sys.exit({entry_point}()) # type: ignore """ @@ -75,7 +72,7 @@ class EntryPointsParser(configparser.ConfigParser): optionxform = staticmethod(str) -def _guess_entry_point(guess: str, console_scripts: dict[string, string]) -> str | None: +def _guess_entry_point(guess: str, console_scripts: dict[string, string]) -> str | None: # noqa: F821 for key, candidate in console_scripts.items(): if guess == key: return candidate diff --git a/python/private/py_exec_tools_info.bzl b/python/private/py_exec_tools_info.bzl index ad9a7b0c5e..470c0c1bf0 100644 --- a/python/private/py_exec_tools_info.bzl +++ b/python/private/py_exec_tools_info.bzl @@ -26,7 +26,7 @@ e.g. if all the exec tools are prebuilt binaries. :::{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` +the Python toolchain itself. When possible, prefer 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. @@ -39,11 +39,27 @@ toolchain. ::: :::{warning} -This does not work correctly in case of RBE, please use exec_runtime instead. +This does not work correctly with RBE. Use {obj}`exec_runtime` instead. Once https://github.com/bazelbuild/bazel/issues/23620 is resolved this warning may be removed. ::: +""", + "exec_runtime": """ +:type: PyRuntimeInfo | None + +The Python runtime to use for the exec configuration. + +:::{versionadded} 1.9.0 + +In prior versions, the equivalent can be obtained using: +``` +exec_runtime = ( + ctx.toolchains["@rules_python//python:exec_tools_toolchain_type"]. + exec_tools.exec_interpreter[platform_common.ToolchainInfo].py3_runtime + ) +``` +::: """, "precompiler": """ :type: Target | None diff --git a/python/private/py_exec_tools_toolchain.bzl b/python/private/py_exec_tools_toolchain.bzl index 00ad8072f6..d126262033 100644 --- a/python/private/py_exec_tools_toolchain.bzl +++ b/python/private/py_exec_tools_toolchain.bzl @@ -18,7 +18,7 @@ load("@bazel_skylib//lib:paths.bzl", "paths") load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") load(":common_labels.bzl", "labels") load(":py_exec_tools_info.bzl", "PyExecToolsInfo") -load(":sentinel.bzl", "SentinelInfo") +load(":sentinel_impl.bzl", "SentinelInfo") load(":toolchain_types.bzl", "TARGET_TOOLCHAIN_TYPE") def _py_exec_tools_toolchain_impl(ctx): @@ -30,11 +30,17 @@ def _py_exec_tools_toolchain_impl(ctx): if SentinelInfo in ctx.attr.exec_interpreter: exec_interpreter = None + exec_runtime = None + if exec_interpreter != None and platform_common.ToolchainInfo in exec_interpreter: + tc = exec_interpreter[platform_common.ToolchainInfo] + exec_runtime = getattr(tc, "py3_runtime", None) + return [ platform_common.ToolchainInfo( exec_tools = PyExecToolsInfo( exec_interpreter = exec_interpreter, precompiler = ctx.attr.precompiler, + exec_runtime = exec_runtime, ), **extra_kwargs ), @@ -104,6 +110,12 @@ def _current_interpreter_executable_impl(ctx): # re-exec. If it's not a recognized name, then they fail. if runtime.interpreter: executable = ctx.actions.declare_file(runtime.interpreter.basename) + + # NOTE: Using ctx.actions.symlink() here doesn't always work with RBE + # because it's not guaranteed that it will materialize as a symlink, but + # we rely on it being a symlink so that Python can find its actual + # PYTHONHOME. + # See https://github.com/bazelbuild/bazel/issues/23620 ctx.actions.symlink(output = executable, target_file = runtime.interpreter, is_executable = True) else: executable = ctx.actions.declare_symlink(paths.basename(runtime.interpreter_path)) diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl index dd0a1a1d6e..242b09344e 100644 --- a/python/private/py_executable.bzl +++ b/python/private/py_executable.bzl @@ -18,6 +18,7 @@ load("@bazel_skylib//lib:paths.bzl", "paths") load("@bazel_skylib//lib:structs.bzl", "structs") load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") load("@rules_cc//cc/common:cc_common.bzl", "cc_common") +load("@rules_python_internal//:rules_python_config.bzl", rp_config = "config") load(":attr_builders.bzl", "attrb") load( ":attributes.bzl", @@ -29,46 +30,52 @@ load( "PrecompileAttr", "PycCollectionAttr", "REQUIRED_EXEC_GROUP_BUILDERS", + "WINDOWS_CONSTRAINTS_ATTRS", "apply_config_settings_attr", ) load(":builders.bzl", "builders") load(":cc_helper.bzl", "cc_helper") load( ":common.bzl", + "ExplicitSymlink", + "actions_run", "collect_cc_info", + "collect_deps", "collect_imports", "collect_runfiles", "create_binary_semantics_struct", "create_cc_details_struct", - "create_executable_result_struct", "create_instrumented_files_info", "create_output_group_info", "create_py_info", + "create_windows_exe_launcher", "csv", "filter_to_py_srcs", - "get_imports", "is_bool", + "is_windows_platform", + "maybe_create_repo_mapping", "relative_path", "runfiles_root_path", - "target_platform_has_any_constraint", ) load(":common_labels.bzl", "labels") -load(":flags.bzl", "BootstrapImplFlag", "VenvsUseDeclareSymlinkFlag", "read_possibly_native_flag") +load(":flags.bzl", "BootstrapImplFlag", "ValidateTestMainFlag", "VenvsUseDeclareSymlinkFlag", "read_possibly_native_flag") load(":precompile.bzl", "maybe_precompile") load(":py_cc_link_params_info.bzl", "PyCcLinkParamsInfo") load(":py_executable_info.bzl", "PyExecutableInfo") load(":py_info.bzl", "PyInfo", "VenvSymlinkKind") load(":py_internal.bzl", "py_internal") +load(":py_interpreter_program.bzl", "PyInterpreterProgramInfo") load(":py_runtime_info.bzl", "DEFAULT_STUB_SHEBANG") load(":reexports.bzl", "BuiltinPyInfo", "BuiltinPyRuntimeInfo") load(":rule_builders.bzl", "ruleb") -load(":toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE", "TARGET_TOOLCHAIN_TYPE", TOOLCHAIN_TYPE = "TARGET_TOOLCHAIN_TYPE") +load(":toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE", "LAUNCHER_MAKER_TOOLCHAIN_TYPE", TOOLCHAIN_TYPE = "TARGET_TOOLCHAIN_TYPE") load(":transition_labels.bzl", "TRANSITION_LABELS") load(":venv_runfiles.bzl", "create_venv_app_files") _py_builtins = py_internal _EXTERNAL_PATH_PREFIX = "external" _ZIP_RUNFILES_DIRECTORY_NAME = "runfiles" +_INIT_PY = "__init__.py" # Non-Google-specific attributes for executables # These attributes are for rules that accept Python sources. @@ -77,6 +84,14 @@ EXECUTABLE_ATTRS = dicts.add( AGNOSTIC_EXECUTABLE_ATTRS, PY_SRCS_ATTRS, IMPORTS_ATTRS, + WINDOWS_CONSTRAINTS_ATTRS, + # starlark flags attributes + { + "_build_python_zip_flag": attr.label(default = "//python/config_settings:build_python_zip"), + "_default_to_explicit_init_py_flag": attr.label(default = "//python/config_settings:incompatible_default_to_explicit_init_py"), + "_python_import_all_repositories_flag": attr.label(default = "//python/config_settings:experimental_python_import_all_repositories"), + "_python_path_flag": attr.label(default = "//python/config_settings:python_path"), + }, { "interpreter_args": lambda: attrb.StringList( doc = """ @@ -139,7 +154,7 @@ This is mutually exclusive with {obj}`main`. :::{versionadded} 1.3.0 ::: -:::{versionchanged} VERSION_NEXT_FEATURE +:::{versionchanged} 1.7.0 Support added for {obj}`--bootstrap_impl=system_python`. ::: """, @@ -196,6 +211,19 @@ accepting arbitrary Python versions. allow_single_file = True, default = "@bazel_tools//tools/python:python_bootstrap_template.txt", ), + "_build_data_writer": lambda: attrb.Label( + default = "//python/private:build_data_writer", + allow_files = True, + cfg = "exec", + ), + "_debugger_flag": lambda: attrb.Label( + default = "//python/private:debugger_if_target_config", + providers = [PyInfo], + ), + "_exe_zip_maker": lambda: attrb.Label( + cfg = "exec", + default = "//tools/private/zipapp:exe_zip_maker", + ), "_launcher": lambda: attrb.Label( cfg = "target", # NOTE: This is an executable, but is only used for Windows. It @@ -203,35 +231,30 @@ accepting arbitrary Python versions. # empty target for other platforms. default = "//tools/launcher:launcher", ), - # TODO: This appears to be vestigial. It's only added because - # GraphlessQueryTest.testLabelsOperator relies on it to test for - # query behavior of implicit dependencies. - "_py_toolchain_type": attr.label( - default = TARGET_TOOLCHAIN_TYPE, - ), "_python_version_flag": lambda: attrb.Label( default = labels.PYTHON_VERSION, ), + "_uncachable_version_file": lambda: attrb.Label( + default = "//python/private:uncachable_version_file", + allow_files = True, + ), "_venvs_use_declare_symlink_flag": lambda: attrb.Label( default = labels.VENVS_USE_DECLARE_SYMLINK, providers = [BuildSettingInfo], ), - "_windows_constraints": lambda: attrb.LabelList( - default = [ - "@platforms//os:windows", - ], - ), - "_windows_launcher_maker": lambda: attrb.Label( - default = "@bazel_tools//tools/launcher:launcher_maker", - cfg = "exec", - executable = True, - ), "_zipper": lambda: attrb.Label( cfg = "exec", executable = True, default = "@bazel_tools//tools/zip:zipper", ), }, + { + "_windows_launcher_maker": lambda: attrb.Label( + default = "@bazel_tools//tools/launcher:launcher_maker", + cfg = "exec", + executable = True, + ), + } if not rp_config.bazel_9_or_later else {}, ) def convert_legacy_create_init_to_int(kwargs): @@ -255,42 +278,11 @@ def py_executable_impl(ctx, *, is_test, inherited_environment): def create_binary_semantics(): return create_binary_semantics_struct( # keep-sorted start - create_executable = _create_executable, - get_cc_details_for_binary = _get_cc_details_for_binary, - get_central_uncachable_version_file = lambda ctx: None, - get_coverage_deps = _get_coverage_deps, - get_debugger_deps = _get_debugger_deps, - get_extra_common_runfiles_for_binary = lambda ctx: ctx.runfiles(), - get_extra_providers = _get_extra_providers, - get_extra_write_build_data_env = lambda ctx: {}, - get_imports = get_imports, - get_interpreter_path = _get_interpreter_path, get_native_deps_dso_name = _get_native_deps_dso_name, - get_native_deps_user_link_flags = _get_native_deps_user_link_flags, - get_stamp_flag = _get_stamp_flag, - maybe_precompile = maybe_precompile, should_build_native_deps_dso = lambda ctx: False, - should_create_init_files = _should_create_init_files, - should_include_build_data = lambda ctx: False, # keep-sorted end ) -def _get_coverage_deps(ctx, runtime_details): - _ = ctx, runtime_details # @unused - return [] - -def _get_debugger_deps(ctx, runtime_details): - _ = ctx, runtime_details # @unused - return [] - -def _get_extra_providers(ctx, main_py, runtime_details): - _ = ctx, main_py, runtime_details # @unused - return [] - -def _get_stamp_flag(ctx): - # NOTE: Undocumented API; private to builtins - return ctx.configuration.stamp_binaries - def _should_create_init_files(ctx): if ctx.attr.legacy_create_init == -1: return not read_possibly_native_flag(ctx, "default_to_explicit_init_py") @@ -307,10 +299,11 @@ def _create_executable( runtime_details, cc_details, native_deps_details, - runfiles_details): + runfiles_details, + extra_deps): _ = is_test, cc_details, native_deps_details # @unused - is_windows = target_platform_has_any_constraint(ctx, ctx.attr._windows_constraints) + is_windows = is_windows_platform(ctx) if is_windows: if not executable.extension == "exe": @@ -319,6 +312,9 @@ def _create_executable( else: base_executable_name = executable.basename + # Venv outputs are package-relative, so preserve the full target name to + # avoid collisions between targets like foo/tool, bar/tool, and foo_tool. + venv_output_prefix = ctx.label.name venv = None # The check for stage2_bootstrap_template is to support legacy @@ -331,9 +327,13 @@ def _create_executable( ): venv = _create_venv( ctx, - output_prefix = base_executable_name, + output_prefix = venv_output_prefix, imports = imports, runtime_details = runtime_details, + add_runfiles_root_to_sys_path = ( + "1" if BootstrapImplFlag.get_value(ctx) == BootstrapImplFlag.SYSTEM_PYTHON else "0" + ), + extra_deps = extra_deps, ) stage2_bootstrap = _create_stage2_bootstrap( @@ -344,12 +344,13 @@ def _create_executable( imports = imports, runtime_details = runtime_details, venv = venv, + build_data_file = runfiles_details.build_data_file, ) extra_runfiles = ctx.runfiles( [stage2_bootstrap] + ( venv.files_without_interpreter if venv else [] ), - ) + ).merge(venv.lib_runfiles) zip_main = _create_zip_main( ctx, stage2_bootstrap = stage2_bootstrap, @@ -373,20 +374,38 @@ def _create_executable( _create_zip_file( ctx, output = zip_file, - original_nonzip_executable = executable, zip_main = zip_main, - runfiles = runfiles_details.default_runfiles.merge(extra_runfiles), + runfiles = runfiles_details.runfiles_without_exe.merge(extra_runfiles), ) - extra_files_to_build = [] + extra_default_outputs = [] # NOTE: --build_python_zip defaults to true on Windows - build_zip_enabled = read_possibly_native_flag(ctx, "build_python_zip") + build_zip_enabled = read_possibly_native_flag(ctx, "build_python_zip") and not is_windows + if is_windows: + # The legacy build_python_zip codepath isn't compatible with full venvs on Windows. + build_zip_enabled = False # When --build_python_zip is enabled, then the zip file becomes # one of the default outputs. if build_zip_enabled: - extra_files_to_build.append(zip_file) + if not is_windows: + # buildifier: disable=print + print( + """ +====================================================================== +WARNING: Target: {} + The `--build_python_zip` flag and implicit zipapp output of `py_binary` + and `py_test` is deprecated and will be removed in a future release. + Switch to `py_zipapp_binary` or `py_zipapp_test`. For migration + instructions and guide, see: + + https://github.com/bazel-contrib/rules_python/issues/3567 +====================================================================== + """.rstrip().format(ctx.label), + ) + + extra_default_outputs.append(zip_file) # The logic here is a bit convoluted. Essentially, there are 3 types of # executables produced: @@ -405,20 +424,20 @@ def _create_executable( else: bootstrap_output = executable else: - _create_windows_exe_launcher( + create_windows_exe_launcher( ctx, output = executable, use_zip_file = build_zip_enabled, python_binary_path = runtime_details.executable_interpreter_path, ) - if not build_zip_enabled: - # On Windows, the main executable has an "exe" extension, so - # here we re-use the un-extensioned name for the bootstrap output. - bootstrap_output = ctx.actions.declare_file(base_executable_name) - # The launcher looks for the non-zip executable next to - # itself, so add it to the default outputs. - extra_files_to_build.append(bootstrap_output) + # On Windows, the main executable has an "exe" extension, so + # here we re-use the un-extensioned name for the bootstrap output. + bootstrap_output = ctx.actions.declare_file(base_executable_name, sibling = executable) + + # The launcher looks for the non-zip executable next to + # itself, so add it to the default outputs. + extra_default_outputs.append(bootstrap_output) if should_create_executable_zip: if bootstrap_output != None: @@ -456,14 +475,39 @@ def _create_executable( build_zip_enabled = build_zip_enabled, )) + app_runfiles = builders.RunfilesBuilder() + app_runfiles.add(runfiles_details.app_runfiles) + if venv: + app_runfiles.add(venv.files_without_interpreter) + app_runfiles.add(venv.lib_runfiles) + # The interpreter is added this late in the process so that it isn't # added to the zipped files. - if venv and venv.interpreter: - extra_runfiles = extra_runfiles.merge(ctx.runfiles([venv.interpreter])) - return create_executable_result_struct( - extra_files_to_build = depset(extra_files_to_build), + if venv and venv.interpreter_runfiles: + extra_runfiles = extra_runfiles.merge(venv.interpreter_runfiles) + return struct( + # depset[File] of additional files that should be included as default + # outputs. + extra_default_outputs = depset(extra_default_outputs), + # dict[str, depset[File]]; additional output groups that should be + # returned. output_groups = {"python_zip_file": depset([zip_file])}, + # runfiles; additional runfiles to include. extra_runfiles = extra_runfiles, + # File|None; the stage2 bootstrap file, if any + stage2_bootstrap = stage2_bootstrap, + # runfiles; runfiles for the app itself (e.g its deps, but no Python + # runtime files) + app_runfiles = app_runfiles.build(ctx), + # depset[ExplicitSymlink]None; symlinks that should be created in + # the venv to augment app_runfiles + venv_app_symlinks = venv.lib_symlinks if venv else None, + # File|None; the venv `bin/python3` file, if any. + venv_python_exe = venv.interpreter if venv else None, + # runfiles|None; runfiles in the venv for the interpreter + venv_interpreter_runfiles = venv.interpreter_runfiles if venv else None, + # depset[ExplicitSymlink]|None; symlinks that should be created + venv_interpreter_symlinks = venv.interpreter_symlinks if venv else None, ) def _create_zip_main(ctx, *, stage2_bootstrap, runtime_details, venv): @@ -483,10 +527,7 @@ def _create_zip_main(ctx, *, stage2_bootstrap, runtime_details, venv): substitutions = { "%python_binary%": python_binary, "%python_binary_actual%": python_binary_actual, - "%stage2_bootstrap%": "{}/{}".format( - ctx.workspace_name, - stage2_bootstrap.short_path, - ), + "%stage2_bootstrap%": runfiles_root_path(ctx, stage2_bootstrap.short_path), "%workspace_name%": ctx.workspace_name, }, ) @@ -498,33 +539,140 @@ def _create_zip_main(ctx, *, stage2_bootstrap, runtime_details, venv): # * https://snarky.ca/how-virtual-environments-work/ # * https://github.com/python/cpython/blob/main/Modules/getpath.py # * https://github.com/python/cpython/blob/main/Lib/site.py -def _create_venv(ctx, output_prefix, imports, runtime_details): - create_full_venv = BootstrapImplFlag.get_value(ctx) == BootstrapImplFlag.SCRIPT - venv = "_{}.venv".format(output_prefix.lstrip("_")) +def _create_venv(ctx, output_prefix, imports, runtime_details, add_runfiles_root_to_sys_path, extra_deps): + venv_ctx_rel_root = "_{}.venv".format(output_prefix.lstrip("_")) + runtime = runtime_details.effective_runtime + if runtime.interpreter: + interpreter_actual_path = runfiles_root_path(ctx, runtime.interpreter.short_path) + else: + interpreter_actual_path = runtime.interpreter_path + + is_windows = is_windows_platform(ctx) + if is_windows: + venv_details = _create_venv_windows( + ctx, + venv_ctx_rel_root = venv_ctx_rel_root, + interpreter_actual_path = interpreter_actual_path, + runtime = runtime, + ) + else: + venv_details = _create_venv_unixy( + ctx, + venv_ctx_rel_root = venv_ctx_rel_root, + interpreter_actual_path = interpreter_actual_path, + runtime = runtime, + ) + + site_packages = "{}/{}".format(venv_ctx_rel_root, venv_details.site_packages) + + pth = ctx.actions.declare_file("{}/bazel.pth".format(site_packages)) + ctx.actions.write(pth, "import _bazel_site_init\n") + + site_init = ctx.actions.declare_file("{}/_bazel_site_init.py".format(site_packages)) + computed_subs = ctx.actions.template_dict() + computed_subs.add_joined("%imports%", imports, join_with = ":", map_each = _map_each_identity) + ctx.actions.expand_template( + template = runtime.site_init_template, + output = site_init, + substitutions = { + "%add_runfiles_root_to_sys_path%": add_runfiles_root_to_sys_path, + "%coverage_tool%": _get_coverage_tool_runfiles_path(ctx, runtime), + "%import_all%": "True" if read_possibly_native_flag(ctx, "python_import_all_repositories") else "False", + "%site_init_runfiles_path%": runfiles_root_path(ctx, site_init.short_path), + "%workspace_name%": ctx.workspace_name, + }, + computed_substitutions = computed_subs, + ) + + # See https://docs.python.org/3/library/sysconfig.html#posix-prefix + # for how schemes map under the venv. + venv_dir_map = { + VenvSymlinkKind.BIN: "{}/{}".format(venv_ctx_rel_root, venv_details.bin_dir), + VenvSymlinkKind.LIB: site_packages, + VenvSymlinkKind.INCLUDE: "{}/{}".format(venv_ctx_rel_root, venv_details.include_dir), + VenvSymlinkKind.DATA: venv_ctx_rel_root, + } + venv_app_files = create_venv_app_files( + ctx, + deps = collect_deps(ctx, extra_deps), + venv_dir_map = venv_dir_map, + ) + + files_without_interpreter = [pth, site_init] + venv_app_files.venv_files + if venv_details.pyvenv_cfg: + files_without_interpreter.append(venv_details.pyvenv_cfg) + + return struct( + # File or None; the `bin/python3` executable in the venv. + # None if a full venv isn't created. + interpreter = venv_details.interpreter, + # Files in the venv that need to be created for the interpreter to work + interpreter_runfiles = venv_details.interpreter_runfiles, + # depset[ExplicitSymlink] of symlinks to create. + # This is only used when declare_symlink() can't be used to represent + # creating such a link (i.e Windows) + interpreter_symlinks = venv_details.interpreter_symlinks, + # bool; True if the venv should be recreated at runtime + recreate_venv_at_runtime = venv_details.recreate_venv_at_runtime, + # Runfiles root relative path or absolute path + interpreter_actual_path = interpreter_actual_path, + files_without_interpreter = files_without_interpreter, + # string; venv-relative path to the site-packages directory. + venv_site_packages = venv_details.site_packages, + # string; runfiles-root relative path to venv root. + venv_root = runfiles_root_path( + ctx, + paths.join( + py_internal.get_label_repo_runfiles_path(ctx.label), + venv_ctx_rel_root, + ), + ), + # venv files for user library dependencies (files that are specific + # to the executable bootstrap and python runtime aren't here). + # `root_symlinks` should be used, otherwise, with symlinks files always go + # to `_main` prefix, and binaries from non-root module become broken. + lib_runfiles = ctx.runfiles( + root_symlinks = venv_app_files.runfiles_symlinks, + ), + lib_symlinks = venv_app_files.explicit_symlinks, + ) + +def _create_venv_unixy(ctx, *, venv_ctx_rel_root, runtime, interpreter_actual_path): + interpreter_runfiles = builders.RunfilesBuilder() + is_bootstrap_script = BootstrapImplFlag.get_value(ctx) == BootstrapImplFlag.SCRIPT + create_full_venv = True + + # The legacy build_python_zip codepath (enabled by default on windows) isn't + # compatible with full venv. + # TODO: Use non-build_python_zip codepath for Windows + if not rp_config.bazel_8_or_later and not is_bootstrap_script: + # Full venv for Bazel 7 + system_python is disabled because packaging + # it using build_python_zip=true or rules_pkg breaks. + # * Using build_python_zip=true breaks because the legacy zipapp support + # doesn't handle symlinks correctly. + # * Using rules_pkg breaks for two reasons: + # 1. It requires rules_pkg 1.2, which crashes under Bazel 7 + # 2. It requires File.is_symlink, which is a Bazel 8+ API. + # While bootstrap=script has the same problems, it has always been like + # that. + create_full_venv = False if create_full_venv: # The pyvenv.cfg file must be present to trigger the venv site hooks. # Because it's paths are expected to be absolute paths, we can't reliably # put much in it. See https://github.com/python/cpython/issues/83650 - pyvenv_cfg = ctx.actions.declare_file("{}/pyvenv.cfg".format(venv)) + pyvenv_cfg = ctx.actions.declare_file("{}/pyvenv.cfg".format(venv_ctx_rel_root)) ctx.actions.write(pyvenv_cfg, "") else: pyvenv_cfg = None - runtime = runtime_details.effective_runtime - venvs_use_declare_symlink_enabled = ( VenvsUseDeclareSymlinkFlag.get_value(ctx) == VenvsUseDeclareSymlinkFlag.YES ) - recreate_venv_at_runtime = False - if runtime.interpreter: - interpreter_actual_path = runfiles_root_path(ctx, runtime.interpreter.short_path) - else: - interpreter_actual_path = runtime.interpreter_path - - bin_dir = "{}/bin".format(venv) + recreate_venv_at_runtime = False + venv_bin_ctx_rel_path = "{}/bin".format(venv_ctx_rel_root) if create_full_venv: # Some wrappers around the interpreter (e.g. pyenv) use the program # name to decide what to do, so preserve the name. @@ -536,7 +684,7 @@ def _create_venv(ctx, output_prefix, imports, runtime_details): # When the venv symlinks are disabled, the $venv/bin/python3 file isn't # needed or used at runtime. However, the zip code uses the interpreter # File object to figure out some paths. - interpreter = ctx.actions.declare_file("{}/{}".format(bin_dir, py_exe_basename)) + interpreter = ctx.actions.declare_file("{}/{}".format(venv_bin_ctx_rel_path, py_exe_basename)) ctx.actions.write(interpreter, "actual:{}".format(interpreter_actual_path)) elif runtime.interpreter: @@ -544,7 +692,8 @@ def _create_venv(ctx, output_prefix, imports, runtime_details): # declare_symlink() is required to ensure that the resulting file # in runfiles is always a symlink. An RBE implementation, for example, # may choose to write what symlink() points to instead. - interpreter = ctx.actions.declare_symlink("{}/{}".format(bin_dir, py_exe_basename)) + interpreter = ctx.actions.declare_symlink("{}/{}".format(venv_bin_ctx_rel_path, py_exe_basename)) + interpreter_runfiles.add(interpreter) rel_path = relative_path( # dirname is necessary because a relative symlink is relative to @@ -552,10 +701,10 @@ def _create_venv(ctx, output_prefix, imports, runtime_details): from_ = paths.dirname(runfiles_root_path(ctx, interpreter.short_path)), to = interpreter_actual_path, ) - ctx.actions.symlink(output = interpreter, target_path = rel_path) else: - interpreter = ctx.actions.declare_symlink("{}/{}".format(bin_dir, py_exe_basename)) + interpreter = ctx.actions.declare_symlink("{}/{}".format(venv_bin_ctx_rel_path, py_exe_basename)) + interpreter_runfiles.add(interpreter) ctx.actions.symlink(output = interpreter, target_path = runtime.interpreter_path) else: interpreter = None @@ -574,55 +723,121 @@ def _create_venv(ctx, output_prefix, imports, runtime_details): if "t" in runtime.abi_flags: version += "t" - venv_site_packages = "lib/python{}/site-packages".format(version) - site_packages = "{}/{}".format(venv, venv_site_packages) - pth = ctx.actions.declare_file("{}/bazel.pth".format(site_packages)) - ctx.actions.write(pth, "import _bazel_site_init\n") - - site_init = ctx.actions.declare_file("{}/_bazel_site_init.py".format(site_packages)) - computed_subs = ctx.actions.template_dict() - computed_subs.add_joined("%imports%", imports, join_with = ":", map_each = _map_each_identity) - ctx.actions.expand_template( - template = runtime.site_init_template, - output = site_init, - substitutions = { - "%coverage_tool%": _get_coverage_tool_runfiles_path(ctx, runtime), - "%import_all%": "True" if read_possibly_native_flag(ctx, "python_import_all_repositories") else "False", - "%site_init_runfiles_path%": "{}/{}".format(ctx.workspace_name, site_init.short_path), - "%workspace_name%": ctx.workspace_name, - }, - computed_substitutions = computed_subs, + site_packages = "lib/python{}/site-packages".format(version) + return _venv_details( + interpreter = interpreter, + pyvenv_cfg = pyvenv_cfg, + site_packages = site_packages, + bin_dir = "bin", + include_dir = "include", + recreate_venv_at_runtime = recreate_venv_at_runtime, + interpreter_runfiles = interpreter_runfiles.build(ctx), + interpreter_symlinks = depset(), ) - venv_dir_map = { - VenvSymlinkKind.BIN: bin_dir, - VenvSymlinkKind.LIB: site_packages, - } - venv_app_files = create_venv_app_files(ctx, ctx.attr.deps, venv_dir_map) +def _create_venv_windows(ctx, *, venv_ctx_rel_root, runtime, interpreter_actual_path): + interpreter_runfiles = builders.RunfilesBuilder() + interpreter_symlinks = builders.DepsetBuilder() + + # Some wrappers around the interpreter (e.g. pyenv) use the program + # name to decide what to do, so preserve the name. + py_exe_basename = paths.basename(interpreter_actual_path) + venv_bin_rel_path = "Scripts" + venv_bin_ctx_rel_path = "{}/{}".format(venv_ctx_rel_root, venv_bin_rel_path) + if runtime.interpreter: + venv_rel_path = paths.join(venv_bin_rel_path, py_exe_basename) + venv_ctx_rel_path = paths.join(venv_ctx_rel_root, venv_rel_path) + interpreter = ctx.actions.declare_file(venv_ctx_rel_path) + interpreter_runfiles.add(interpreter) + ctx.actions.symlink(output = interpreter, target_file = runtime.interpreter) + + rf_path = runfiles_root_path(ctx, interpreter.short_path) + interpreter_symlinks.add(ExplicitSymlink( + runfiles_path = rf_path, + venv_path = venv_rel_path, + link_to_path = interpreter_actual_path, + files = depset([runtime.interpreter]), + )) - files_without_interpreter = [pth, site_init] + venv_app_files - if pyvenv_cfg: - files_without_interpreter.append(pyvenv_cfg) + # This isn't strictly correct, but should work ok. + interpreter_symlinks.add(ExplicitSymlink( + runfiles_path = paths.join(paths.dirname(rf_path), "pythonw.exe"), + venv_path = paths.join(paths.dirname(venv_rel_path), "pythonw.exe"), + link_to_path = paths.join(paths.dirname(interpreter_actual_path), "pythonw.exe"), + files = depset(), + )) + else: + # It's OK to use declare_symlink here because an absolute path + # will be written to it, so Bazel won't mangle it. + interpreter = ctx.actions.declare_symlink("{}/{}".format(venv_bin_ctx_rel_path, py_exe_basename)) + interpreter_runfiles.add(interpreter) + ctx.actions.symlink(output = interpreter, target_path = runtime.interpreter_path) + + # NOTE: The .dll files must exist, however, they may not be known at build time + # if the interpreter is resolved at runtime. + for f in runtime.venv_bin_files: + venv_rel_path = paths.join(venv_bin_rel_path, f.basename) + venv_ctx_rel_path = paths.join(venv_ctx_rel_root, venv_rel_path) + + venv_file = ctx.actions.declare_file(venv_ctx_rel_path) + ctx.actions.symlink(output = venv_file, target_file = f) + + interpreter_runfiles.add(venv_file) + + rf_path = runfiles_root_path(ctx, venv_file.short_path) + interpreter_symlinks.add(ExplicitSymlink( + runfiles_path = rf_path, + venv_path = venv_rel_path, + link_to_path = runfiles_root_path(ctx, f.short_path), + files = depset([f]), + )) + + # See site.py logic: Windows uses a version/build agnostic site-packages path + site_packages = "Lib/site-packages" + + return _venv_details( + interpreter = interpreter, + pyvenv_cfg = None, + site_packages = site_packages, + bin_dir = venv_bin_rel_path, + include_dir = "Include", + recreate_venv_at_runtime = True, + interpreter_runfiles = interpreter_runfiles.build(ctx), + interpreter_symlinks = interpreter_symlinks.build(), + ) +def _venv_details( + *, + interpreter, + pyvenv_cfg, + site_packages, + bin_dir, + include_dir, + recreate_venv_at_runtime, + interpreter_runfiles, + interpreter_symlinks): + """Helper to create a struct of platform-specific venv details.""" return struct( - # File or None; the `bin/python3` executable in the venv. - # None if a full venv isn't created. + # File; the `bin/python` executable (or equivalent) within the venv. interpreter = interpreter, - # bool; True if the venv should be recreated at runtime + # File|None; the pyvenv.cfg file, if any. May be none, in which case, + # it's expected that one will be created at runtime. + pyvenv_cfg = pyvenv_cfg, + # str; venv-relative path to the site-packages directory + site_packages = site_packages, + # str; venv-relative path to the venv's bin directory. + bin_dir = bin_dir, + # str; venv-relative-path to the venv's include directory. + include_dir = include_dir, + # bool; True if the venv needs to be recreated at runtime (because the + # build-time construction isn't sufficient). False if the build-time + # constructed venv is sufficient. recreate_venv_at_runtime = recreate_venv_at_runtime, - # Runfiles root relative path or absolute path - interpreter_actual_path = interpreter_actual_path, - files_without_interpreter = files_without_interpreter, - # string; venv-relative path to the site-packages directory. - venv_site_packages = venv_site_packages, - # string; runfiles-root relative path to venv root. - venv_root = runfiles_root_path( - ctx, - paths.join( - py_internal.get_label_repo_runfiles_path(ctx.label), - venv, - ), - ), + # runfiles; runfiles for interpreter-specific files in the venv. + interpreter_runfiles = interpreter_runfiles, + # depset[ExplicitSymlink] of symlinks specific + # to the interpreter. Only used for Windows. + interpreter_symlinks = interpreter_symlinks, ) def _map_each_identity(v): @@ -632,10 +847,7 @@ def _get_coverage_tool_runfiles_path(ctx, runtime): if (ctx.configuration.coverage_enabled and runtime and runtime.coverage_tool): - return "{}/{}".format( - ctx.workspace_name, - runtime.coverage_tool.short_path, - ) + return runfiles_root_path(ctx, runtime.coverage_tool.short_path) else: return "" @@ -647,6 +859,7 @@ def _create_stage2_bootstrap( main_py, imports, runtime_details, + build_data_file, venv): output = ctx.actions.declare_file( # Prepend with underscore to prevent pytest from trying to @@ -667,6 +880,8 @@ def _create_stage2_bootstrap( template = template, output = output, substitutions = { + "%build_data_file%": runfiles_root_path(ctx, build_data_file.short_path), + "%coverage_instrumented%": str(int(ctx.configuration.coverage_enabled and ctx.coverage_instrumented())), "%coverage_tool%": _get_coverage_tool_runfiles_path(ctx, runtime), "%import_all%": "True" if read_possibly_native_flag(ctx, "python_import_all_repositories") else "False", "%imports%": ":".join(imports.to_list()), @@ -714,10 +929,7 @@ def _create_stage1_bootstrap( resolve_python_binary_at_runtime = "1" subs = { - "%interpreter_args%": "\n".join([ - '"{}"'.format(v) - for v in ctx.attr.interpreter_args - ]), + "%interpreter_args%": "\n".join(ctx.attr.interpreter_args), "%is_zipfile%": "1" if is_for_zip else "0", "%python_binary%": python_binary_path, "%python_binary_actual%": python_binary_actual, @@ -727,12 +939,20 @@ def _create_stage1_bootstrap( "%venv_rel_site_packages%": venv.venv_site_packages if venv else "", "%workspace_name%": ctx.workspace_name, } + computed_subs = ctx.actions.template_dict() + if venv: + runtime_venv_symlinks = depset( + transitive = [venv.interpreter_symlinks, venv.lib_symlinks], + ) + computed_subs.add_joined( + "%runtime_venv_symlinks%", + runtime_venv_symlinks, + join_with = "\n", + map_each = _map_runtime_venv_symlink, + ) if stage2_bootstrap: - subs["%stage2_bootstrap%"] = "{}/{}".format( - ctx.workspace_name, - stage2_bootstrap.short_path, - ) + subs["%stage2_bootstrap%"] = runfiles_root_path(ctx, stage2_bootstrap.short_path) template = runtime.bootstrap_template subs["%shebang%"] = runtime.stub_shebang elif not ctx.files.srcs: @@ -741,10 +961,7 @@ def _create_stage1_bootstrap( if (ctx.configuration.coverage_enabled and runtime and runtime.coverage_tool): - coverage_tool_runfiles_path = "{}/{}".format( - ctx.workspace_name, - runtime.coverage_tool.short_path, - ) + coverage_tool_runfiles_path = runfiles_root_path(ctx, runtime.coverage_tool.short_path) else: coverage_tool_runfiles_path = "" if runtime: @@ -757,46 +974,20 @@ def _create_stage1_bootstrap( subs["%coverage_tool%"] = coverage_tool_runfiles_path subs["%import_all%"] = ("True" if read_possibly_native_flag(ctx, "python_import_all_repositories") else "False") subs["%imports%"] = ":".join(imports.to_list()) - subs["%main%"] = "{}/{}".format(ctx.workspace_name, main_py.short_path) + subs["%main%"] = runfiles_root_path(ctx, main_py.short_path) ctx.actions.expand_template( template = template, output = output, substitutions = subs, + computed_substitutions = computed_subs, is_executable = True, ) -def _create_windows_exe_launcher( - ctx, - *, - output, - python_binary_path, - use_zip_file): - launch_info = ctx.actions.args() - launch_info.use_param_file("%s", use_always = True) - launch_info.set_param_file_format("multiline") - launch_info.add("binary_type=Python") - launch_info.add(ctx.workspace_name, format = "workspace_name=%s") - launch_info.add( - "1" if py_internal.runfiles_enabled(ctx) else "0", - format = "symlink_runfiles_enabled=%s", - ) - launch_info.add(python_binary_path, format = "python_bin_path=%s") - launch_info.add("1" if use_zip_file else "0", format = "use_zip_file=%s") - - launcher = ctx.attr._launcher[DefaultInfo].files_to_run.executable - ctx.actions.run( - executable = ctx.executable._windows_launcher_maker, - arguments = [launcher.path, launch_info, output.path], - inputs = [launcher], - outputs = [output], - mnemonic = "PyBuildLauncher", - progress_message = "Creating launcher for %{label}", - # Needed to inherit PATH when using non-MSVC compilers like MinGW - use_default_shell_env = True, - ) +def _map_runtime_venv_symlink(entry): + return entry.venv_path + "|" + entry.link_to_path -def _create_zip_file(ctx, *, output, original_nonzip_executable, zip_main, runfiles): +def _create_zip_file(ctx, *, output, zip_main, runfiles): """Create a Python zipapp (zip with __main__.py entry point).""" workspace_name = ctx.workspace_name legacy_external_runfiles = _py_builtins.get_legacy_external_runfiles(ctx) @@ -808,48 +999,46 @@ def _create_zip_file(ctx, *, output, original_nonzip_executable, zip_main, runfi manifest.add("__main__.py={}".format(zip_main.path)) manifest.add("__init__.py=") manifest.add( - "{}=".format( - _get_zip_runfiles_path("__init__.py", workspace_name, legacy_external_runfiles), - ), + "{}=".format(_get_zip_runfiles_path(_INIT_PY, workspace_name)), + ) + + def map_zip_empty_filenames(list_paths_cb): + return [ + # FIXME @aignas 2025-12-06: what kind of paths do we expect here? Will they + # ever start with `../` or `external`? + _get_zip_runfiles_path_legacy(path, workspace_name, legacy_external_runfiles) + "=" + for path in list_paths_cb().to_list() + ] + + manifest.add_all( + # NOTE: Accessing runfiles.empty_filenames implicitly flattens the runfiles. + # Smuggle a lambda in via a list to defer that flattening. + [lambda: runfiles.empty_filenames], + map_each = map_zip_empty_filenames, + allow_closure = True, ) - for path in runfiles.empty_filenames.to_list(): - manifest.add("{}=".format(_get_zip_runfiles_path(path, workspace_name, legacy_external_runfiles))) def map_zip_runfiles(file): - if file != original_nonzip_executable and file != output: - return "{}={}".format( - _get_zip_runfiles_path(file.short_path, workspace_name, legacy_external_runfiles), - file.path, - ) - else: - return None + return ( + # NOTE: Use "+" for performance + _get_zip_runfiles_path_legacy(file.short_path, workspace_name, legacy_external_runfiles) + + "=" + file.path + ) manifest.add_all(runfiles.files, map_each = map_zip_runfiles, allow_closure = True) inputs = [zip_main] - if _py_builtins.is_bzlmod_enabled(ctx): - zip_repo_mapping_manifest = ctx.actions.declare_file( - output.basename + ".repo_mapping", - sibling = output, - ) - _py_builtins.create_repo_mapping_manifest( - ctx = ctx, - runfiles = runfiles, - output = zip_repo_mapping_manifest, - ) + zip_repo_mapping_manifest = maybe_create_repo_mapping( + ctx = ctx, + runfiles = runfiles, + ) + if zip_repo_mapping_manifest: manifest.add("{}/_repo_mapping={}".format( _ZIP_RUNFILES_DIRECTORY_NAME, zip_repo_mapping_manifest.path, )) inputs.append(zip_repo_mapping_manifest) - for artifact in runfiles.files.to_list(): - # Don't include the original executable because it isn't used by the - # zip file, so no need to build it for the action. - # Don't include the zipfile itself because it's an output. - if artifact != original_nonzip_executable and artifact != output: - inputs.append(artifact) - zip_cli_args = ctx.actions.args() zip_cli_args.add("cC") zip_cli_args.add(output) @@ -857,23 +1046,35 @@ def _create_zip_file(ctx, *, output, original_nonzip_executable, zip_main, runfi ctx.actions.run( executable = ctx.executable._zipper, arguments = [zip_cli_args, manifest], - inputs = depset(inputs), + inputs = depset(inputs, transitive = [runfiles.files]), outputs = [output], use_default_shell_env = True, mnemonic = "PythonZipper", progress_message = "Building Python zip: %{label}", ) -def _get_zip_runfiles_path(path, workspace_name, legacy_external_runfiles): - if legacy_external_runfiles and path.startswith(_EXTERNAL_PATH_PREFIX): - zip_runfiles_path = paths.relativize(path, _EXTERNAL_PATH_PREFIX) +def _get_zip_runfiles_path(path, workspace_name = ""): + # NOTE @aignas 2025-12-06: This is to avoid the prefix checking in the very + # trivial case that is always happening once per this function call + + # NOTE: Use "+" for performance + if workspace_name: + # NOTE: Use "+" for performance + return _ZIP_RUNFILES_DIRECTORY_NAME + "/" + workspace_name + "/" + path else: + return _ZIP_RUNFILES_DIRECTORY_NAME + "/" + path + +def _get_zip_runfiles_path_legacy(path, workspace_name, legacy_external_runfiles): + if legacy_external_runfiles and path.startswith(_EXTERNAL_PATH_PREFIX): + return _get_zip_runfiles_path(path.removeprefix(_EXTERNAL_PATH_PREFIX)) + elif path.startswith("../"): # NOTE: External runfiles (artifacts in other repos) will have a leading # path component of "../" so that they refer outside the main workspace - # directory and into the runfiles root. By normalizing, we simplify e.g. + # directory and into the runfiles root. So we simplify it, e.g. # "workspace/../foo/bar" to simply "foo/bar". - zip_runfiles_path = paths.normalize("{}/{}".format(workspace_name, path)) - return "{}/{}".format(_ZIP_RUNFILES_DIRECTORY_NAME, zip_runfiles_path) + return _get_zip_runfiles_path(path[3:]) + else: + return _get_zip_runfiles_path(path, workspace_name) def _create_executable_zip_file( ctx, @@ -899,15 +1100,16 @@ def _create_executable_zip_file( else: ctx.actions.write(prelude, "#!/usr/bin/env python3\n") - ctx.actions.run_shell( - command = "cat {prelude} {zip} > {output}".format( - prelude = prelude.path, - zip = zip_file.path, - output = output.path, - ), - inputs = [prelude, zip_file], + args = ctx.actions.args() + args.add(prelude) + args.add(zip_file) + args.add(output) + actions_run( + ctx, + executable = ctx.attr._exe_zip_maker, + arguments = [args], + inputs = depset([prelude, zip_file]), outputs = [output], - use_default_shell_env = True, mnemonic = "PyBuildExecutableZip", progress_message = "Build Python zip executable: %{label}", ) @@ -952,10 +1154,6 @@ def _get_native_deps_dso_name(ctx): _ = ctx # @unused fail("Building native deps DSO not supported.") -def _get_native_deps_user_link_flags(ctx): - _ = ctx # @unused - fail("Building native deps DSO not supported.") - def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment = []): """Base rule implementation for a Python executable. @@ -979,8 +1177,13 @@ def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment = main_py = determine_main(ctx) else: main_py = None + + # Keep a reference to the main source file (before it may be replaced with a + # precompiled pyc below) so the test-main validation can statically analyze + # the original source. + main_py_source = main_py direct_sources = filter_to_py_srcs(ctx.files.srcs) - precompile_result = semantics.maybe_precompile(ctx, direct_sources) + precompile_result = maybe_precompile(ctx, direct_sources) required_py_files = precompile_result.keep_srcs required_pyc_files = [] @@ -1004,20 +1207,16 @@ def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment = default_outputs.add(precompile_result.keep_srcs) default_outputs.add(required_pyc_files) - imports = collect_imports(ctx, semantics) - - runtime_details = _get_runtime_details(ctx, semantics) - if ctx.configuration.coverage_enabled: - extra_deps = semantics.get_coverage_deps(ctx, runtime_details) - else: - extra_deps = [] + extra_deps = [] # The debugger dependency should be prevented by select() config elsewhere, # but just to be safe, also guard against adding it to the output here. if not _is_tool_config(ctx): - extra_deps.extend(semantics.get_debugger_deps(ctx, runtime_details)) + extra_deps.append(ctx.attr._debugger_flag) - cc_details = semantics.get_cc_details_for_binary(ctx, extra_deps = extra_deps) + imports = collect_imports(ctx, extra_deps = extra_deps) + runtime_details = _get_runtime_details(ctx) + cc_details = _get_cc_details_for_binary(ctx, extra_deps = extra_deps) native_deps_details = _get_native_deps_details( ctx, semantics = semantics, @@ -1032,15 +1231,13 @@ def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment = required_pyc_files = required_pyc_files, implicit_pyc_files = implicit_pyc_files, implicit_pyc_source_files = implicit_pyc_source_files, + runtime_runfiles = runtime_details.runfiles, extra_common_runfiles = [ - runtime_details.runfiles, cc_details.extra_runfiles, native_deps_details.runfiles, - semantics.get_extra_common_runfiles_for_binary(ctx), ], - semantics = semantics, ) - exec_result = semantics.create_executable( + exec_result = _create_executable( ctx, executable = executable, main_py = main_py, @@ -1050,11 +1247,12 @@ def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment = cc_details = cc_details, native_deps_details = native_deps_details, runfiles_details = runfiles_details, + extra_deps = extra_deps, ) - default_outputs.add(exec_result.extra_files_to_build) + default_outputs.add(exec_result.extra_default_outputs) extra_exec_runfiles = exec_result.extra_runfiles.merge( - ctx.runfiles(transitive_files = exec_result.extra_files_to_build), + ctx.runfiles(transitive_files = exec_result.extra_default_outputs), ) # Copy any existing fields in case of company patches. @@ -1065,24 +1263,120 @@ def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment = ) )) - return _create_providers( - ctx = ctx, + providers = [] + + _add_provider_default_info( + providers, + ctx, executable = executable, + default_outputs = default_outputs.build(), runfiles_details = runfiles_details, + ) + _add_provider_instrumented_files_info(providers, ctx) + _add_provider_run_environment_info(providers, ctx, inherited_environment) + _add_provider_py_executable_info( + providers, + app_runfiles = exec_result.app_runfiles, + build_data_file = runfiles_details.build_data_file, + interpreter_args = ctx.attr.interpreter_args, + interpreter_path = runtime_details.executable_interpreter_path, main_py = main_py, - imports = imports, + runfiles_without_exe = runfiles_details.runfiles_without_exe, + stage2_bootstrap = exec_result.stage2_bootstrap, + venv_app_symlinks = exec_result.venv_app_symlinks, + venv_interpreter_runfiles = exec_result.venv_interpreter_runfiles, + venv_interpreter_symlinks = exec_result.venv_interpreter_symlinks, + venv_python_exe = exec_result.venv_python_exe, + ) + _add_provider_py_runtime_info(providers, runtime_details) + _add_provider_py_cc_link_params_info(providers, cc_details.cc_info_for_propagating) + py_info = _add_provider_py_info( + providers, + ctx = ctx, original_sources = direct_sources, required_py_files = required_py_files, required_pyc_files = required_pyc_files, implicit_pyc_files = implicit_pyc_files, implicit_pyc_source_files = implicit_pyc_source_files, - default_outputs = default_outputs.build(), - runtime_details = runtime_details, - cc_info = cc_details.cc_info_for_propagating, - inherited_environment = inherited_environment, - semantics = semantics, - output_groups = exec_result.output_groups, + imports = imports, ) + output_groups = dict(exec_result.output_groups) + if is_test: + _maybe_add_test_main_validation(ctx, main_py_source, output_groups) + _add_provider_output_group_info(providers, py_info, output_groups) + + return providers + +def _maybe_add_test_main_validation(ctx, main_py, output_groups): + """Adds a validation action that checks the test main actually runs tests. + + This is a safeguard against the common pitfall of defining test classes or + functions but forgetting to invoke a test runner, which causes the test to + silently pass without running anything. See the + `//python/config_settings:validate_test_main` flag. + + Args: + ctx: Rule ctx. + main_py: File or None; the main entry point source file. None when the + target uses `main_module` (which can't be statically analyzed here). + output_groups: dict[str, depset[File]]; mutated in place to add the + `_validation` output group when a validation action is created. + """ + if not ValidateTestMainFlag.is_enabled(ctx): + return + + # `main_module` targets execute a module by name; there's no single source + # file to statically analyze, so the check doesn't apply. + if main_py == None: + return + + exec_tools_toolchain = ctx.toolchains[EXEC_TOOLS_TOOLCHAIN_TYPE] + if exec_tools_toolchain == None or exec_tools_toolchain.exec_tools.exec_interpreter == None: + fail( + "Validating py_test main modules requires the exec tools toolchain " + + "with an exec interpreter, but none was found. Either register one " + + "or set --@rules_python//python/config_settings:validate_test_main=disabled.", + ) + + exec_tools = exec_tools_toolchain.exec_tools + validator = ctx.attr._validate_test_main + program_info = validator[PyInterpreterProgramInfo] + interpreter = exec_tools.exec_interpreter[DefaultInfo].files_to_run + validator_files_to_run = validator[DefaultInfo].files_to_run + + validation_output = ctx.actions.declare_file(ctx.label.name + "_validate_test_main.txt") + + args = ctx.actions.args() + args.add_all(program_info.interpreter_args) + args.add(validator_files_to_run.executable) + args.add("--src", main_py) + args.add("--src_name", main_py.short_path) + args.add("--label", str(ctx.label)) + args.add("--output", validation_output) + + execution_requirements = {} + if testing.ExecutionInfo in validator: + execution_requirements = validator[testing.ExecutionInfo].requirements + + ctx.actions.run( + executable = interpreter, + arguments = [args], + inputs = [main_py], + outputs = [validation_output], + tools = [validator_files_to_run], + mnemonic = "PyValidateTestMain", + progress_message = "Validating py_test main %{label}", + env = program_info.env | { + "PYTHONNOUSERSITE": "1", + "PYTHONSAFEPATH": "1", + }, + execution_requirements = execution_requirements, + toolchain = EXEC_TOOLS_TOOLCHAIN_TYPE, + ) + if "_validation" in output_groups: + output_groups["_validation"] = depset([validation_output], transitive = [output_groups["_validation"]]) + else: + output_groups["_validation"] = depset([validation_output]) def _get_build_info(ctx, cc_toolchain): build_info_files = py_internal.cc_toolchain_build_info_files(cc_toolchain) @@ -1105,14 +1399,14 @@ def _validate_executable(ctx): ).format(ctx.attr.main, ctx.attr.main_module)) def _declare_executable_file(ctx): - if target_platform_has_any_constraint(ctx, ctx.attr._windows_constraints): + if is_windows_platform(ctx): executable = ctx.actions.declare_file(ctx.label.name + ".exe") else: executable = ctx.actions.declare_file(ctx.label.name) return executable -def _get_runtime_details(ctx, semantics): +def _get_runtime_details(ctx): """Gets various information about the Python runtime to use. While most information comes from the toolchain, various legacy and @@ -1120,7 +1414,6 @@ def _get_runtime_details(ctx, semantics): Args: ctx: Rule ctx - semantics: A `BinarySemantics` struct; see `create_binary_semantics_struct` Returns: A struct; see inline-field comments of the return value for details. @@ -1128,19 +1421,13 @@ def _get_runtime_details(ctx, semantics): # Bazel has --python_path. This flag has a computed default of "python" when # its actual default is null (see - # BazelPythonConfiguration.java#getPythonPath). This flag is only used if - # toolchains are not enabled and `--python_top` isn't set. Note that Google - # used to have a variant of this named --python_binary, but it has since - # been removed. - # # TOOD(bazelbuild/bazel#7901): Remove this once --python_path flag is removed. flag_interpreter_path = read_possibly_native_flag(ctx, "python_path") - toolchain_runtime, effective_runtime = _maybe_get_runtime_from_ctx(ctx) - if not effective_runtime: - # Clear these just in case - toolchain_runtime = None - effective_runtime = None + if not flag_interpreter_path.startswith("python") and not paths.is_absolute(flag_interpreter_path): + fail("'python_path' must be an absolute path or a name to be resolved from the system PATH (e.g., 'python', 'python3').") + + effective_runtime = _maybe_get_runtime_from_ctx(ctx) if effective_runtime: direct = [] # List of files @@ -1158,23 +1445,16 @@ def _get_runtime_details(ctx, semantics): else: runtime_files = depset() - executable_interpreter_path = semantics.get_interpreter_path( + executable_interpreter_path = _get_interpreter_path( ctx, runtime = effective_runtime, flag_interpreter_path = flag_interpreter_path, ) return struct( - # Optional PyRuntimeInfo: The runtime found from toolchain resolution. - # This may be None because, within Google, toolchain resolution isn't - # yet enabled. - toolchain_runtime = toolchain_runtime, - # Optional PyRuntimeInfo: The runtime that should be used. When - # toolchain resolution is enabled, this is the same as - # `toolchain_resolution`. Otherwise, this probably came from the - # `_python_top` attribute that the Google implementation still uses. - # This is separate from `toolchain_runtime` because toolchain_runtime - # is propagated as a provider, while non-toolchain runtimes are not. + # Optional PyRuntimeInfo: The runtime that should be used. + # If None, it's probably Windows using the legacy auto-detecting toolchain + # that acts as if no toolchain was found. effective_runtime = effective_runtime, # str; Path to the Python interpreter to use for running the executable # itself (not the bootstrap script). Either an absolute path (which @@ -1191,7 +1471,7 @@ def _maybe_get_runtime_from_ctx(ctx): """Finds the PyRuntimeInfo from the toolchain or attribute, if available. Returns: - 2-tuple of toolchain_runtime, effective_runtime + A PyRuntimeInfo provider, or None. """ toolchain = ctx.toolchains[TOOLCHAIN_TYPE] @@ -1208,13 +1488,13 @@ def _maybe_get_runtime_from_ctx(ctx): # TODO(#7844): Remove this hack when the autodetecting toolchain has a # Windows implementation. if py3_runtime.interpreter_path == "/_magic_pyruntime_sentinel_do_not_use": - return None, None + return None if py3_runtime.python_version != "PY3": fail("Python toolchain py3_runtime must be python_version=PY3, got {}".format( py3_runtime.python_version, )) - return py3_runtime, py3_runtime + return py3_runtime def _get_base_runfiles_for_binary( ctx, @@ -1225,8 +1505,8 @@ def _get_base_runfiles_for_binary( required_pyc_files, implicit_pyc_files, implicit_pyc_source_files, - extra_common_runfiles, - semantics): + runtime_runfiles, + extra_common_runfiles): """Returns the set of runfiles necessary prior to executable creation. NOTE: The term "common runfiles" refers to the runfiles that are common to @@ -1246,9 +1526,9 @@ def _get_base_runfiles_for_binary( collection is enabled. implicit_pyc_source_files: `depset[File]` source files for implicit pyc files that are used when the implicit pyc files are not. + runtime_runfiles: runfiles for the python runtime. extra_common_runfiles: List of runfiles; additional runfiles that will be added to the common runfiles. - semantics: A `BinarySemantics` struct; see `create_binary_semantics_struct`. Returns: struct with attributes: @@ -1256,102 +1536,103 @@ def _get_base_runfiles_for_binary( * data_runfiles: The data runfiles * runfiles_without_exe: The default runfiles, but without the executable or files specific to the original program/executable. - * build_data_file: A file with build stamp information if stamping is enabled, otherwise - None. + * build_data_file: A file with build stamp information if stamping is + enabled, otherwise None. + * app_runfiles: Runfiles for user-space dependencies (doesn't + include the runtime or build data files) """ - common_runfiles = builders.RunfilesBuilder() - common_runfiles.files.add(required_py_files) - common_runfiles.files.add(required_pyc_files) + app_runfiles = builders.RunfilesBuilder() + app_runfiles.files.add(required_py_files) + app_runfiles.files.add(required_pyc_files) pyc_collection_enabled = PycCollectionAttr.is_pyc_collection_enabled(ctx) if pyc_collection_enabled: - common_runfiles.files.add(implicit_pyc_files) + app_runfiles.files.add(implicit_pyc_files) else: - common_runfiles.files.add(implicit_pyc_source_files) + app_runfiles.files.add(implicit_pyc_source_files) for dep in (ctx.attr.deps + extra_deps): if not (PyInfo in dep or (BuiltinPyInfo != None and BuiltinPyInfo in dep)): continue info = dep[PyInfo] if PyInfo in dep else dep[BuiltinPyInfo] - common_runfiles.files.add(info.transitive_sources) + app_runfiles.files.add(info.transitive_sources) # Everything past this won't work with BuiltinPyInfo if not hasattr(info, "transitive_pyc_files"): continue - common_runfiles.files.add(info.transitive_pyc_files) + app_runfiles.files.add(info.transitive_pyc_files) if pyc_collection_enabled: - common_runfiles.files.add(info.transitive_implicit_pyc_files) + app_runfiles.files.add(info.transitive_implicit_pyc_files) else: - common_runfiles.files.add(info.transitive_implicit_pyc_source_files) + app_runfiles.files.add(info.transitive_implicit_pyc_source_files) - common_runfiles.runfiles.append(collect_runfiles(ctx)) + app_runfiles.runfiles.append(collect_runfiles(ctx)) if extra_deps: - common_runfiles.add_targets(extra_deps) - common_runfiles.add(extra_common_runfiles) - - common_runfiles = common_runfiles.build(ctx) - - if semantics.should_create_init_files(ctx): - common_runfiles = _py_builtins.merge_runfiles_with_generated_inits_empty_files_supplier( + app_runfiles.add_targets(extra_deps) + app_runfiles.add(extra_common_runfiles) + + app_runfiles = app_runfiles.build(ctx) + + if _should_create_init_files(ctx): + # buildifier: disable=print + print( + """ +====================================================================== +WARNING: Target {} is using implicit __init__.py creation. + This diabolic behavior is deprecated and will be disabled by default in a + future release. + See https://github.com/bazel-contrib/rules_python/issues/2945 + + Ensure all __init__.py files are explicitly created and + added to the srcs or deps of your targets. + + Disable implicit creation by setting: + legacy_create_init = 0 + on the target, or globally by setting: + --incompatible_default_to_explicit_init_py +====================================================================== + """.rstrip().format(ctx.label), + ) + app_runfiles = _py_builtins.merge_runfiles_with_generated_inits_empty_files_supplier( ctx = ctx, - runfiles = common_runfiles, + runfiles = app_runfiles, ) - # Don't include build_data.txt in the non-exe runfiles. The build data - # may contain program-specific content (e.g. target name). - runfiles_with_exe = common_runfiles.merge(ctx.runfiles([executable])) + runfiles_without_exe = builders.RunfilesBuilder() + runfiles_without_exe.add(app_runfiles) + runfiles_without_exe.add(runtime_runfiles) + build_data_file = _write_build_data(ctx) + runfiles_without_exe.add(build_data_file) - # Don't include build_data.txt in data runfiles. This allows binaries to - # contain other binaries while still using the same fixed location symlink - # for the build_data.txt file. Really, the fixed location symlink should be - # removed and another way found to locate the underlying build data file. - data_runfiles = runfiles_with_exe + runfiles_without_exe = runfiles_without_exe.build(ctx) - if is_stamping_enabled(ctx, semantics) and semantics.should_include_build_data(ctx): - build_data_file, build_data_runfiles = _create_runfiles_with_build_data( - ctx, - semantics.get_central_uncachable_version_file(ctx), - semantics.get_extra_write_build_data_env(ctx), - ) - default_runfiles = runfiles_with_exe.merge(build_data_runfiles) - else: - build_data_file = None - default_runfiles = runfiles_with_exe + runfiles_with_exe = runfiles_without_exe.merge(ctx.runfiles([executable])) + # There are three types of runfiles: + # 1. app: Deps added by a user. This is akin to the typical files that would + # be in a traditional venv. No Python runtime files or build data files. + # 2. without-exe: (1) + build data + python runtime + # 3. binary (default/data runfiles): (2) + main executable return struct( - runfiles_without_exe = common_runfiles, - default_runfiles = default_runfiles, + app_runfiles = app_runfiles, build_data_file = build_data_file, - data_runfiles = data_runfiles, + data_runfiles = runfiles_with_exe, + default_runfiles = runfiles_with_exe, + runfiles_without_exe = runfiles_without_exe, ) -def _create_runfiles_with_build_data( - ctx, - central_uncachable_version_file, - extra_write_build_data_env): - build_data_file = _write_build_data( - ctx, - central_uncachable_version_file, - extra_write_build_data_env, - ) - build_data_runfiles = ctx.runfiles(files = [ - build_data_file, - ]) - return build_data_file, build_data_runfiles - -def _write_build_data(ctx, central_uncachable_version_file, extra_write_build_data_env): - # TODO: Remove this logic when a central file is always available - if not central_uncachable_version_file: - version_file = ctx.actions.declare_file(ctx.label.name + "-uncachable_version_file.txt") - _py_builtins.copy_without_caching( - ctx = ctx, - read_from = ctx.version_file, - write_to = version_file, - ) +def _write_build_data(ctx): + inputs = builders.DepsetBuilder() + if is_stamping_enabled(ctx): + # NOTE: ctx.info_file is undocumented; see + # https://github.com/bazelbuild/bazel/issues/9363 + info_file = ctx.info_file + version_file = ctx.files._uncachable_version_file[0] + inputs.add(info_file) + inputs.add(version_file) else: - version_file = central_uncachable_version_file - - direct_inputs = [ctx.info_file, version_file] + info_file = None + version_file = None # A "constant metadata" file is basically a special file that doesn't # support change detection logic and reports that it is unchanged. i.e., it @@ -1383,23 +1664,41 @@ def _write_build_data(ctx, central_uncachable_version_file, extra_write_build_da root = ctx.bin_dir, ) + action_args = ctx.actions.args() + writer_file = ctx.files._build_data_writer[0] + if writer_file.path.endswith(".ps1"): + # powershell.exe is used for broader compatibility + # It is installed by default on most Windows versions + action_exe = "powershell.exe" + action_args.add_all([ + # Bypass execution policy is needed because, + # by default, Windows blocks ps1 scripts. + "-ExecutionPolicy", + "Bypass", + "-File", + writer_file, + ]) + inputs.add(writer_file) + else: + action_exe = ctx.attr._build_data_writer[DefaultInfo].files_to_run + ctx.actions.run( - executable = ctx.executable._build_data_gen, - env = dicts.add({ - # NOTE: ctx.info_file is undocumented; see - # https://github.com/bazelbuild/bazel/issues/9363 - "INFO_FILE": ctx.info_file.path, + executable = action_exe, + arguments = [action_args], + env = { + "INFO_FILE": info_file.path if info_file else "", "OUTPUT": build_data.path, - "PLATFORM": cc_helper.find_cpp_toolchain(ctx).toolchain_id, + # Include this so it's explicit, otherwise, one has to detect + # this by looking for the absense of info_file keys. + "STAMPED": "TRUE" if is_stamping_enabled(ctx) else "FALSE", "TARGET": str(ctx.label), - "VERSION_FILE": version_file.path, - }, extra_write_build_data_env), - inputs = depset( - direct = direct_inputs, - ), + "VERSION_FILE": version_file.path if version_file else "", + }, + inputs = inputs.build(), outputs = [build_data], mnemonic = "PyWriteBuildData", - progress_message = "Generating %{label} build_data.txt", + progress_message = "Reticulating %{label} build data", + toolchain = None, ) return build_data @@ -1444,12 +1743,9 @@ def _get_native_deps_details(ctx, *, semantics, cc_details, is_test): feature_configuration = cc_feature_config.feature_configuration, cc_toolchain = cc_details.cc_toolchain, test_only_target = is_test, # private - stamp = 1 if is_stamping_enabled(ctx, semantics) else 0, + stamp = 1 if is_stamping_enabled(ctx) else 0, main_output = linked_lib, # private use_shareable_artifact_factory = True, # private - # NOTE: Only flags not captured by cc_info.linking_context need to - # be manually passed - user_link_flags = semantics.get_native_deps_user_link_flags(ctx), ) return struct( dso = dso, @@ -1589,15 +1885,17 @@ def _path_endswith(path, endswith): # "ab/c.py".endswith("b/c.py") from incorrectly matching. return ("/" + path).endswith("/" + endswith) -def is_stamping_enabled(ctx, semantics): +def is_stamping_enabled(ctx): """Tells if stamping is enabled or not. Args: ctx: The rule ctx - semantics: a semantics struct (see create_semantics_struct). Returns: bool; True if stamping is enabled, False if not. """ + + # Always ignore stamping for exec config. This mitigates stamping + # invalidating build action caching. if _is_tool_config(ctx): return False @@ -1607,7 +1905,9 @@ def is_stamping_enabled(ctx, semantics): elif stamp == 0: return False elif stamp == -1: - return semantics.get_stamp_flag(ctx) + # NOTE: ctx.configuration.stamp_binaries() exposes this, but that's + # a private API. To workaround, it'd been eposed via py_internal. + return py_internal.stamp_binaries(ctx) else: fail("Unsupported `stamp` value: {}".format(stamp)) @@ -1617,84 +1917,120 @@ def _is_tool_config(ctx): # a more public API. Until that's available, py_internal to the rescue. return py_internal.is_tool_configuration(ctx) -def _create_providers( - *, - ctx, - executable, - main_py, - original_sources, - required_py_files, - required_pyc_files, - implicit_pyc_files, - implicit_pyc_source_files, - default_outputs, - runfiles_details, - imports, - cc_info, - inherited_environment, - runtime_details, - output_groups, - semantics): - """Creates the providers an executable should return. +def _add_provider_default_info(providers, ctx, *, executable, default_outputs, runfiles_details): + """Adds the DefaultInfo provider. Args: + providers: list of providers to append to. ctx: The rule ctx. executable: File; the target's executable file. - main_py: File; the main .py entry point. - original_sources: `depset[File]` the direct `.py` sources for the - target that were the original input sources. - required_py_files: `depset[File]` the direct, `.py` sources for the - target that **must** be included by downstream targets. This should - only be Python source files. It should not include pyc files. - required_pyc_files: `depset[File]` the direct `.pyc` files this target - produces. - implicit_pyc_files: `depset[File]` pyc files that are only used if pyc - collection is enabled. - implicit_pyc_source_files: `depset[File]` source files for implicit pyc - files that are used when the implicit pyc files are not. default_outputs: depset of Files; the files for DefaultInfo.files - runfiles_details: runfiles that will become the default and data runfiles. - imports: depset of strings; the import paths to propagate - cc_info: optional CcInfo; Linking information to propagate as - PyCcLinkParamsInfo. Note that only the linking information - is propagated, not the whole CcInfo. + runfiles_details: runfiles that will become the default and data runfiles. + """ + providers.append(DefaultInfo( + executable = executable, + files = default_outputs, + default_runfiles = _py_builtins.make_runfiles_respect_legacy_external_runfiles( + ctx, + runfiles_details.default_runfiles, + ), + data_runfiles = _py_builtins.make_runfiles_respect_legacy_external_runfiles( + ctx, + runfiles_details.data_runfiles, + ), + )) + +def _add_provider_instrumented_files_info(providers, ctx): + """Adds the InstrumentedFilesInfo provider. + + Args: + providers: list of providers to append to. + ctx: The rule ctx. + """ + providers.append(create_instrumented_files_info(ctx)) + +def _add_provider_run_environment_info(providers, ctx, inherited_environment): + """Adds the RunEnvironmentInfo provider. + + Args: + providers: list of providers to append to. + ctx: The rule ctx. inherited_environment: list of strings; Environment variable names that should be inherited from the environment the executuble is run within. - runtime_details: struct of runtime information; see _get_runtime_details() - output_groups: dict[str, depset[File]]; used to create OutputGroupInfo - semantics: BinarySemantics struct; see create_binary_semantics() + """ + expanded_env = {} + for key, value in ctx.attr.env.items(): + expanded_env[key] = _py_builtins.expand_location_and_make_variables( + ctx = ctx, + attribute_name = "env[{}]".format(key), + expression = value, + targets = ctx.attr.data, + ) + if "PYTHONBREAKPOINT" not in inherited_environment: + inherited_environment = inherited_environment + ["PYTHONBREAKPOINT"] + providers.append(RunEnvironmentInfo( + environment = expanded_env, + inherited_environment = inherited_environment, + )) - Returns: - A list of modern providers. +def _add_provider_py_executable_info( + providers, + *, + app_runfiles, + build_data_file, + interpreter_args, + interpreter_path, + main_py, + runfiles_without_exe, + stage2_bootstrap, + venv_app_symlinks, + venv_interpreter_runfiles, + venv_interpreter_symlinks, + venv_python_exe): + """Adds the PyExecutableInfo provider. + + Args: + providers: list of providers to append to. + app_runfiles: runfiles; the runfiles for the application (deps, etc). + build_data_file: File; a file with build stamp information. + interpreter_args: list of strings; arguments to pass to the interpreter. + interpreter_path: str; path to the Python interpreter. + main_py: File; the main .py entry point. + runfiles_without_exe: runfiles; the default runfiles, but without the executable. + stage2_bootstrap: File; the stage 2 bootstrap script. + venv_app_symlinks: depset[ExplicitSymlink]; symlinks to create for the + venv that are the application (deps, etc). + venv_interpreter_runfiles: runfiles; runfiles specific to the interpreter for the venv. + venv_interpreter_symlinks: depset[ExplicitSymlink]; interpreter-specific symlinks to create for the venv. + venv_python_exe: File; the python executable in the venv. + """ + providers.append(PyExecutableInfo( + app_runfiles = app_runfiles, + build_data_file = build_data_file, + interpreter_args = interpreter_args, + interpreter_path = interpreter_path, + main = main_py, + runfiles_without_exe = runfiles_without_exe, + stage2_bootstrap = stage2_bootstrap, + venv_app_symlinks = venv_app_symlinks, + venv_interpreter_runfiles = venv_interpreter_runfiles, + venv_interpreter_symlinks = venv_interpreter_symlinks, + venv_python_exe = venv_python_exe, + )) + +def _add_provider_py_runtime_info(providers, runtime_details): + """Adds the PyRuntimeInfo provider. + + Args: + providers: list of providers to append to. + runtime_details: struct of runtime information; see _get_runtime_details() """ - providers = [ - DefaultInfo( - executable = executable, - files = default_outputs, - default_runfiles = _py_builtins.make_runfiles_respect_legacy_external_runfiles( - ctx, - runfiles_details.default_runfiles, - ), - data_runfiles = _py_builtins.make_runfiles_respect_legacy_external_runfiles( - ctx, - runfiles_details.data_runfiles, - ), - ), - create_instrumented_files_info(ctx), - _create_run_environment_info(ctx, inherited_environment), - PyExecutableInfo( - main = main_py, - runfiles_without_exe = runfiles_details.runfiles_without_exe, - build_data_file = runfiles_details.build_data_file, - interpreter_path = runtime_details.executable_interpreter_path, - ), - ] - # TODO(b/265840007): Make this non-conditional once Google enables - # --incompatible_use_python_toolchains. - if runtime_details.toolchain_runtime: - py_runtime_info = runtime_details.toolchain_runtime + # TODO - The effective runtime can be None for Windows + auto detecting toolchain. + # This can be removed once that's fixed; see maybe_get_runtime_from_ctx(). + if runtime_details.effective_runtime: + py_runtime_info = runtime_details.effective_runtime providers.append(py_runtime_info) # Re-add the builtin PyRuntimeInfo for compatibility to make @@ -1717,6 +2053,16 @@ def _create_providers( bootstrap_template = py_runtime_info.bootstrap_template, )) +def _add_provider_py_cc_link_params_info(providers, cc_info): + """Adds the PyCcLinkParamsInfo provider. + + Args: + providers: list of providers to append to. + cc_info: optional CcInfo; Linking information to propagate as + PyCcLinkParamsInfo. Note that only the linking information + is propagated, not the whole CcInfo. + """ + # TODO(b/163083591): Remove the PyCcLinkParamsInfo once binaries-in-deps # are cleaned up. if cc_info: @@ -1724,6 +2070,37 @@ def _create_providers( PyCcLinkParamsInfo(cc_info = cc_info), ) +def _add_provider_py_info( + providers, + *, + ctx, + original_sources, + required_py_files, + required_pyc_files, + implicit_pyc_files, + implicit_pyc_source_files, + imports): + """Adds the PyInfo provider. + + Args: + providers: list of providers to append to. + ctx: The rule ctx. + original_sources: `depset[File]` the direct `.py` sources for the + target that were the original input sources. + required_py_files: `depset[File]` the direct, `.py` sources for the + target that **must** be included by downstream targets. This should + only be Python source files. It should not include pyc files. + required_pyc_files: `depset[File]` the direct `.pyc` files this target + produces. + implicit_pyc_files: `depset[File]` pyc files that are only used if pyc + collection is enabled. + implicit_pyc_source_files: `depset[File]` source files for implicit pyc + files that are used when the implicit pyc files are not. + imports: depset of strings; the import paths to propagate + + Returns: + PyInfo. + """ py_info, builtin_py_info = create_py_info( ctx, original_sources = original_sources, @@ -1733,33 +2110,43 @@ def _create_providers( implicit_pyc_source_files = implicit_pyc_source_files, imports = imports, ) - providers.append(py_info) if builtin_py_info: providers.append(builtin_py_info) + return py_info + +def _add_provider_output_group_info(providers, py_info, output_groups): + """Adds the OutputGroupInfo provider. + + Args: + providers: list of providers to append to. + py_info: PyInfo; the PyInfo provider. + output_groups: dict[str, depset[File]]; used to create OutputGroupInfo + """ providers.append(create_output_group_info(py_info.transitive_sources, output_groups)) - extra_providers = semantics.get_extra_providers( - ctx, - main_py = main_py, - runtime_details = runtime_details, - ) - providers.extend(extra_providers) - return providers +def _add_config_setting_defaults(kwargs): + config_settings = kwargs.get("config_settings", None) + if config_settings == None: + config_settings = {} + + # NOTE: This code runs in loading phase within the context of the caller. + # Label() must be used to resolve repo names within rules_python's + # context to avoid unknown repo name errors. + default = select({ + labels.PLATFORMS_OS_WINDOWS: { + labels.ENABLE_RUNFILES: "true", + }, + "//conditions:default": {}, + }) -def _create_run_environment_info(ctx, inherited_environment): - expanded_env = {} - for key, value in ctx.attr.env.items(): - expanded_env[key] = _py_builtins.expand_location_and_make_variables( - ctx = ctx, - attribute_name = "env[{}]".format(key), - expression = value, - targets = ctx.attr.data, - ) - return RunEnvironmentInfo( - environment = expanded_env, - inherited_environment = inherited_environment, - ) + # Let user-provided settings have precedence + config_settings = default | config_settings + kwargs["config_settings"] = config_settings + +def common_executable_macro_kwargs_setup(kwargs): + convert_legacy_create_init_to_int(kwargs) + _add_config_setting_defaults(kwargs) def _transition_executable_impl(settings, attr): settings = dict(settings) @@ -1767,6 +2154,10 @@ def _transition_executable_impl(settings, attr): if attr.python_version and attr.python_version not in ("PY2", "PY3"): settings[labels.PYTHON_VERSION] = attr.python_version + + if attr.stamp != -1: + settings["//command_line_option:stamp"] = str(attr.stamp) + return settings def create_executable_rule(*, attrs, **kwargs): @@ -1801,7 +2192,7 @@ def create_executable_rule_builder(implementation, **kwargs): ::: Returns: - {type}`ruleb.Rule` with the necessary settings + {obj}`ruleb.Rule` with the necessary settings for creating an executable Python rule. """ builder = ruleb.Rule( @@ -1814,11 +2205,21 @@ def create_executable_rule_builder(implementation, **kwargs): ruleb.ToolchainType(TOOLCHAIN_TYPE), ruleb.ToolchainType(EXEC_TOOLS_TOOLCHAIN_TYPE, mandatory = False), ruleb.ToolchainType("@bazel_tools//tools/cpp:toolchain_type", mandatory = False), - ], + ] + ([ruleb.ToolchainType(LAUNCHER_MAKER_TOOLCHAIN_TYPE)] if rp_config.bazel_9_or_later else []), cfg = dict( implementation = _transition_executable_impl, - inputs = TRANSITION_LABELS + [labels.PYTHON_VERSION], - outputs = TRANSITION_LABELS + [labels.PYTHON_VERSION], + inputs = TRANSITION_LABELS + [ + labels.PYTHON_VERSION, + "//command_line_option:stamp", + "//command_line_option:build_runfile_links", + "//command_line_option:enable_runfiles", + ], + outputs = TRANSITION_LABELS + [ + labels.PYTHON_VERSION, + "//command_line_option:stamp", + "//command_line_option:build_runfile_links", + "//command_line_option:enable_runfiles", + ], ), **kwargs ) @@ -1859,7 +2260,3 @@ def cc_configure_features( feature_configuration = feature_configuration, requested_features = requested_features, ) - -only_exposed_for_google_internal_reason = struct( - create_runfiles_with_build_data = _create_runfiles_with_build_data, -) diff --git a/python/private/py_executable_info.bzl b/python/private/py_executable_info.bzl index deb119428d..7693fa950f 100644 --- a/python/private/py_executable_info.bzl +++ b/python/private/py_executable_info.bzl @@ -10,10 +10,36 @@ This provider is for executable-specific information (e.g. tests and binaries). ::: """, fields = { + "app_runfiles": """ +:type: runfiles + +The runfiles for the executable's "user" dependencies. These are things in e.g. +`deps` (or similar), but doesn't include "external" or "implicit" pieces, +e.g. the Python runtime itself. It's roughly akin to the files a traditional +venv would have installed into it. + +:::{seealso} +{obj}`PyRuntimeInfo` for the Python runtime files. The {obj}`py_binary` et al +rules provide it directly so that the runtime the binary original chose +can be accessed. +::: + +:::{versionadded} 1.9.0 +::: +""", "build_data_file": """ :type: None | File A symlink to build_data.txt if stamping is enabled, otherwise None. +""", + "interpreter_args": """ +:type: list[str] + +Args that should be passed to the interpreter before regular args +(e.g. `-X whatever`). + +:::{versionadded} 1.9.0 +::: """, "interpreter_path": """ :type: None | str @@ -28,6 +54,12 @@ should be within `runtime_files`) The user-level entry point file. Usually a `.py` file, but may also be `.pyc` file if precompiling is enabled. + +:::{seealso} + +The {obj}`stage2_bootstrap` attribute, which bootstraps an executable to run +the user main file. +::: """, "runfiles_without_exe": """ :type: runfiles @@ -35,6 +67,58 @@ file if precompiling is enabled. The runfiles the program needs, but without the original executable, files only added to support the original executable, or files specific to the original program. +""", + "stage2_bootstrap": """ +:type: File | None + +The Bazel-executable-level entry point to the program, which handles Bazel-specific +setup before running the file in {obj}`main`. May be None if a two-stage bootstrap +implementation isn't being used. + +:::{versionadded} 1.9.0 +::: +""", + "venv_app_symlinks": """ +:type: depset[ExplicitSymlink] | None + +Symlinks that are specific to the application within the venv (e.g. +dependencies). + +Only used with Windows for files that would have used `declare_symlink()` +to create relative symlinks. These may overlap with paths in runfiles; it's +up to the consumer to determine how to handle such overlaps. + +:::{versionadded} 2.1.0 +::: +""", + "venv_interpreter_runfiles": """ +:type: runfiles | None + +Runfiles that are specific to the interpreter within the venv. + +:::{versionadded} 2.0.0 +::: +""", + "venv_interpreter_symlinks": """ +:type: depset[ExplicitSymlink] | None + +Symlinks that are specific to the interpreter within the venv. + +Only used with Windows for files that would have used `declare_symlink()` +to create relative symlinks. These may overlap with paths in runfiles; it's +up to the consumer to determine how to handle such overlaps. + +:::{versionadded} 2.0.0 +::: +""", + "venv_python_exe": """ +:type: File | None + +The `bin/python3` file within the venv this binary uses. May be None if venv +mode is not enabled. + +:::{versionadded} 1.9.0 +::: """, }, ) diff --git a/python/private/py_info.bzl b/python/private/py_info.bzl index f96dec554b..dac1ddeff3 100644 --- a/python/private/py_info.bzl +++ b/python/private/py_info.bzl @@ -13,10 +13,8 @@ # limitations under the License. """Implementation of PyInfo provider and PyInfo-specific utilities.""" -load("@rules_python_internal//:rules_python_config.bzl", "config") load(":builders.bzl", "builders") load(":reexports.bzl", "BuiltinPyInfo") -load(":util.bzl", "define_bazel_6_provider") def _VenvSymlinkKind_typedef(): """An enum of types of venv directories. @@ -39,6 +37,12 @@ def _VenvSymlinkKind_typedef(): Indicates to create paths under the venv's include directory. ::: + + :::{field} DATA + :type: object + + Indicates to create paths under the venv's data directory. + ::: """ # buildifier: disable=name-conventions @@ -47,14 +51,20 @@ VenvSymlinkKind = struct( BIN = "BIN", LIB = "LIB", INCLUDE = "INCLUDE", + DATA = "DATA", ) +def _VenvSymlinkEntry_init(**kwargs): + kwargs.setdefault("link_to_file", None) + return kwargs + # A provider is used for memory efficiency. # buildifier: disable=name-conventions -VenvSymlinkEntry = provider( +VenvSymlinkEntry, _ = provider( doc = """ An entry in `PyInfo.venv_symlinks` """, + init = _VenvSymlinkEntry_init, fields = { "files": """ :type: depset[File] @@ -69,12 +79,21 @@ if one adds files to `venv_path=a/` and another adds files to `venv_path=a/b/`. One of the {obj}`VenvSymlinkKind` values. It represents which directory within the venv to create the path under. +""", + "link_to_file": """ +:type: File | None + +A file that `venv_path` should point to. The file to link to should also be in +`files`. + +:::{versionadded} 1.7.0 +::: """, "link_to_path": """ :type: str | None -A runfiles-root relative path that `venv_path` will symlink to. If `None`, -it means to not create a symlink. +A runfiles-root relative path that `venv_path` will symlink to (if +`link_to_file` is `None`). If `None`, it means to not create it in the venv. """, "package": """ :type: str | None @@ -97,6 +116,240 @@ PEP440 standard. }, ) +def _VenvSymlinkEntryBuilder_typedef(): + """Builder for VenvSymlinkEntry. + + To create an instance, use {obj}`PyInfoBuilder.add_venv_symlink()`. + + :::{field} files + :type: DepsetBuilder[File] + ::: + + :::{versionadded} 2.2.0 + ::: + """ + +def _VenvSymlinkEntryBuilder(): + # buildifier: disable=uninitialized + self = struct( + _state = { + "kind": None, + "link_to_file": None, + "link_to_path": None, + "package": None, + "venv_path": None, + "version": None, + }, + files = builders.DepsetBuilder(), + get_kind = lambda: _VenvSymlinkEntryBuilder_get_kind(self), + set_kind = lambda k: _VenvSymlinkEntryBuilder_set_kind(self, k), + get_link_to_file = lambda: _VenvSymlinkEntryBuilder_get_link_to_file(self), + set_link_to_file = lambda f: _VenvSymlinkEntryBuilder_set_link_to_file(self, f), + get_link_to_path = lambda: _VenvSymlinkEntryBuilder_get_link_to_path(self), + set_link_to_path = lambda p: _VenvSymlinkEntryBuilder_set_link_to_path(self, p), + get_package = lambda: _VenvSymlinkEntryBuilder_get_package(self), + set_package = lambda p: _VenvSymlinkEntryBuilder_set_package(self, p), + get_venv_path = lambda: _VenvSymlinkEntryBuilder_get_venv_path(self), + set_venv_path = lambda p: _VenvSymlinkEntryBuilder_set_venv_path(self, p), + get_version = lambda: _VenvSymlinkEntryBuilder_get_version(self), + set_version = lambda v: _VenvSymlinkEntryBuilder_set_version(self, v), + build = lambda: _VenvSymlinkEntryBuilder_build(self), + ) + return self + +def _VenvSymlinkEntryBuilder_get_kind(self): + """Get the kind of the symlink. + + Args: + self: implicitly added. + + Returns: + {type}`string` One of the {obj}`VenvSymlinkKind` values. + """ + return self._state["kind"] + +def _VenvSymlinkEntryBuilder_get_link_to_file(self): + """Get the file that the symlink points to. + + Args: + self: implicitly added. + + Returns: + {type}`File | None` + """ + return self._state["link_to_file"] + +def _VenvSymlinkEntryBuilder_get_link_to_path(self): + """Get the runfiles-root relative path that the symlink points to. + + Args: + self: implicitly added. + + Returns: + {type}`string | None` + """ + return self._state["link_to_path"] + +def _VenvSymlinkEntryBuilder_get_package(self): + """Get the PyPI package name that the code originates from. + + Args: + self: implicitly added. + + Returns: + {type}`string | None` + """ + return self._state["package"] + +def _VenvSymlinkEntryBuilder_get_venv_path(self): + """Get the path relative to the kind directory within the venv. + + Args: + self: implicitly added. + + Returns: + {type}`string` + """ + return self._state["venv_path"] + +def _VenvSymlinkEntryBuilder_get_version(self): + """Get the PyPI package version that the code originates from. + + Args: + self: implicitly added. + + Returns: + {type}`string | None` + """ + return self._state["version"] + +def _VenvSymlinkEntryBuilder_set_kind(self, kind): + """Set the kind of the symlink. + + Args: + self: implicitly added. + kind: {type}`string` One of the {obj}`VenvSymlinkKind` values. + + Returns: + {type}`VenvSymlinkEntryBuilder` self. + """ + _check_arg_type("kind", "string", kind) + self._state["kind"] = kind + return self + +def _VenvSymlinkEntryBuilder_set_link_to_file(self, link_to_file): + """Set the file that the symlink points to. + + Args: + self: implicitly added. + link_to_file: {type}`File | None` + + Returns: + {type}`VenvSymlinkEntryBuilder` self. + """ + if link_to_file != None: + _check_arg_type("link_to_file", "File", link_to_file) + self._state["link_to_file"] = link_to_file + return self + +def _VenvSymlinkEntryBuilder_set_link_to_path(self, link_to_path): + """Set the runfiles-root relative path that the symlink points to. + + Args: + self: implicitly added. + link_to_path: {type}`string | None` + + Returns: + {type}`VenvSymlinkEntryBuilder` self. + """ + if link_to_path != None: + _check_arg_type("link_to_path", "string", link_to_path) + self._state["link_to_path"] = link_to_path + return self + +def _VenvSymlinkEntryBuilder_set_package(self, package): + """Set the PyPI package name that the code originates from. + + Args: + self: implicitly added. + package: {type}`string | None` + + Returns: + {type}`VenvSymlinkEntryBuilder` self. + """ + if package != None: + _check_arg_type("package", "string", package) + self._state["package"] = package + return self + +def _VenvSymlinkEntryBuilder_set_venv_path(self, venv_path): + """Set the path relative to the kind directory within the venv. + + Args: + self: implicitly added. + venv_path: {type}`string` + + Returns: + {type}`VenvSymlinkEntryBuilder` self. + """ + _check_arg_type("venv_path", "string", venv_path) + self._state["venv_path"] = venv_path + return self + +def _VenvSymlinkEntryBuilder_set_version(self, version): + """Set the PyPI package version that the code originates from. + + Args: + self: implicitly added. + version: {type}`string | None` + + Returns: + {type}`VenvSymlinkEntryBuilder` self. + """ + if version != None: + _check_arg_type("version", "string", version) + self._state["version"] = version + return self + +def _VenvSymlinkEntryBuilder_build(self): + """Builds into a {obj}`VenvSymlinkEntry` object. + + Args: + self: implicitly added. + + Returns: + {type}`VenvSymlinkEntry` + """ + if not self._state["venv_path"]: + fail("venv_path must be set") + return VenvSymlinkEntry( + files = self.files.build(), + kind = self._state["kind"], + link_to_file = self._state["link_to_file"], + link_to_path = self._state["link_to_path"], + package = self._state["package"], + venv_path = self._state["venv_path"], + version = self._state["version"], + ) + +# buildifier: disable=name-conventions +VenvSymlinkEntryBuilder = struct( + TYPEDEF = _VenvSymlinkEntryBuilder_typedef, + build = _VenvSymlinkEntryBuilder_build, + get_kind = _VenvSymlinkEntryBuilder_get_kind, + set_kind = _VenvSymlinkEntryBuilder_set_kind, + get_link_to_file = _VenvSymlinkEntryBuilder_get_link_to_file, + set_link_to_file = _VenvSymlinkEntryBuilder_set_link_to_file, + get_link_to_path = _VenvSymlinkEntryBuilder_get_link_to_path, + set_link_to_path = _VenvSymlinkEntryBuilder_set_link_to_path, + get_package = _VenvSymlinkEntryBuilder_get_package, + set_package = _VenvSymlinkEntryBuilder_set_package, + get_venv_path = _VenvSymlinkEntryBuilder_get_venv_path, + set_venv_path = _VenvSymlinkEntryBuilder_set_venv_path, + get_version = _VenvSymlinkEntryBuilder_get_version, + set_version = _VenvSymlinkEntryBuilder_set_version, +) + def _check_arg_type(name, required_type, value): """Check that a value is of an expected type.""" value_type = type(value) @@ -148,7 +401,7 @@ def _PyInfo_init( "direct_pyc_files": direct_pyc_files, "direct_pyi_files": direct_pyi_files, "has_py2_only_sources": has_py2_only_sources, - "has_py3_only_sources": has_py2_only_sources, + "has_py3_only_sources": has_py3_only_sources, "imports": imports, "transitive_implicit_pyc_files": transitive_implicit_pyc_files, "transitive_implicit_pyc_source_files": transitive_implicit_pyc_source_files, @@ -160,7 +413,7 @@ def _PyInfo_init( "venv_symlinks": venv_symlinks, } -PyInfo, _unused_raw_py_info_ctor = define_bazel_6_provider( +PyInfo, _unused_raw_py_info_ctor = provider( doc = """Encapsulates information provided by the Python rules. Instead of creating this object directly, use {obj}`PyInfoBuilder` and @@ -327,7 +580,7 @@ This field is currently unused in Bazel and may go away in the future. ) # The "effective" PyInfo is what the canonical //python:py_info.bzl%PyInfo symbol refers to -_EffectivePyInfo = PyInfo if (config.enable_pystar or BuiltinPyInfo == None) else BuiltinPyInfo +_EffectivePyInfo = PyInfo def _PyInfoBuilder_typedef(): """Builder for PyInfo. @@ -390,6 +643,8 @@ def _PyInfoBuilder_new(): _has_py2_only_sources = [False], _has_py3_only_sources = [False], _uses_shared_libraries = [False], + _venv_symlink_builders = [], + add_venv_symlink = lambda *a, **k: _PyInfoBuilder_add_venv_symlink(self, *a, **k), build = lambda *a, **k: _PyInfoBuilder_build(self, *a, **k), build_builtin_py_info = lambda *a, **k: _PyInfoBuilder_build_builtin_py_info(self, *a, **k), direct_original_sources = builders.DepsetBuilder(), @@ -419,6 +674,22 @@ def _PyInfoBuilder_new(): ) return self +def _PyInfoBuilder_add_venv_symlink(self): + """Create and return a new VenvSymlinkEntryBuilder. + + :::{versionadded} 2.2.0 + ::: + + Args: + self: implicitly added. + + Returns: + {type}`VenvSymlinkEntryBuilder` + """ + entry_builder = _VenvSymlinkEntryBuilder() + self._venv_symlink_builders.append(entry_builder) + return entry_builder + def _PyInfoBuilder_get_has_py3_only_sources(self): """Get the `has_py3_only_sources` value. @@ -630,20 +901,10 @@ def _PyInfoBuilder_build(self): Returns: {type}`PyInfo` """ - if config.enable_pystar: - kwargs = dict( - direct_original_sources = self.direct_original_sources.build(), - direct_pyc_files = self.direct_pyc_files.build(), - direct_pyi_files = self.direct_pyi_files.build(), - transitive_implicit_pyc_files = self.transitive_implicit_pyc_files.build(), - transitive_implicit_pyc_source_files = self.transitive_implicit_pyc_source_files.build(), - transitive_original_sources = self.transitive_original_sources.build(), - transitive_pyc_files = self.transitive_pyc_files.build(), - transitive_pyi_files = self.transitive_pyi_files.build(), - venv_symlinks = self.venv_symlinks.build(), - ) - else: - kwargs = {} + venv_symlinks = depset( + direct = [b.build() for b in self._venv_symlink_builders], + transitive = [self.venv_symlinks.build()], + ) return _EffectivePyInfo( has_py2_only_sources = self._has_py2_only_sources[0], @@ -651,7 +912,15 @@ def _PyInfoBuilder_build(self): imports = self.imports.build(), transitive_sources = self.transitive_sources.build(), uses_shared_libraries = self._uses_shared_libraries[0], - **kwargs + direct_original_sources = self.direct_original_sources.build(), + direct_pyc_files = self.direct_pyc_files.build(), + direct_pyi_files = self.direct_pyi_files.build(), + transitive_implicit_pyc_files = self.transitive_implicit_pyc_files.build(), + transitive_implicit_pyc_source_files = self.transitive_implicit_pyc_source_files.build(), + transitive_original_sources = self.transitive_original_sources.build(), + transitive_pyc_files = self.transitive_pyc_files.build(), + transitive_pyi_files = self.transitive_pyi_files.build(), + venv_symlinks = venv_symlinks, ) def _PyInfoBuilder_build_builtin_py_info(self): @@ -680,6 +949,7 @@ def _PyInfoBuilder_build_builtin_py_info(self): PyInfoBuilder = struct( TYPEDEF = _PyInfoBuilder_typedef, new = _PyInfoBuilder_new, + add_venv_symlink = _PyInfoBuilder_add_venv_symlink, build = _PyInfoBuilder_build, build_builtin_py_info = _PyInfoBuilder_build_builtin_py_info, get_has_py2_only_sources = _PyInfoBuilder_get_has_py2_only_sources, diff --git a/python/private/py_internal.bzl b/python/private/py_internal.bzl index 429637253f..000501d26c 100644 --- a/python/private/py_internal.bzl +++ b/python/private/py_internal.bzl @@ -18,9 +18,10 @@ Re-exports the restricted-use py_internal helper under its original name. These may change at any time and are closely coupled to the rule implementation. """ -# The py_internal global is only available in Bazel 7+, so loading of it -# must go through a repo rule with Bazel version detection logic. -load("@rules_python_internal//:py_internal.bzl", "py_internal_impl") +# The native `py_internal` object is only visible to Starlark files under +# `tools/build_defs/python`. To access it from `//python/private`, we use an +# indirection through `//tools/build_defs/python/private/py_internal_renamed.bzl`, +# which re-exports it under a different name. +load("//tools/build_defs/python/private:py_internal_renamed.bzl", "py_internal_renamed") # buildifier: disable=bzl-visibility -# NOTE: This is None prior to Bazel 7, as set by @rules_python_internal -py_internal = py_internal_impl +py_internal = py_internal_renamed diff --git a/python/private/py_interpreter_program.bzl b/python/private/py_interpreter_program.bzl index cd62a7190d..1f1abb1bfe 100644 --- a/python/private/py_interpreter_program.bzl +++ b/python/private/py_interpreter_program.bzl @@ -15,6 +15,7 @@ """Internal only bootstrap level binary-like rule.""" load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") +load("//python/private:sentinel_impl.bzl", "SentinelInfo") PyInterpreterProgramInfo = provider( doc = "Information about how to run a program with an external interpreter.", @@ -28,14 +29,20 @@ PyInterpreterProgramInfo = provider( def _py_interpreter_program_impl(ctx): # Bazel requires the executable file to be an output created by this target. - executable = ctx.actions.declare_file(ctx.label.name) + # To avoid colliding with the source file (e.g. target=foo, main=foo.py), + # we append an underscore to the name, but keep the extension so that + # the original extension is preserved. + extension = ctx.file.main.extension + executable_name = "{}_.{}".format(ctx.label.name, extension) + executable = ctx.actions.declare_file(executable_name) ctx.actions.symlink(output = executable, target_file = ctx.file.main) execution_requirements = {} - execution_requirements.update([ - value.split("=", 1) - for value in ctx.attr.execution_requirements[BuildSettingInfo].value - if value.strip() - ]) + if BuildSettingInfo in ctx.attr.execution_requirements: + execution_requirements.update([ + value.split("=", 1) + for value in ctx.attr.execution_requirements[BuildSettingInfo].value + if value.strip() + ]) return [ DefaultInfo( @@ -85,8 +92,9 @@ ctx.actions.run( doc = "Environment variables that should set prior to running.", ), "execution_requirements": attr.label( + default = "//python:none", doc = "Execution requirements to set when running it as an action", - providers = [BuildSettingInfo], + providers = [[BuildSettingInfo], [SentinelInfo]], ), "interpreter_args": attr.string_list( doc = "Args that should be passed to the interpreter.", diff --git a/python/private/py_library.bzl b/python/private/py_library.bzl index b2a9fdd3be..3d8ba4aa71 100644 --- a/python/private/py_library.bzl +++ b/python/private/py_library.bzl @@ -32,11 +32,9 @@ load( "collect_imports", "collect_runfiles", "create_instrumented_files_info", - "create_library_semantics_struct", "create_output_group_info", "create_py_info", "filter_to_py_srcs", - "get_imports", ) load(":common_labels.bzl", "labels") load(":flags.bzl", "AddSrcsToRunfilesFlag", "PrecompileFlag", "VenvsSitePackages") @@ -99,6 +97,19 @@ The topological order has been removed and if 2 different versions of the same P package are observed, the behaviour has no guarantees except that it is deterministic and that only one package version will be included. ::: +""", + ), + "namespace_package_files": lambda: attrb.LabelList( + allow_empty = True, + allow_files = True, + doc = """ +Files whose directories are namespace packages. + +When {obj}`--venvs_site_packages=yes` is set, this helps inform which directories should be +treated as namespace packages and expect files from other targets to be contributed. +This allows optimizing the generation of symlinks to be cheaper at analysis time. +:::{versionadded} 1.8.0 +::: """, ), "_add_srcs_to_runfiles_flag": lambda: attrb.Label( @@ -107,29 +118,18 @@ and that only one package version will be included. }, ) -def _py_library_impl_with_semantics(ctx): - return py_library_impl( - ctx, - semantics = create_library_semantics_struct( - get_imports = get_imports, - maybe_precompile = maybe_precompile, - get_cc_info_for_library = collect_cc_info, - ), - ) - -def py_library_impl(ctx, *, semantics): +def py_library_impl(ctx): """Abstract implementation of py_library rule. Args: ctx: The rule ctx - semantics: A `LibrarySemantics` struct; see `create_library_semantics_struct` Returns: A list of modern providers to propagate. """ direct_sources = filter_to_py_srcs(ctx.files.srcs) - precompile_result = semantics.maybe_precompile(ctx, direct_sources) + precompile_result = maybe_precompile(ctx, direct_sources) required_py_files = precompile_result.keep_srcs required_pyc_files = [] @@ -158,9 +158,9 @@ def py_library_impl(ctx, *, semantics): imports = [] venv_symlinks = [] - imports, venv_symlinks = _get_imports_and_venv_symlinks(ctx, semantics) + imports, venv_symlinks = _get_imports_and_venv_symlinks(ctx) - cc_info = semantics.get_cc_info_for_library(ctx) + cc_info = collect_cc_info(ctx) py_info, builtins_py_info = create_py_info( ctx, original_sources = direct_sources, @@ -229,7 +229,7 @@ def _get_package_and_version(ctx): version.normalize(version_str), # will have no dashes either ) -def _get_imports_and_venv_symlinks(ctx, semantics): +def _get_imports_and_venv_symlinks(ctx): imports = depset() venv_symlinks = [] if VenvsSitePackages.is_enabled(ctx): @@ -245,15 +245,28 @@ def _get_imports_and_venv_symlinks(ctx, semantics): fail("When venvs_site_packages is enabled, exactly one `imports` " + "value must be specified, got {}".format(imports)) + site_packages_root = paths.normalize(paths.join( + ctx.label.package, + imports[0], + )) + + # Prevent escaping out of the repo root. + if site_packages_root.startswith("../") or site_packages_root == "..": + fail(("Invalid `imports` value '{}': resolves to '{}' which is " + + "above the repo root").format( + imports[0], + site_packages_root, + )) venv_symlinks = get_venv_symlinks( ctx, ctx.files.srcs + ctx.files.data + ctx.files.pyi_srcs, package, version_str, - site_packages_root = imports[0], + site_packages_root = site_packages_root, + namespace_package_files = ctx.files.namespace_package_files, ) else: - imports = collect_imports(ctx, semantics) + imports = collect_imports(ctx) return imports, venv_symlinks _MaybeBuiltinPyInfo = [BuiltinPyInfo] if BuiltinPyInfo != None else [] @@ -269,11 +282,11 @@ def create_py_library_rule_builder(): ::: Returns: - {type}`ruleb.Rule` with the necessary settings + {obj}`ruleb.Rule` with the necessary settings for creating a `py_library` rule. """ builder = ruleb.Rule( - implementation = _py_library_impl_with_semantics, + implementation = py_library_impl, doc = _DEFAULT_PY_LIBRARY_DOC, exec_groups = dict(REQUIRED_EXEC_GROUP_BUILDERS), attrs = LIBRARY_ATTRS, diff --git a/python/private/py_package.bzl b/python/private/py_package.bzl index adf2b6deef..d23276a53b 100644 --- a/python/private/py_package.bzl +++ b/python/private/py_package.bzl @@ -41,13 +41,8 @@ def _py_package_impl(ctx): py_info.merge_target(dep) py_info = py_info.build() inputs.add(py_info.transitive_sources) - - # Remove conditional once Bazel 6 support dropped. - if hasattr(py_info, "transitive_pyc_files"): - inputs.add(py_info.transitive_pyc_files) - - if hasattr(py_info, "transitive_pyi_files"): - inputs.add(py_info.transitive_pyi_files) + inputs.add(py_info.transitive_pyc_files) + inputs.add(py_info.transitive_pyi_files) inputs = inputs.build() diff --git a/python/private/py_repositories.bzl b/python/private/py_repositories.bzl index 3ad2a97214..3489613a6c 100644 --- a/python/private/py_repositories.bzl +++ b/python/private/py_repositories.bzl @@ -60,10 +60,10 @@ def py_repositories(transition_settings = []): ) http_archive( name = "bazel_skylib", - sha256 = "d00f1389ee20b60018e92644e0948e16e350a7707219e7a390fb0a99b6ec9262", + sha256 = "6e78f0e57de26801f6f564fa7c4a48dc8b36873e416257a92bbb0937eeac8446", urls = [ - "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.7.0/bazel-skylib-1.7.0.tar.gz", - "https://github.com/bazelbuild/bazel-skylib/releases/download/1.7.0/bazel-skylib-1.7.0.tar.gz", + "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.8.2/bazel-skylib-1.8.2.tar.gz", + "https://github.com/bazelbuild/bazel-skylib/releases/download/1.8.2/bazel-skylib-1.8.2.tar.gz", ], ) http_archive( @@ -72,6 +72,18 @@ def py_repositories(transition_settings = []): strip_prefix = "rules_cc-0.1.5", urls = ["https://github.com/bazelbuild/rules_cc/releases/download/0.1.5/rules_cc-0.1.5.tar.gz"], ) + http_archive( + name = "toml.bzl", + sha256 = "63633c762bdb4d836add5a9a81deeeae93c4b2edbd62ac032733020b5652b90a", + strip_prefix = "toml.bzl-0.4.0", + url = "https://github.com/jvolkman/toml.bzl/releases/download/v0.4.0/toml.bzl-v0.4.0.tar.gz", + ) + http_archive( + name = "package_metadata", + sha256 = "8f27dc7393e3f3bdc793bdc4ba36d67a63c22cc9d38cc65d3204654974ea4563", + strip_prefix = "supply-chain-0.0.7/metadata", + url = "https://github.com/bazel-contrib/supply-chain/releases/download/v0.0.7/supply-chain-v0.0.7.tar.gz", + ) # Needed by rules_cc, triggered by @rules_java_prebuilt in Bazel by using @rules_cc//cc:defs.bzl # NOTE: This name must be com_google_protobuf until Bazel drops WORKSPACE diff --git a/python/private/py_runtime_info.bzl b/python/private/py_runtime_info.bzl index efe14b2c06..daf01e6c7e 100644 --- a/python/private/py_runtime_info.bzl +++ b/python/private/py_runtime_info.bzl @@ -13,8 +13,6 @@ # limitations under the License. """Providers for Python rules.""" -load(":util.bzl", "define_bazel_6_provider") - DEFAULT_STUB_SHEBANG = "#!/usr/bin/env python3" _PYTHON_VERSION_VALUES = ["PY2", "PY3"] @@ -57,6 +55,7 @@ def _PyRuntimeInfo_init( interpreter_path = None, interpreter = None, files = None, + interpreter_files_to_run = None, coverage_tool = None, coverage_files = None, pyc_tag = None, @@ -68,13 +67,23 @@ def _PyRuntimeInfo_init( zip_main_template = None, abi_flags = "", site_init_template = None, - supports_build_time_venv = True): + supports_build_time_venv = True, + venv_bin_files = None): if (interpreter_path and interpreter) or (not interpreter_path and not interpreter): fail("exactly one of interpreter or interpreter_path must be specified") if interpreter_path and files != None: fail("cannot specify 'files' if 'interpreter_path' is given") + if interpreter_path and interpreter_files_to_run: + fail("cannot specify 'interpreter_files_to_run' if 'interpreter_path' is given") + + if interpreter_files_to_run: + if not interpreter_files_to_run.executable: + fail("'interpreter_files_to_run' must have an executable") + if interpreter_files_to_run.executable != interpreter: + fail("'interpreter_files_to_run.executable' must match 'interpreter'") + if (coverage_tool and not coverage_files) or (not coverage_tool and coverage_files): fail( "coverage_tool and coverage_files must both be set or neither must be set, " + @@ -113,6 +122,7 @@ def _PyRuntimeInfo_init( "files": files, "implementation_name": implementation_name, "interpreter": interpreter, + "interpreter_files_to_run": interpreter_files_to_run, "interpreter_path": interpreter_path, "interpreter_version_info": interpreter_version_info_struct_from_dict(interpreter_version_info), "pyc_tag": pyc_tag, @@ -121,10 +131,11 @@ def _PyRuntimeInfo_init( "stage2_bootstrap_template": stage2_bootstrap_template, "stub_shebang": stub_shebang, "supports_build_time_venv": supports_build_time_venv, + "venv_bin_files": venv_bin_files, "zip_main_template": zip_main_template, } -PyRuntimeInfo, _unused_raw_py_runtime_info_ctor = define_bazel_6_provider( +PyRuntimeInfo, _unused_raw_py_runtime_info_ctor = provider( doc = """Contains information about a Python runtime, as returned by the `py_runtime` rule. @@ -239,6 +250,19 @@ The Python implementation name (`sys.implementation.name`) If this is an in-build runtime, this field is a `File` representing the interpreter. Otherwise, this is `None`. Note that an in-build runtime can use either a prebuilt, checked-in interpreter or an interpreter built from source. +""", + "interpreter_files_to_run": """ +:type: None | FilesToRunProvider + +The `FilesToRunProvider` for the interpreter target when this runtime was +created from an executable target. This includes the interpreter executable and +the runfiles metadata needed to use it as an action tool. Rules that execute the +interpreter in an action should use this field so Bazel can stage the +interpreter together with its runfiles. This is `None` for platform runtimes +using `interpreter_path` and for file-only interpreter targets. + +:::{versionadded} 2.1.0 +::: """, "interpreter_path": """ :type: str | None @@ -336,6 +360,14 @@ to meet two criteria: :::{versionadded} 1.5.0 ::: +""", + "venv_bin_files": """ +:type: list[File] + +Files that should be added to the venv's `bin/` (or platform-specific equivalent) +directory (using the file's basename). + +:::{versionadded} 2.0.0 """, "zip_main_template": """ :type: File diff --git a/python/private/py_runtime_pair_rule.bzl b/python/private/py_runtime_pair_rule.bzl index 61cbdcd6f4..c6c4c34d19 100644 --- a/python/private/py_runtime_pair_rule.bzl +++ b/python/private/py_runtime_pair_rule.bzl @@ -17,9 +17,7 @@ load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") load("//python:py_runtime_info.bzl", "PyRuntimeInfo") load(":common_labels.bzl", "labels") -load(":flags.bzl", "read_possibly_native_flag") load(":reexports.bzl", "BuiltinPyRuntimeInfo") -load(":util.bzl", "IS_BAZEL_7_OR_HIGHER") def _py_runtime_pair_impl(ctx): if ctx.attr.py2_runtime != None: @@ -38,10 +36,9 @@ def _py_runtime_pair_impl(ctx): else: py3_runtime = None - # TODO: Uncomment this after --incompatible_python_disable_py2 defaults to true - # if _is_py2_disabled(ctx) and py2_runtime != None: - # fail("Using Python 2 is not supported and disabled; see " + - # "https://github.com/bazelbuild/bazel/issues/15684") + if py2_runtime != None: + fail("Using Python 2 is not supported and disabled; see " + + "https://github.com/bazelbuild/bazel/issues/15684") extra_kwargs = {} if ctx.attr._visible_for_testing[BuildSettingInfo].value: @@ -58,20 +55,11 @@ def _get_py_runtime_info(target): # py_binary (implemented in Java) performs a type check on the provider # value to verify it is an instance of the Java-implemented PyRuntimeInfo # class. - if (IS_BAZEL_7_OR_HIGHER and PyRuntimeInfo in target) or BuiltinPyRuntimeInfo == None: + if (PyRuntimeInfo in target) or BuiltinPyRuntimeInfo == None: return target[PyRuntimeInfo] else: return target[BuiltinPyRuntimeInfo] -# buildifier: disable=unused-variable -def _is_py2_disabled(ctx): - # Because this file isn't bundled with Bazel, so we have to conditionally - # check for this flag. - # TODO: Remove this once all supported Balze versions have this flag. - if not hasattr(ctx.fragments.py, "disable_py"): - return False - return read_possibly_native_flag(ctx, "disable_py2") - _MaybeBuiltinPyRuntimeInfo = [[BuiltinPyRuntimeInfo]] if BuiltinPyRuntimeInfo != None else [] py_runtime_pair = rule( diff --git a/python/private/py_runtime_rule.bzl b/python/private/py_runtime_rule.bzl index ba1a390ef3..48637389bf 100644 --- a/python/private/py_runtime_rule.bzl +++ b/python/private/py_runtime_rule.bzl @@ -21,7 +21,7 @@ load(":flags.bzl", "FreeThreadedFlag") load(":py_internal.bzl", "py_internal") load(":py_runtime_info.bzl", "DEFAULT_STUB_SHEBANG", "PyRuntimeInfo") load(":reexports.bzl", "BuiltinPyRuntimeInfo") -load(":util.bzl", "IS_BAZEL_7_OR_HIGHER") +load(":version.bzl", "version") _py_builtins = py_internal @@ -39,6 +39,7 @@ def _py_runtime_impl(ctx): runfiles = ctx.runfiles() hermetic = bool(interpreter) + interpreter_files_to_run = None if not hermetic: if runtime_files: fail("if 'interpreter_path' is given then 'files' must be empty") @@ -46,9 +47,35 @@ def _py_runtime_impl(ctx): fail("interpreter_path must be an absolute path") else: interpreter_di = interpreter[DefaultInfo] + interpreter_file = None - if interpreter_di.files_to_run and interpreter_di.files_to_run.executable: + if _is_singleton_depset(interpreter_di.files): + interpreter_file = interpreter_di.files.to_list()[0] + + is_executable_source_file = ( + interpreter_file and + interpreter_file.is_source and + interpreter_di.files_to_run and + interpreter_di.files_to_run.executable == interpreter_file + ) + + if is_executable_source_file: + # Source files Bazel treats as executable, e.g. direct file labels + # and filegroups: preserve historical runtime-file expansion, but + # do not expose a FilesToRunProvider. + interpreter = interpreter_file + runfiles = runfiles.merge(interpreter_di.default_runfiles) + + runtime_files = depset(transitive = [ + interpreter_di.files, + interpreter_di.default_runfiles.files, + runtime_files, + ]) + elif interpreter_di.files_to_run and interpreter_di.files_to_run.executable: + # Executable rule target: use the executable and preserve the full + # FilesToRunProvider so action consumers can stage its runfiles. interpreter = interpreter_di.files_to_run.executable + interpreter_files_to_run = interpreter_di.files_to_run runfiles = runfiles.merge(interpreter_di.default_runfiles) runtime_files = depset(transitive = [ @@ -56,8 +83,10 @@ def _py_runtime_impl(ctx): interpreter_di.default_runfiles.files, runtime_files, ]) - elif _is_singleton_depset(interpreter_di.files): - interpreter = interpreter_di.files.to_list()[0] + elif interpreter_file: + # Non-executable rule with exactly one output: preserve the + # historical file-only interpreter behavior. + interpreter = interpreter_file else: fail("interpreter must be an executable target or must produce exactly one file.") @@ -87,10 +116,9 @@ def _py_runtime_impl(ctx): if python_version_flag: interpreter_version_info = _interpreter_version_info_from_version_str(python_version_flag) - # TODO: Uncomment this after --incompatible_python_disable_py2 defaults to true - # if ctx.fragments.py.disable_py2 and python_version == "PY2": - # fail("Using Python 2 is not supported and disabled; see " + - # "https://github.com/bazelbuild/bazel/issues/15684") + if python_version == "PY2": + fail("Using Python 2 is not supported and disabled; see " + + "https://github.com/bazelbuild/bazel/issues/15684") pyc_tag = ctx.attr.pyc_tag if not pyc_tag and (ctx.attr.implementation_name and @@ -125,17 +153,16 @@ def _py_runtime_impl(ctx): py_runtime_info_kwargs.update(dict( implementation_name = ctx.attr.implementation_name, interpreter_version_info = interpreter_version_info, + interpreter_files_to_run = interpreter_files_to_run, pyc_tag = pyc_tag, stage2_bootstrap_template = ctx.file.stage2_bootstrap_template, zip_main_template = ctx.file.zip_main_template, abi_flags = abi_flags, site_init_template = ctx.file.site_init_template, supports_build_time_venv = ctx.attr.supports_build_time_venv, + venv_bin_files = ctx.files.venv_bin_files, )) - if not IS_BAZEL_7_OR_HIGHER: - builtin_py_runtime_info_kwargs.pop("bootstrap_template") - providers = [ PyRuntimeInfo(**py_runtime_info_kwargs), DefaultInfo( @@ -364,8 +391,9 @@ See {obj}`PyRuntimeInfo.supports_build_time_venv` for docs. """, default = True, ), + "venv_bin_files": attr.label_list(allow_files = True), "zip_main_template": attr.label( - default = "//python/private:zip_main_template", + default = "//python/private/zipapp:zip_main_template", allow_single_file = True, doc = """ The template to use for a zip's top-level `__main__.py` file. @@ -388,18 +416,14 @@ The {obj}`PyRuntimeInfo.zip_main_template` field. ) def _is_singleton_depset(files): - # Bazel 6 doesn't have this helper to optimize detecting singleton depsets. - if _py_builtins: - return _py_builtins.is_singleton_depset(files) - else: - return len(files.to_list()) == 1 + return _py_builtins.is_singleton_depset(files) def _interpreter_version_info_from_version_str(version_str): - parts = version_str.split(".") + v = version.parse(version_str) version_info = {} + parts = list(v.release) for key in ("major", "minor", "micro"): if not parts: break version_info[key] = parts.pop(0) - return version_info diff --git a/python/private/py_test_macro.bzl b/python/private/py_test_macro.bzl index 028dee6678..bc58f859f8 100644 --- a/python/private/py_test_macro.bzl +++ b/python/private/py_test_macro.bzl @@ -13,12 +13,12 @@ # limitations under the License. """Implementation of macro-half of py_test rule.""" -load(":py_executable.bzl", "convert_legacy_create_init_to_int") +load(":py_executable.bzl", "common_executable_macro_kwargs_setup") load(":py_test_rule.bzl", py_test_rule = "py_test") def py_test(**kwargs): py_test_macro(py_test_rule, **kwargs) def py_test_macro(py_rule, **kwargs): - convert_legacy_create_init_to_int(kwargs) + common_executable_macro_kwargs_setup(kwargs) py_rule(**kwargs) diff --git a/python/private/py_test_main_validator.py b/python/private/py_test_main_validator.py new file mode 100644 index 0000000000..e3849c5f57 --- /dev/null +++ b/python/private/py_test_main_validator.py @@ -0,0 +1,224 @@ +"""Static check that a py_test main module actually runs something. + +A common ``py_test`` pitfall is to define test classes or functions but forget +to add any code that actually executes them (for example, assuming that +``py_test`` automatically invokes ``unittest`` or ``pytest``). When that +happens, running the test does nothing and the target silently passes. + +This validator parses the main module with :mod:`ast` and fails if the +module body is "inert", i.e. every top-level statement is one that does not +run anything (definitions, imports, assignments, docstrings, ``pass``). A +single active statement -- a bare call, a loop, or the conventional +``if __name__ == "__main__":`` guard -- is enough to consider the module able +to run tests. Top-level ``if`` and ``try`` blocks are inspected recursively, so +common guards like ``if TYPE_CHECKING:`` or ``try: import foo except +ImportError:`` (whose branches are themselves inert) don't bypass the check. + +As an exception, a module that defines no classes or functions at all (for +example, one that only imports other modules) is always allowed: it isn't the +"defined some tests but forgot to run them" case this check targets, and it +may legitimately rely on import side effects. +""" + +import argparse +import ast +import sys + +# Statement node types that never run any code on their own, regardless of +# their contents. A module whose top-level body consists solely of these (and +# inert assignments/expressions/guards, see below) is considered inert. +_INERT_NODE_TYPES = [ + ast.FunctionDef, + ast.AsyncFunctionDef, + ast.ClassDef, + ast.Import, + ast.ImportFrom, + ast.Global, + ast.Pass, +] + +# `ast.TypeAlias` (PEP 695, e.g. `type Alias = int`) only exists on Python +# 3.12+. Add it dynamically so the validator still imports on older versions. +if hasattr(ast, "TypeAlias"): + _INERT_NODE_TYPES.append(ast.TypeAlias) + +_INERT_NODE_TYPES = tuple(_INERT_NODE_TYPES) + +# `ast.TryStar` (PEP 654, `try/except*`) only exists on Python 3.11+. +_TRY_NODE_TYPES = (ast.Try, ast.TryStar) if hasattr(ast, "TryStar") else (ast.Try,) + +# Expression node types whose evaluation runs code (and thus may run tests). +# `await`/`yield` can't appear at the module top level, but are included for +# completeness when walking nested expressions. +_ACTIVE_EXPR_NODE_TYPES = (ast.Call, ast.Await, ast.Yield, ast.YieldFrom) + + +def _expression_runs_code(value) -> bool: + """Returns True if evaluating the expression would invoke/await/yield. + + Note this walks the expression as written; it does not try to model what + actually executes (e.g. a call inside a `lambda` body won't run until the + lambda is called). Erring toward "runs code" keeps the safeguard from + flagging valid tests. + """ + if value is None: + return False + return any(isinstance(child, _ACTIVE_EXPR_NODE_TYPES) for child in ast.walk(value)) + + +def _all_inert(nodes) -> bool: + return all(_is_inert_statement(node) for node in nodes) + + +def _is_inert_statement(node: ast.stmt) -> bool: + """Returns True if the top-level statement does not run any test code.""" + if isinstance(node, _INERT_NODE_TYPES): + return True + + # Assignments are inert unless their value runs code, e.g. + # `exit_code = unittest.main()` actually runs the tests. + if isinstance(node, (ast.Assign, ast.AnnAssign, ast.AugAssign)): + return not _expression_runs_code(node.value) + + # Bare expression statements: docstrings and other no-op expressions are + # inert; a call/await/yield (e.g. `unittest.main()`) runs code. + # Assert statements are inert unless their condition or message runs code. + if isinstance(node, ast.Assert): + return not (_expression_runs_code(node.test) or _expression_runs_code(node.msg)) + + if isinstance(node, ast.Expr): + return not _expression_runs_code(node.value) + + # An `if` whose condition and branches are entirely inert, e.g. the very + # common `if TYPE_CHECKING:` guard around typing-only imports. A call in the + # condition (e.g. `if feature_enabled():`) or a runner call in a branch body + # makes it active. + if isinstance(node, ast.If): + return ( + not _expression_runs_code(node.test) + and _all_inert(node.body) + and _all_inert(node.orelse) + ) + + # A `try` (or `try/except*`) whose body, handlers, else, and finally are all + # inert, e.g. the common `try: import foo except ImportError: ...` optional + # import pattern. + if isinstance(node, _TRY_NODE_TYPES): + if not ( + _all_inert(node.body) + and _all_inert(node.orelse) + and _all_inert(node.finalbody) + ): + return False + return all(_all_inert(handler.body) for handler in node.handlers) + + return False + + +def module_runs_something(tree: ast.Module) -> bool: + """Returns True if the module body has at least one active statement.""" + return any(not _is_inert_statement(node) for node in tree.body) + + +def _defines_test_code(tree: ast.Module) -> bool: + """Returns True if the module defines any top-level class or function.""" + return any( + isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) + for node in tree.body + ) + + +def module_runs_tests(tree: ast.Module) -> bool: + """Returns True if the module appears able to run tests (or isn't checked). + + The check targets the specific pitfall of defining test classes/functions + but never running them. A module that defines no classes or functions at + all (for example, one that only imports other modules) is not subject to + the check and is always allowed, since it isn't the "defined but never run" + case and may legitimately rely on import side effects. + """ + if module_runs_something(tree): + return True + return not _defines_test_code(tree) + + +def _format_error(label: str, src_name: str) -> str: + target = label or src_name + return ( + "py_test target {target} will not run any tests.\n" + "\n" + "The main module '{src_name}' only contains inert top-level statements " + "(class/function definitions, imports, assignments). Running it does " + "nothing, so the test silently passes without executing any test " + "code.\n" + "\n" + "py_test runs the main module directly; it does not automatically " + "invoke a test runner such as unittest or pytest. Add code that runs " + "your tests, for example:\n" + "\n" + ' if __name__ == "__main__":\n' + " unittest.main()\n" + "\n" + "or use a main module that invokes a runner (e.g. pytest.main()).\n" + "\n" + "This check can be disabled by setting " + "--@rules_python//python/config_settings:validate_test_main=disabled." + ).format(target=target, src_name=src_name) + + +def main(args) -> int: + parser = argparse.ArgumentParser() + parser.add_argument( + "--src", + required=True, + help="Path to the main .py source file to analyze.", + ) + parser.add_argument( + "--src_name", + default="", + help="Human-friendly name of the source file, used in error messages.", + ) + parser.add_argument( + "--label", + default="", + help="The py_test target label, used in error messages.", + ) + parser.add_argument( + "--output", + required=True, + help="Path to the validation marker file to write on success.", + ) + options = parser.parse_args(args) + + src_name = options.src_name or options.src + + with open(options.src, "rb") as f: + source = f.read() + + try: + tree = ast.parse(source, filename=src_name) + except SyntaxError as e: + # A syntax error is surfaced by other actions (compilation/execution). + # The validator can't analyze the file, so don't fail here; treat it as + # passing to avoid duplicate or confusing errors. + sys.stderr.write( + "WARNING: py_test main validator could not parse {}: {}\n".format( + src_name, e + ) + ) + with open(options.output, "w") as out: + out.write("") + return 0 + + if not module_runs_tests(tree): + sys.stderr.write(_format_error(options.label, src_name) + "\n") + return 1 + + # Validation actions must produce their declared outputs on success. + with open(options.output, "w") as out: + out.write("") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/python/private/py_test_rule.bzl b/python/private/py_test_rule.bzl index bb35d6974e..5848682deb 100644 --- a/python/private/py_test_rule.bzl +++ b/python/private/py_test_rule.bzl @@ -41,7 +41,7 @@ def create_py_test_rule_builder(): ::: Returns: - {type}`ruleb.Rule` with the necessary settings + {obj}`ruleb.Rule` with the necessary settings for creating a `py_test` rule. """ builder = create_executable_rule_builder( diff --git a/python/private/py_wheel.bzl b/python/private/py_wheel.bzl index 8202fa015a..b622411c56 100644 --- a/python/private/py_wheel.bzl +++ b/python/private/py_wheel.bzl @@ -18,7 +18,7 @@ load(":attributes.bzl", "CONFIG_SETTINGS_ATTR", "apply_config_settings_attr") load(":py_info.bzl", "PyInfo") load(":py_package.bzl", "py_package_lib") load(":rule_builders.bzl", "ruleb") -load(":stamp.bzl", "is_stamping_enabled") +load(":stamp_impl.bzl", "is_stamping_enabled") load(":transition_labels.bzl", "TRANSITION_LABELS") load(":version.bzl", "version") @@ -170,6 +170,23 @@ entry_points, e.g. `{'console_scripts': ['main = examples.wheel.main:main']}`. } _other_attrs = { + "add_path_prefix": attr.string( + default = "", + doc = """\ +Path prefix to prepend to files added to the generated package. +This prefix will be prepended **after** the paths are first stripped of the prefixes +specified in `strip_path_prefixes`. + +For example: ++ `"foo/" will prepend to `"bar/baz/file.py"` as `"foo/bar/baz/file.py"` ++ `"foo_" will prepend to `"bar/baz/file.py"` as `"foo_bar/baz/file.py"` ++ `stripping ["bar/"] and adding "foo/" will change `"bar/baz/file.py"` to `"foo/baz/file.py"` + +:::{versionadded} 2.0.0 +The {attr}`add_path_prefix` attribute was added. +::: +""", + ), "author": attr.string( doc = "A string specifying the author of the package.", default = "", @@ -182,8 +199,35 @@ _other_attrs = { doc = "A list of strings describing the categories for the package. For valid classifiers see https://pypi.org/classifiers", ), "data_files": attr.label_keyed_string_dict( - doc = ("Any file that is not normally installed inside site-packages goes into the .data directory, named " + - "as the .dist-info directory but with the .data/ extension. Allowed paths: {prefixes}".format(prefixes = ALLOWED_DATA_FILE_PREFIX)), + doc = (""" +Mapping of data files to go into the wheel. + +The keys are targets of files to include, and the values are the `.data`-relative +path to use. + +Any file that is not normally installed inside site-packages goes into the .data +directory, named as the .dist-info directory but with the .data/ extension. If +the destination of a file or group of files ends in a `/`, the destination is a +folder and files are placed with their existing basenames under that folder. + +For example: + +``` +":file1.txt": "data/file1.txt", # Destination: .data/data/file1.txt +":file1.txt": "data/", # Destination: .data/data/file1.txt +":file1.txt": "data/special.txt", # Destination: .data/data/special.txt + +filegroup(name = "files", srcs = [":file1.txt", ":file2.txt"]) +":files": "data/", # Destinations: .data/data/file1.txt, .data/data/file2.txt +``` + +Allowed paths: {prefixes} + +:::{{versionchanged}} 2.0.0 +Values can end in slash (`/`) to indicate that all files of the target should +be moved under that directory. +::: +""".format(prefixes = ALLOWED_DATA_FILE_PREFIX)), allow_files = True, ), "description_content_type": attr.string( @@ -344,12 +388,13 @@ def _py_wheel_impl(ctx): # Currently this is only the description file (if used). other_inputs = [] - # Wrap the inputs into a file to reduce command line length. + # Wrap the inputs into a file to reduce command line length, deferring + # depset expansion to execution time via Args.add_all with map_each. packageinputfile = ctx.actions.declare_file(ctx.attr.name + "_target_wrapped_inputs.txt") - content = "" - for input_file in inputs_to_package.to_list(): - content += _input_file_to_arg(input_file) + "\n" - ctx.actions.write(output = packageinputfile, content = content) + package_args = ctx.actions.args() + package_args.set_param_file_format("multiline") + package_args.add_all(inputs_to_package, map_each = _input_file_to_arg) + ctx.actions.write(output = packageinputfile, content = package_args) other_inputs.append(packageinputfile) args = ctx.actions.args() @@ -361,6 +406,7 @@ def _py_wheel_impl(ctx): args.add("--out", outfile) args.add("--name_file", name_file) args.add_all(ctx.attr.strip_path_prefixes, format_each = "--strip_path_prefix=%s") + args.add("--path_prefix", ctx.attr.add_path_prefix) # Pass workspace status files if stamping is enabled if is_stamping_enabled(ctx.attr): @@ -505,9 +551,9 @@ def _py_wheel_impl(ctx): for target, filename in ctx.attr.data_files.items(): target_files = target[DefaultInfo].files.to_list() - if len(target_files) != 1: + if len(target_files) != 1 and not filename.endswith("/"): fail( - "Multi-file target listed in data_files %s", + "Multi-file target listed in data_files %s, this is only supported when specifying a folder path (i.e. a path ending in '/')", filename, ) @@ -519,11 +565,15 @@ def _py_wheel_impl(ctx): filename, ), ) - other_inputs.extend(target_files) - args.add( - "--data_files", - filename + ";" + target_files[0].path, - ) + + for file in target_files: + final_filename = filename + file.basename if filename.endswith("/") else filename + + other_inputs.extend(target_files) + args.add( + "--data_files", + final_filename + ";" + file.path, + ) ctx.actions.run( mnemonic = "PyWheel", diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel index 0d2f73fb0b..8dd32afb9c 100644 --- a/python/private/pypi/BUILD.bazel +++ b/python/private/pypi/BUILD.bazel @@ -23,6 +23,29 @@ exports_files( visibility = ["//visibility:public"], ) +alias( + name = "venv_entry_point_template", + actual = select({ + "@platforms//os:windows": "venv_entry_point_template.bat", + "//conditions:default": "venv_entry_point_template.sh", + }), + visibility = ["//visibility:public"], +) + +alias( + name = "venv_shebang_rewriter", + actual = select({ + "@platforms//os:windows": "venv_shebang_rewriter.ps1", + "//conditions:default": "venv_shebang_rewriter.sh", + }), + visibility = ["//visibility:public"], +) + +exports_files( + srcs = ["deps.bzl"], + visibility = ["//tools/private/update_deps:__pkg__"], +) + filegroup( name = "distribution", srcs = glob( @@ -51,412 +74,508 @@ filegroup( visibility = ["//tools/private/update_deps:__pkg__"], ) -# Keep sorted by library name and keep the files named by the main symbol they export - bzl_library( - name = "attrs_bzl", - srcs = ["attrs.bzl"], -) - -bzl_library( - name = "config_settings_bzl", + name = "config_settings", srcs = ["config_settings.bzl"], - deps = [ - ":flags_bzl", - "//python/private:common_labels_bzl", - "//python/private:flags_bzl", - "@bazel_skylib//lib:selects", - ], + deps = ["@bazel_skylib//lib:selects"], ) bzl_library( - name = "deps_bzl", + name = "deps", srcs = ["deps.bzl"], - deps = [ - "//python/private:bazel_tools_bzl", - "//python/private:glob_excludes_bzl", - ], + deps = ["//python/private:bazel_tools"], ) bzl_library( - name = "env_marker_info_bzl", - srcs = ["env_marker_info.bzl"], -) - -bzl_library( - name = "env_marker_setting_bzl", + name = "env_marker_setting", srcs = ["env_marker_setting.bzl"], deps = [ - ":env_marker_info_bzl", - ":pep508_env_bzl", - ":pep508_evaluate_bzl", - "//python/private:common_labels_bzl", - "//python/private:toolchain_types_bzl", + ":env_marker_info", + ":pep508_env", + ":pep508_evaluate", + "//python/private:common_labels", + "//python/private:toolchain_types", "@bazel_skylib//rules:common_settings", ], ) bzl_library( - name = "evaluate_markers_bzl", + name = "evaluate_markers", srcs = ["evaluate_markers.bzl"], deps = [ - ":deps_bzl", - ":pep508_evaluate_bzl", - ":pep508_requirement_bzl", - ":pypi_repo_utils_bzl", + ":pep508_evaluate", + ":pep508_requirement", ], ) +# keep bzl_library( - name = "extension_bzl", + name = "extension", srcs = ["extension.bzl"], deps = [ - ":evaluate_markers_bzl", - ":hub_builder_bzl", - ":hub_repository_bzl", - ":parse_whl_name_bzl", - ":pep508_env_bzl", - ":pip_repository_attrs_bzl", - ":platform_bzl", - ":simpleapi_download_bzl", - ":whl_library_bzl", - "//python/private:auth_bzl", - "//python/private:normalize_name_bzl", - "//python/private:repo_utils_bzl", - "@bazel_features//:features", - "@pythons_hub//:interpreters_bzl", - "@pythons_hub//:versions_bzl", - ], -) - -bzl_library( - name = "flags_bzl", + ":hub_builder", + ":hub_repository", + ":parse_whl_name", + ":pep508_env", + ":pip_repository_attrs", + ":platform", + ":pypi_cache", + ":simpleapi_download", + ":unified_hub_repo", + ":whl_library", + "//python/private:auth", + "//python/private:normalize_name", + "//python/private:pyproject_utils", + "//python/private:repo_utils", + "@pythons_hub//:interpreters", + "@pythons_hub//:versions", + "@rules_python_internal//:rules_python_config", + "@toml.bzl//:toml", + ], +) + +bzl_library( + name = "flags", srcs = ["flags.bzl"], deps = [ - ":env_marker_info.bzl", - ":pep508_env_bzl", - "//python/private:enum_bzl", + ":env_marker_info", + ":pep508_env", + "//python/private:common_labels", "@bazel_skylib//rules:common_settings", ], ) bzl_library( - name = "generate_whl_library_build_bazel_bzl", - srcs = ["generate_whl_library_build_bazel.bzl"], - deps = [ - "//python/private:text_util_bzl", - ], -) - -bzl_library( - name = "generate_group_library_build_bazel_bzl", + name = "generate_group_library_build_bazel", srcs = ["generate_group_library_build_bazel.bzl"], deps = [ - ":labels_bzl", - "//python/private:normalize_name_bzl", + ":labels", + "//python/private:normalize_name", + "//python/private:text_util", ], ) bzl_library( - name = "group_library_bzl", - srcs = ["group_library.bzl"], - deps = [ - ":generate_group_library_build_bazel_bzl", - ], + name = "generate_whl_library_build_bazel", + srcs = ["generate_whl_library_build_bazel.bzl"], + deps = ["//python/private:text_util"], ) bzl_library( - name = "hub_builder_bzl", + name = "hub_builder", srcs = ["hub_builder.bzl"], - visibility = ["//:__subpackages__"], deps = [ - ":attrs_bzl", - ":evaluate_markers_bzl", - ":parse_requirements_bzl", - ":pep508_env_bzl", - ":pep508_evaluate_bzl", - ":python_tag_bzl", - ":requirements_files_by_platform_bzl", - ":whl_config_setting_bzl", - ":whl_repo_name_bzl", - "//python/private:full_version_bzl", - "//python/private:normalize_name_bzl", - "//python/private:version_bzl", - "//python/private:version_label_bzl", - ], -) - -bzl_library( - name = "hub_repository_bzl", + ":attrs", + ":parse_requirements", + ":pep508_env", + ":pep508_evaluate", + ":python_tag", + ":requirements_files_by_platform", + ":whl_config_setting", + ":whl_repo_name", + "//python/private:envsubst", + "//python/private:full_version", + "//python/private:normalize_name", + "//python/private:repo_utils", + "//python/private:text_util", + "//python/private:version", + "//python/private:version_label", + ], +) + +bzl_library( + name = "hub_repository", srcs = ["hub_repository.bzl"], - visibility = ["//:__subpackages__"], deps = [ - ":render_pkg_aliases_bzl", - "//python/private:text_util_bzl", + ":render_pkg_aliases", + ":whl_config_setting", + "//python/private:text_util", ], ) bzl_library( - name = "index_sources_bzl", - srcs = ["index_sources.bzl"], -) - -bzl_library( - name = "labels_bzl", - srcs = ["labels.bzl"], + name = "missing_package", + srcs = ["missing_package.bzl"], + deps = [ + "//python/private:py_info", + "//python/private:reexports", + ], ) bzl_library( - name = "multi_pip_parse_bzl", + name = "multi_pip_parse", srcs = ["multi_pip_parse.bzl"], deps = [ - ":pip_repository_bzl", - "//python/private:text_util_bzl", + ":pip_repository", + "//python/private:text_util", ], ) bzl_library( - name = "package_annotation_bzl", - srcs = ["package_annotation.bzl"], + name = "namespace_pkgs", + srcs = ["namespace_pkgs.bzl"], + deps = ["@bazel_skylib//rules:copy_file"], ) bzl_library( - name = "parse_requirements_bzl", + name = "parse_requirements", srcs = ["parse_requirements.bzl"], deps = [ - ":index_sources_bzl", - ":parse_requirements_txt_bzl", - ":pypi_repo_utils_bzl", - ":requirements_files_by_platform_bzl", - ":select_whl_bzl", - "//python/private:normalize_name_bzl", - "//python/private:repo_utils_bzl", + ":argparse", + ":index_sources", + ":parse_requirements_txt", + ":pep508_evaluate", + ":pep508_requirement", + ":select_whl", + "//python/private:normalize_name", + "//python/private:repo_utils", + "//python/uv/private:uv_lock_to_requirements", ], ) bzl_library( - name = "parse_requirements_txt_bzl", - srcs = ["parse_requirements_txt.bzl"], -) - -bzl_library( - name = "parse_simpleapi_html_bzl", + name = "parse_simpleapi_html", srcs = ["parse_simpleapi_html.bzl"], + deps = [ + ":version_from_filename", + "//python/private:normalize_name", + ], ) bzl_library( - name = "parse_whl_name_bzl", - srcs = ["parse_whl_name.bzl"], -) - -bzl_library( - name = "patch_whl_bzl", + name = "patch_whl", srcs = ["patch_whl.bzl"], deps = [ - ":parse_whl_name_bzl", - "//python/private:repo_utils_bzl", + ":parse_whl_name", + "//python/private:repo_utils", + "@rules_python_internal//:rules_python_config", ], ) bzl_library( - name = "pep508_deps_bzl", + name = "pep508_deps", srcs = ["pep508_deps.bzl"], deps = [ - ":pep508_evaluate_bzl", - ":pep508_requirement_bzl", - "//python/private:normalize_name_bzl", + ":pep508_env", + ":pep508_evaluate", + ":pep508_requirement", + "//python/private:normalize_name", ], ) bzl_library( - name = "pep508_env_bzl", + name = "pep508_env", srcs = ["pep508_env.bzl"], deps = [ - "//python/private:version_bzl", + "//python/private:normalize_name", + "//python/private:version", ], ) bzl_library( - name = "pep508_evaluate_bzl", + name = "pep508_evaluate", srcs = ["pep508_evaluate.bzl"], deps = [ - "//python/private:enum_bzl", - "//python/private:version_bzl", + "//python/private:enum", + "//python/private:version", ], ) bzl_library( - name = "pep508_requirement_bzl", + name = "pep508_requirement", srcs = ["pep508_requirement.bzl"], - deps = [ - "//python/private:normalize_name_bzl", - ], + deps = ["//python/private:normalize_name"], ) bzl_library( - name = "pip_bzl", + name = "pip", srcs = ["pip.bzl"], - deps = [ - ":extension_bzl", - ], + deps = [":extension"], ) bzl_library( - name = "pip_compile_bzl", + name = "pip_compile", srcs = ["pip_compile.bzl"], deps = [ - ":deps_bzl", - "//python:py_binary_bzl", - "//python:py_test_bzl", + "//python:py_binary", + "//python:py_test", ], ) bzl_library( - name = "pip_repository_bzl", + name = "pip_repository", srcs = ["pip_repository.bzl"], deps = [ - ":attrs_bzl", - ":evaluate_markers_bzl", - ":parse_requirements_bzl", - ":pip_repository_attrs_bzl", - ":pypi_repo_utils_bzl", - ":render_pkg_aliases_bzl", - ":whl_config_setting_bzl", - "//python/private:normalize_name_bzl", - "//python/private:repo_utils_bzl", - "//python/private:text_util_bzl", + ":parse_requirements", + ":pep508_env", + ":pip_repository_attrs", + ":pypi_repo_utils", + ":render_pkg_aliases", + ":requirements_files_by_platform", + "//python/private:normalize_name", + "//python/private:repo_utils", + "//python/private:text_util", "@bazel_skylib//lib:sets", ], ) bzl_library( - name = "pip_repository_attrs_bzl", + name = "pip_repository_attrs", srcs = ["pip_repository_attrs.bzl"], + deps = [":attrs"], ) bzl_library( - name = "pkg_aliases_bzl", + name = "pkg_aliases", srcs = ["pkg_aliases.bzl"], deps = [ - ":labels_bzl", - "//python/private:common_labels_bzl", - "//python/private:text_util_bzl", + ":labels", + "//python/private:common_labels", + "//python/private:text_util", "@bazel_skylib//lib:selects", ], ) bzl_library( - name = "platform_bzl", - srcs = ["platform.bzl"], + name = "pypi_cache", + srcs = ["pypi_cache.bzl"], + deps = [":version_from_filename"], ) bzl_library( - name = "pypi_repo_utils_bzl", + name = "pypi_repo_utils", srcs = ["pypi_repo_utils.bzl"], deps = [ - "//python/private:repo_utils_bzl", + "//python/private:repo_utils", + "//python/private:util", "@bazel_skylib//lib:types", ], ) bzl_library( - name = "python_tag_bzl", + name = "python_tag", srcs = ["python_tag.bzl"], - deps = [ - "//python/private:version_bzl", - ], + deps = ["//python/private:version"], ) bzl_library( - name = "render_pkg_aliases_bzl", + name = "render_pkg_aliases", srcs = ["render_pkg_aliases.bzl"], deps = [ - ":generate_group_library_build_bazel_bzl", - ":whl_config_setting_bzl", - "//python/private:normalize_name_bzl", - "//python/private:text_util_bzl", + ":generate_group_library_build_bazel", + "//python/private:normalize_name", + "//python/private:text_util", ], ) bzl_library( - name = "requirements_files_by_platform_bzl", + name = "requirements_files_by_platform", srcs = ["requirements_files_by_platform.bzl"], deps = [ - ":whl_target_platforms_bzl", + ":argparse", + ":whl_target_platforms", ], ) bzl_library( - name = "select_whl_bzl", + name = "select_whl", srcs = ["select_whl.bzl"], deps = [ - ":parse_whl_name_bzl", - ":python_tag_bzl", - "//python/private:version_bzl", + ":parse_whl_name", + ":python_tag", + "//python/private:version", ], ) bzl_library( - name = "simpleapi_download_bzl", + name = "simpleapi_download", srcs = ["simpleapi_download.bzl"], deps = [ - ":parse_simpleapi_html_bzl", - "//python/private:auth_bzl", - "//python/private:normalize_name_bzl", - "//python/private:text_util_bzl", - "@bazel_features//:features", + ":parse_simpleapi_html", + ":urllib", + "//python/private:auth", + "//python/private:envsubst", + "//python/private:normalize_name", ], ) bzl_library( - name = "whl_config_setting_bzl", - srcs = ["whl_config_setting.bzl"], + name = "unified_hub_repo", + srcs = ["unified_hub_repo.bzl"], + deps = ["//python/private:text_util"], ) bzl_library( - name = "whl_library_alias_bzl", - srcs = ["whl_library_alias.bzl"], + name = "unified_hub_setup", + srcs = ["unified_hub_setup.bzl"], + deps = [ + "@rules_python//python/private/pypi:labels", + "@rules_python//python/private/pypi:missing_package", + ], +) + +bzl_library( + name = "venv_entry_point", + srcs = ["venv_entry_point.bzl"], deps = [ - ":render_pkg_aliases_bzl", - "//python/private:full_version_bzl", + "//python/private:attributes", + "//python/private:common", + "//python/private:rule_builders", ], ) bzl_library( - name = "whl_library_bzl", + name = "venv_rewrite_shebang", + srcs = ["venv_rewrite_shebang.bzl"], + deps = [ + "//python/private:attributes", + "//python/private:common", + "//python/private:py_info", + "//python/private:rule_builders", + ], +) + +bzl_library( + name = "whl_config_repo", + srcs = ["whl_config_repo.bzl"], + deps = [ + ":generate_group_library_build_bazel", + "//python/private:text_util", + ], +) + +bzl_library( + name = "whl_extract", + srcs = ["whl_extract.bzl"], + deps = [ + ":whl_metadata", + "//python/private:repo_utils", + "@rules_python_internal//:rules_python_config", + ], +) + +bzl_library( + name = "whl_library", srcs = ["whl_library.bzl"], deps = [ - ":attrs_bzl", - ":deps_bzl", - ":generate_whl_library_build_bazel_bzl", - ":patch_whl_bzl", - ":pep508_requirement_bzl", - ":pypi_repo_utils_bzl", - ":whl_metadata_bzl", - ":whl_target_platforms_bzl", - "//python/private:auth_bzl", - "//python/private:bzlmod_enabled_bzl", - "//python/private:envsubst_bzl", - "//python/private:is_standalone_interpreter_bzl", - "//python/private:repo_utils_bzl", + ":attrs", + ":deps", + ":generate_whl_library_build_bazel", + ":patch_whl", + ":pep508_requirement", + ":pypi_repo_utils", + ":urllib", + ":whl_extract", + ":whl_metadata", + "//python/private:auth", + "//python/private:envsubst", + "//python/private:is_standalone_interpreter", + "//python/private:normalize_name", + "//python/private:repo_utils", ], ) bzl_library( - name = "whl_metadata_bzl", - srcs = ["whl_metadata.bzl"], + name = "whl_library_alias", + srcs = ["whl_library_alias.bzl"], + deps = [ + ":render_pkg_aliases", + "//python/private:full_version", + ], ) bzl_library( - name = "whl_repo_name_bzl", + name = "whl_library_targets", + srcs = ["whl_library_targets.bzl"], + deps = [ + ":env_marker_setting", + ":labels", + ":namespace_pkgs", + ":pep508_deps", + ":venv_entry_point", + ":venv_rewrite_shebang", + "//python:py_binary", + "//python:py_library", + "//python/private:normalize_name", + "@bazel_skylib//rules:copy_file", + ], +) + +bzl_library( + name = "whl_repo_name", srcs = ["whl_repo_name.bzl"], deps = [ - ":parse_whl_name_bzl", - "//python/private:normalize_name_bzl", + ":parse_whl_name", + "//python/private:normalize_name", ], ) bzl_library( - name = "whl_target_platforms_bzl", + name = "argparse", + srcs = ["argparse.bzl"], +) + +bzl_library( + name = "attrs", + srcs = ["attrs.bzl"], +) + +bzl_library( + name = "env_marker_info", + srcs = ["env_marker_info.bzl"], +) + +bzl_library( + name = "index_sources", + srcs = ["index_sources.bzl"], +) + +bzl_library( + name = "labels", + srcs = ["labels.bzl"], +) + +bzl_library( + name = "package_annotation", + srcs = ["package_annotation.bzl"], +) + +bzl_library( + name = "parse_requirements_txt", + srcs = ["parse_requirements_txt.bzl"], +) + +bzl_library( + name = "parse_whl_name", + srcs = ["parse_whl_name.bzl"], +) + +bzl_library( + name = "platform", + srcs = ["platform.bzl"], +) + +bzl_library( + name = "urllib", + srcs = ["urllib.bzl"], +) + +bzl_library( + name = "version_from_filename", + srcs = ["version_from_filename.bzl"], +) + +bzl_library( + name = "whl_config_setting", + srcs = ["whl_config_setting.bzl"], +) + +bzl_library( + name = "whl_metadata", + srcs = ["whl_metadata.bzl"], +) + +bzl_library( + name = "whl_target_platforms", srcs = ["whl_target_platforms.bzl"], ) diff --git a/python/private/pypi/argparse.bzl b/python/private/pypi/argparse.bzl new file mode 100644 index 0000000000..8964411a3e --- /dev/null +++ b/python/private/pypi/argparse.bzl @@ -0,0 +1,40 @@ +"""A small set of utilities for parsing pip args.""" + +def _get_pip_args(args, *arg_names, value = None, repeated = False): + set_next = False + if repeated: + value = [] + (value or []) + + for arg in (args or []): + if arg in arg_names: + set_next = True + continue + + val = None + if set_next: + set_next = False + val = arg + else: + for arg_name in arg_names: + start = "{}=".format(arg_name) + + if arg.startswith(start): + val = arg[len(start):] + break + + if val == None: + continue + + if repeated: + if val not in value: + value.append(val) + else: + value = val + + return value + +argparse = struct( + index_url = lambda args, default: _get_pip_args(args, "-i", "--index-url", value = default), + extra_index_url = lambda args, default: _get_pip_args(args, "--extra-index-url", value = default, repeated = True), + platform = lambda args, default: _get_pip_args(args, "--platform", value = default, repeated = True), +) diff --git a/python/private/pypi/attrs.bzl b/python/private/pypi/attrs.bzl index a122fc8479..57bd93f40a 100644 --- a/python/private/pypi/attrs.bzl +++ b/python/private/pypi/attrs.bzl @@ -118,39 +118,6 @@ Warning: If a dependency participates in multiple cycles, all of those cycles must be collapsed down to one. For instance `a <-> b` and `a <-> c` cannot be listed as two separate cycles. -""", - ), - "experimental_target_platforms": attr.string_list( - default = [], - doc = """\ -*NOTE*: This will be removed in the next major version, so please consider migrating -to `bzlmod` and rely on {attr}`pip.parse.requirements_by_platform` for this feature. - -A list of platforms that we will generate the conditional dependency graph for -cross platform wheels by parsing the wheel metadata. This will generate the -correct dependencies for packages like `sphinx` or `pylint`, which include -`colorama` when installed and used on Windows platforms. - -An empty list means falling back to the legacy behaviour where the host -platform is the target platform. - -WARNING: It may not work as expected in cases where the python interpreter -implementation that is being used at runtime is different between different platforms. -This has been tested for CPython only. - -For specific target platforms use values of the form `_` where `` -is one of `linux`, `osx`, `windows` and arch is one of `x86_64`, `x86_32`, -`aarch64`, `s390x` and `ppc64le`. - -You can also target a specific Python version by using `cp3__`. -If multiple python versions are specified as target platforms, then select statements -of the `lib` and `whl` targets will include usage of version aware toolchain config -settings like `@rules_python//python/config_settings:is_python_3.y`. - -Special values: `host` (for generating deps for the host platform only) and -`_*` values. For example, `cp39_*`, `linux_*`, `cp39_linux_*`. - -NOTE: this is not for cross-compiling Python wheels but rather for parsing the `whl` METADATA correctly. """, ), "extra_hub_aliases": attr.string_list_dict( @@ -240,7 +207,7 @@ def use_isolated(ctx, attr): use_isolated = attr.isolated # The environment variable will take precedence over the attribute - isolated_env = ctx.os.environ.get("RULES_PYTHON_PIP_ISOLATED", None) + isolated_env = ctx.getenv("RULES_PYTHON_PIP_ISOLATED", None) if isolated_env != None: if isolated_env.lower() in ("0", "false"): use_isolated = False diff --git a/python/private/pypi/config.bzl.tmpl.bzlmod b/python/private/pypi/config.bzl.tmpl similarity index 79% rename from python/private/pypi/config.bzl.tmpl.bzlmod rename to python/private/pypi/config.bzl.tmpl index c3ada70d27..1037153cac 100644 --- a/python/private/pypi/config.bzl.tmpl.bzlmod +++ b/python/private/pypi/config.bzl.tmpl @@ -2,8 +2,6 @@ NOTE: This is internal `rules_python` API and if you would like to depend on it, please raise an issue with your usecase. This may change in between rules_python versions without any notice. - -@generated by rules_python pip.parse bzlmod extension. """ -whl_map = %%WHL_MAP%% +packages = %%PACKAGES%% diff --git a/python/private/pypi/dependency_resolver/dependency_resolver.py b/python/private/pypi/dependency_resolver/dependency_resolver.py index f3a339f929..34f2fb1e5a 100644 --- a/python/private/pypi/dependency_resolver/dependency_resolver.py +++ b/python/private/pypi/dependency_resolver/dependency_resolver.py @@ -159,7 +159,9 @@ def main( # Further, shutil.copy preserves the source file's mode, and so if # our source file is read-only (the default under Perforce Helix), # this scratch file will also be read-only, defeating its purpose. - with open(resolved_requirements_file, "rb") as fsrc, open(requirements_out, "wb") as fdst: + with open(resolved_requirements_file, "rb") as fsrc, open( + requirements_out, "wb" + ) as fdst: shutil.copyfileobj(fsrc, fdst) update_command = ( diff --git a/python/private/pypi/deps.bzl b/python/private/pypi/deps.bzl index 73b30c69ee..5d0507cb98 100644 --- a/python/private/pypi/deps.bzl +++ b/python/private/pypi/deps.bzl @@ -21,13 +21,13 @@ _RULE_DEPS = [ # START: maintained by 'bazel run //tools/private/update_deps:update_pip_deps' ( "pypi__build", - "https://files.pythonhosted.org/packages/e2/03/f3c8ba0a6b6e30d7d18c40faab90807c9bb5e9a1e3b2fe2008af624a9c97/build-1.2.1-py3-none-any.whl", - "75e10f767a433d9a86e50d83f418e83efc18ede923ee5ff7df93b6cb0306c5d4", + "https://files.pythonhosted.org/packages/c5/0d/84a4380f930db0010168e0aa7b7a8fed9ba1835a8fbb1472bc6d0201d529/build-1.4.0-py3-none-any.whl", + "6a07c1b8eb6f2b311b96fcbdbce5dab5fe637ffda0fd83c9cac622e927501596", ), ( "pypi__click", - "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", - "ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", + "981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", ), ( "pypi__colorama", @@ -36,8 +36,8 @@ _RULE_DEPS = [ ), ( "pypi__importlib_metadata", - "https://files.pythonhosted.org/packages/2d/0a/679461c511447ffaf176567d5c496d1de27cbe34a87df6677d7171b2fbd4/importlib_metadata-7.1.0-py3-none-any.whl", - "30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570", + "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", + "5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", ), ( "pypi__installer", @@ -46,13 +46,13 @@ _RULE_DEPS = [ ), ( "pypi__more_itertools", - "https://files.pythonhosted.org/packages/50/e2/8e10e465ee3987bb7c9ab69efb91d867d93959095f4807db102d07995d94/more_itertools-10.2.0-py3-none-any.whl", - "686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684", + "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", + "52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", ), ( "pypi__packaging", - "https://files.pythonhosted.org/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl", - "2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", + "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", + "b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", ), ( "pypi__pep517", @@ -61,38 +61,38 @@ _RULE_DEPS = [ ), ( "pypi__pip", - "https://files.pythonhosted.org/packages/8a/6a/19e9fe04fca059ccf770861c7d5721ab4c2aebc539889e97c7977528a53b/pip-24.0-py3-none-any.whl", - "ba0d021a166865d2265246961bec0152ff124de910c5cc39f1156ce3fa7c69dc", + "https://files.pythonhosted.org/packages/de/f0/c81e05b613866b76d2d1066490adf1a3dbc4ee9d9c839961c3fc8a6997af/pip-26.0.1-py3-none-any.whl", + "bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b", ), ( "pypi__pip_tools", - "https://files.pythonhosted.org/packages/0d/dc/38f4ce065e92c66f058ea7a368a9c5de4e702272b479c0992059f7693941/pip_tools-7.4.1-py3-none-any.whl", - "4c690e5fbae2f21e87843e89c26191f0d9454f362d8acdbd695716493ec8b3a9", + "https://files.pythonhosted.org/packages/6e/74/59906d876c6cb1137f42a137164f2fe683b06283cde84bfcf7f5dd43970b/pip_tools-7.5.3-py3-none-any.whl", + "3aac0c473240ae90db7213c033401f345b05197293ccbdd2704e52e7a783785e", ), ( "pypi__pyproject_hooks", - "https://files.pythonhosted.org/packages/ae/f3/431b9d5fe7d14af7a32340792ef43b8a714e7726f1d7b69cc4e8e7a3f1d7/pyproject_hooks-1.1.0-py3-none-any.whl", - "7ceeefe9aec63a1064c18d939bdc3adf2d8aa1988a510afec15151578b232aa2", + "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", + "9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", ), ( "pypi__setuptools", - "https://files.pythonhosted.org/packages/90/99/158ad0609729111163fc1f674a5a42f2605371a4cf036d0441070e2f7455/setuptools-78.1.1-py3-none-any.whl", - "c3a9c4211ff4c309edb8b8c4f1cbfa7ae324c4ba9f91ff254e3d305b9fd54561", + "https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl", + "70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0", ), ( "pypi__tomli", - "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", - "939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", + "1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", ), ( "pypi__wheel", - "https://files.pythonhosted.org/packages/7d/cd/d7460c9a869b16c3dd4e1e403cce337df165368c71d6af229a74699622ce/wheel-0.43.0-py3-none-any.whl", - "55c570405f142630c6b9f72fe09d9b67cf1477fcf543ae5b8dcb1f5b7377da81", + "https://files.pythonhosted.org/packages/87/22/b76d483683216dde3d67cba61fb2444be8d5be289bf628c13fc0fd90e5f9/wheel-0.46.3-py3-none-any.whl", + "4b399d56c9d9338230118d705d9737a2a468ccca63d5e813e2a4fc7815d8bc4d", ), ( "pypi__zipp", - "https://files.pythonhosted.org/packages/da/55/a03fd7240714916507e1fcf7ae355bd9d9ed2e6db492595f1a67f61681be/zipp-3.18.2-py3-none-any.whl", - "dce197b859eb796242b0622af1b8beb0a722d52aa2f57133ead08edd5bf5374e", + "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", + "071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", ), # END: maintained by 'bazel run //tools/private/update_deps:update_pip_deps' ] @@ -101,7 +101,6 @@ _GENERIC_WHEEL = """\ package(default_visibility = ["//visibility:public"]) load("@rules_python//python:py_library.bzl", "py_library") -load("@rules_python//python/private:glob_excludes.bzl", "glob_excludes") py_library( name = "lib", @@ -115,7 +114,7 @@ py_library( "**/*.dist-info/RECORD", "BUILD", "WORKSPACE", - ] + glob_excludes.version_dependent_exclusions()), + ]), # This makes this directory a top-level in the python import # search path for anything that depends on this. imports = ["."], diff --git a/python/private/pypi/evaluate_markers.bzl b/python/private/pypi/evaluate_markers.bzl index 4d6a39a1df..34076290db 100644 --- a/python/private/pypi/evaluate_markers.bzl +++ b/python/private/pypi/evaluate_markers.bzl @@ -14,19 +14,8 @@ """A simple function that evaluates markers using a python interpreter.""" -load(":deps.bzl", "record_files") load(":pep508_evaluate.bzl", "evaluate") load(":pep508_requirement.bzl", "requirement") -load(":pypi_repo_utils.bzl", "pypi_repo_utils") - -# Used as a default value in a rule to ensure we fetch the dependencies. -SRCS = [ - # When the version, or any of the files in `packaging` package changes, - # this file will change as well. - record_files["pypi__packaging"], - Label("//python/private/pypi/requirements_parser:resolve_target_platforms.py"), - Label("//python/private/pypi/whl_installer:platform.py"), -] def evaluate_markers(*, requirements, platforms): """Return the list of supported platforms per requirements line. @@ -51,57 +40,3 @@ def evaluate_markers(*, requirements, platforms): ret.setdefault(req_string, []).append(platform_str) return ret - -def evaluate_markers_py(mrctx, *, requirements, python_interpreter, python_interpreter_target, srcs, logger = None): - """Return the list of supported platforms per requirements line. - - Args: - mrctx: repository_ctx or module_ctx. - requirements: {type}`dict[str, list[str]]` of the requirement file lines to evaluate. - python_interpreter: str, path to the python_interpreter to use to - evaluate the env markers in the given requirements files. It will - be only called if the requirements files have env markers. This - should be something that is in your PATH or an absolute path. - python_interpreter_target: Label, same as python_interpreter, but in a - label format. - srcs: list[Label], the value of SRCS passed from the `rctx` or `mctx` to this function. - logger: repo_utils.logger or None, a simple struct to log diagnostic - messages. Defaults to None. - - Returns: - dict of string lists with target platforms - """ - if not requirements: - return {} - - in_file = mrctx.path("requirements_with_markers.in.json") - out_file = mrctx.path("requirements_with_markers.out.json") - mrctx.file(in_file, json.encode(requirements)) - - interpreter = pypi_repo_utils.resolve_python_interpreter( - mrctx, - python_interpreter = python_interpreter, - python_interpreter_target = python_interpreter_target, - ) - - pypi_repo_utils.execute_checked( - mrctx, - op = "ResolveRequirementEnvMarkers({})".format(in_file), - python = interpreter, - arguments = [ - "-m", - "python.private.pypi.requirements_parser.resolve_target_platforms", - in_file, - out_file, - ], - srcs = srcs, - environment = { - "PYTHONHOME": str(interpreter.dirname), - "PYTHONPATH": [ - Label("@pypi__packaging//:BUILD.bazel"), - Label("//:BUILD.bazel"), - ], - }, - logger = logger, - ) - return json.decode(mrctx.read(out_file)) diff --git a/python/private/pypi/extension.bzl b/python/private/pypi/extension.bzl index be1a8e4d03..b79d04c075 100644 --- a/python/private/pypi/extension.bzl +++ b/python/private/pypi/extension.bzl @@ -14,28 +14,30 @@ "pip module extension for use with bzlmod" -load("@bazel_features//:features.bzl", "bazel_features") load("@pythons_hub//:interpreters.bzl", "INTERPRETER_LABELS") load("@pythons_hub//:versions.bzl", "MINOR_MAPPING") load("@rules_python_internal//:rules_python_config.bzl", rp_config = "config") +load("@toml.bzl", "toml") load("//python/private:auth.bzl", "AUTH_ATTRS") load("//python/private:normalize_name.bzl", "normalize_name") +load("//python/private:pyproject_utils.bzl", "read_pyproject", "version_from_requires_python") load("//python/private:repo_utils.bzl", "repo_utils") -load(":evaluate_markers.bzl", EVALUATE_MARKERS_SRCS = "SRCS") load(":hub_builder.bzl", "hub_builder") load(":hub_repository.bzl", "hub_repository", "whl_config_settings_to_json") load(":parse_whl_name.bzl", "parse_whl_name") load(":pep508_env.bzl", "env") load(":pip_repository_attrs.bzl", "ATTRS") load(":platform.bzl", _plat = "platform") +load(":pypi_cache.bzl", "pypi_cache") load(":simpleapi_download.bzl", "simpleapi_download") +load(":unified_hub_repo.bzl", "unified_hub_repo") load(":whl_library.bzl", "whl_library") def _whl_mods_impl(whl_mods_dict): """Implementation of the pip.whl_mods tag class. This creates the JSON files used to modify the creation of different wheels. -""" + """ for hub_name, whl_maps in whl_mods_dict.items(): whl_mods = {} @@ -56,6 +58,128 @@ def _whl_mods_impl(whl_mods_dict): whl_mods = whl_mods, ) +def default_platforms(): + """Return the built-in default platform definitions. + + These provide the platform metadata needed for pip wheel resolution + (whl_abi_tags, whl_platform_tags, config_settings, etc.) across all + common OS/arch combinations. They are always used as the starting point + for build_config; root modules can override individual platforms via + pip.default tags. + + Returns: + A dict of platform name to platform config dicts. + """ + # NOTE @aignas 2025-07-06: we define these platforms to keep backwards compatibility. Whilst we + # stabilize the API this list may be updated with a mention in the CHANGELOG. + + platforms = {} + + # Linux platforms + for cpu in ["x86_64", "aarch64"]: + for freethreaded in ["", "_freethreaded"]: + platform_name = "linux_{}{}".format(cpu, freethreaded) + platforms[platform_name] = { + "arch_name": cpu, + "config_settings": [ + "@platforms//cpu:{}".format(cpu), + "@platforms//os:linux", + "//python/config_settings:_is_py_freethreaded_{}".format( + "yes" if freethreaded else "no", + ), + ], + "env": {"platform_version": "0"}, + "marker": "python_version >= '3.13'" if freethreaded else "", + "name": platform_name, + "os_name": "linux", + "whl_abi_tags": ["cp{major}{minor}t"] if freethreaded else [ + "abi3", + "cp{major}{minor}", + ], + "whl_platform_tags": [ + "linux_{}".format(cpu), + "manylinux_*_{}".format(cpu), + ], + } + + # macOS platforms + for cpu, platform_tag_cpus in { + "aarch64": ["universal2", "arm64"], + "x86_64": ["universal2", "x86_64"], + }.items(): + for freethreaded in ["", "_freethreaded"]: + platform_name = "osx_{}{}".format(cpu, freethreaded) + platforms[platform_name] = { + "arch_name": cpu, + "config_settings": [ + "@platforms//cpu:{}".format(cpu), + "@platforms//os:osx", + "//python/config_settings:_is_py_freethreaded_{}".format( + "yes" if freethreaded else "no", + ), + ], + "env": {"platform_version": "14.0"}, + "marker": "python_version >= '3.13'" if freethreaded else "", + "name": platform_name, + "os_name": "osx", + "whl_abi_tags": ["cp{major}{minor}t"] if freethreaded else [ + "abi3", + "cp{major}{minor}", + ], + "whl_platform_tags": [ + "macosx_*_{}".format(suffix) + for suffix in platform_tag_cpus + ], + } + + # Windows x86_64 platforms + for freethreaded in ["", "_freethreaded"]: + platform_name = "windows_x86_64{}".format(freethreaded) + platforms[platform_name] = { + "arch_name": "x86_64", + "config_settings": [ + "@platforms//cpu:x86_64", + "@platforms//os:windows", + "//python/config_settings:_is_py_freethreaded_{}".format( + "yes" if freethreaded else "no", + ), + ], + "env": {"platform_version": "0"}, + "marker": "python_version >= '3.13'" if freethreaded else "", + "name": platform_name, + "os_name": "windows", + "whl_abi_tags": ["cp{major}{minor}t"] if freethreaded else [ + "abi3", + "cp{major}{minor}", + ], + "whl_platform_tags": ["win_amd64"], + } + + # Windows aarch64 platforms + for freethreaded in ["", "_freethreaded"]: + platform_name = "windows_aarch64{}".format(freethreaded) + platforms[platform_name] = { + "arch_name": "aarch64", + "config_settings": [ + "@platforms//cpu:aarch64", + "@platforms//os:windows", + "//python/config_settings:_is_py_freethreaded_{}".format( + "yes" if freethreaded else "no", + ), + ], + "env": {"platform_version": "0"}, + "marker": "python_version >= '3.13'" if freethreaded else "python_version >= '3.11'", + "name": platform_name, + "os_name": "windows", + "whl_abi_tags": ["cp{major}{minor}t"] if freethreaded else [ + "abi3", + "cp{major}{minor}", + ], + "whl_platform_tags": ["win_arm64"], + } + + return platforms + def _configure(config, *, override = False, **kwargs): """Set the value in the config if the value is provided""" env = kwargs.get("env") @@ -71,25 +195,38 @@ def _configure(config, *, override = False, **kwargs): def build_config( *, module_ctx, - enable_pipstar): + enable_pipstar_extract): """Parse 'configure' and 'default' extension tags Args: module_ctx: {type}`module_ctx` module context. - enable_pipstar: {type}`bool` a flag to enable dropping Python dependency for - evaluation of the extension. + enable_pipstar_extract: {type}`bool | None` a flag to also not pass Python + interpreter to `whl_library` when possible. Returns: A struct with the configuration. """ + default_hub = None defaults = { - "platforms": {}, + "platforms": default_platforms(), + "python_version": None, } for mod in module_ctx.modules: if not (mod.is_root or mod.name == "rules_python"): continue for tag in mod.tags.default: + if tag.default_hub: + if mod.is_root: + if default_hub: + fail("Duplicate pip.default tag: only one explicit default PyPI hub is allowed.") + default_hub = tag.default_hub + pyproject_toml = tag.pyproject_toml + if pyproject_toml: + pyproject = read_pyproject(module_ctx, pyproject_toml) + if pyproject.requires_python: + defaults["python_version"] = version_from_requires_python(pyproject.requires_python) + platform = tag.platform if platform: specific_config = defaults["platforms"].setdefault(platform, {}) @@ -112,6 +249,7 @@ def build_config( _configure( defaults, override = mod.is_root, + index_url = tag.index_url, # extra values that we just add auth_patterns = tag.auth_patterns, netrc = tag.netrc, @@ -122,27 +260,31 @@ def build_config( return struct( auth_patterns = defaults.get("auth_patterns", {}), + default_hub = default_hub, + index_url = defaults.get("index_url", "https://pypi.org/simple").rstrip("/"), netrc = defaults.get("netrc", None), + python_version = defaults.get("python_version", None), platforms = { name: _plat(**values) for name, values in defaults["platforms"].items() }, - enable_pipstar = enable_pipstar, + enable_pipstar_extract = enable_pipstar_extract, + toml_decode = toml.decode, ) def parse_modules( module_ctx, _fail = fail, simpleapi_download = simpleapi_download, - enable_pipstar = False, + enable_pipstar_extract = False, **kwargs): """Implementation of parsing the tag classes for the extension and return a struct for registering repositories. Args: module_ctx: {type}`module_ctx` module context. simpleapi_download: Used for testing overrides - enable_pipstar: {type}`bool` a flag to enable dropping Python dependency for - evaluation of the extension. + enable_pipstar_extract: {type}`bool` a flag to enable dropping Python dependency for + extracting wheels. _fail: {type}`function` the failure function, mainly for testing. **kwargs: Extra arguments passed to the hub_builder. @@ -180,7 +322,7 @@ You cannot use both the additive_build_content and additive_build_content_file a srcs_exclude_glob = whl_mod.srcs_exclude_glob, ) - config = build_config(module_ctx = module_ctx, enable_pipstar = enable_pipstar) + config = build_config(module_ctx = module_ctx, enable_pipstar_extract = enable_pipstar_extract) # TODO @aignas 2025-06-03: Merge override API with the builder? _overriden_whl_set = {} @@ -215,12 +357,54 @@ You cannot use both the additive_build_content and additive_build_content_file a # Used to track all the different pip hubs and the spoke pip Python # versions. + # dict[str repo, HubBuilder] + # See `hub_builder.bzl%hub_builder()` for `HubBuilder` pip_hub_map = {} - simpleapi_cache = {} + simpleapi_cache = pypi_cache(mctx = module_ctx) + + is_pypi_hub_reserved = module_ctx.getenv("RULES_PYTHON_PYPI_HUB_RESERVED", "0") == "1" + renamed_default_hub = None for mod in module_ctx.modules: for pip_attr in mod.tags.parse: + python_version = pip_attr.python_version + if not python_version and pip_attr.pyproject_toml: + pyproject = read_pyproject(module_ctx, pip_attr.pyproject_toml) + if pyproject.requires_python: + python_version = version_from_requires_python(pyproject.requires_python) + python_version = python_version or config.python_version + if not python_version: + _fail("pip.parse() requires one of `python_version`, `pyproject_toml`, or `pip.default(pyproject_toml=...)` to be set") + hub_name = pip_attr.hub_name + if hub_name == "pypi": + if is_pypi_hub_reserved: + renamed_name = mod.name + "_pypi" + print( + ( + "WARNING: The PyPI hub name 'pypi' is reserved " + + "(module '{}'). The hub was renamed to '{}'. " + + "Please rename your hub." + ).format( + mod.name, + renamed_name, + ), + ) # buildifier: disable=print + hub_name = renamed_name + if not renamed_default_hub: + renamed_default_hub = hub_name + else: + print( + ( + "WARNING: The PyPI hub name 'pypi' is reserved " + + "(module '{}'). Please rename your hub, otherwise " + + "a future release will rename it to '{}_pypi'." + ).format( + mod.name, + mod.name, + ), + ) # buildifier: disable=print + if hub_name not in pip_hub_map: builder = hub_builder( name = hub_name, @@ -231,11 +415,10 @@ You cannot use both the additive_build_content and additive_build_content_file a simpleapi_cache = simpleapi_cache, # TODO @aignas 2025-09-06: do not use kwargs minor_mapping = kwargs.get("minor_mapping", MINOR_MAPPING), - evaluate_markers_fn = kwargs.get("evaluate_markers", None), available_interpreters = kwargs.get("available_interpreters", INTERPRETER_LABELS), - logger = repo_utils.logger(module_ctx, "pypi:hub:" + hub_name), + logger = repo_utils.logger(module_ctx, "pypi:hub:" + hub_name, mod = mod), ) - pip_hub_map[pip_attr.hub_name] = builder + pip_hub_map[hub_name] = builder elif pip_hub_map[hub_name].module_name != mod.name: # We cannot have two hubs with the same name in different # modules. @@ -251,13 +434,23 @@ You cannot use both the additive_build_content and additive_build_content_file a )) else: - builder = pip_hub_map[pip_attr.hub_name] + builder = pip_hub_map[hub_name] builder.pip_parse( module_ctx, pip_attr = pip_attr, + python_version = python_version, ) + # dict[str package, dict[str, None] extra_targets] + declared_deps = {} + for mod in module_ctx.modules: + for dep_attr in mod.tags.dep: + name = normalize_name(dep_attr.name) + targets = declared_deps.setdefault(name, {}) + for target in dep_attr.extra_targets: + targets[target] = None + # Keeps track of all the hub's whl repos across the different versions. # dict[hub, dict[whl, dict[version, str pip]]] # Where hub, whl, and pip are the repo names @@ -282,8 +475,11 @@ You cannot use both the additive_build_content and additive_build_content_file a return struct( config = config, + declared_deps = declared_deps, + default_hub = config.default_hub or renamed_default_hub, exposed_packages = exposed_packages, extra_aliases = extra_aliases, + facts = simpleapi_cache.get_facts(), hub_group_map = hub_group_map, hub_whl_map = hub_whl_map, whl_libraries = whl_libraries, @@ -297,6 +493,50 @@ You cannot use both the additive_build_content and additive_build_content_file a }, ) +def _create_unified_hub_repo(mods): + if "pypi" in mods.hub_whl_map: + return + + hubs = sorted(mods.hub_whl_map.keys()) + if mods.default_hub and mods.default_hub not in hubs: + fail("default_hub '%s' is not a defined PyPI hub. Available hubs: %s" % (mods.default_hub, ", ".join(hubs))) + + packages = {} + extra_aliases = {} + + for hub_name in hubs: + for pkg_name in mods.exposed_packages.get(hub_name, []): + norm_pkg = normalize_name(pkg_name) + if norm_pkg not in packages: + packages[norm_pkg] = [] + if hub_name not in packages[norm_pkg]: + packages[norm_pkg].append(hub_name) + + extra = mods.extra_aliases.get(hub_name, {}).get(norm_pkg, []) + for alias_name in extra: + qual_alias = "%s:%s" % (norm_pkg, alias_name) + if qual_alias not in extra_aliases: + extra_aliases[qual_alias] = [] + if hub_name not in extra_aliases[qual_alias]: + extra_aliases[qual_alias].append(hub_name) + + for norm_pkg, extra_targets in mods.declared_deps.items(): + if norm_pkg not in packages: + packages[norm_pkg] = [] + + for target_name in extra_targets: + qual_alias = "%s:%s" % (norm_pkg, target_name) + if qual_alias not in extra_aliases: + extra_aliases[qual_alias] = [] + + unified_hub_repo( + name = "pypi", + default_hub = mods.default_hub or (hubs[0] if hubs else ""), + extra_aliases = extra_aliases, + hubs = hubs, + packages = packages, + ) + def _pip_impl(module_ctx): """Implementation of a class tag that creates the pip hub and corresponding pip spoke whl repositories. @@ -363,7 +603,10 @@ def _pip_impl(module_ctx): module_ctx: module contents """ - mods = parse_modules(module_ctx, enable_pipstar = rp_config.enable_pipstar) + mods = parse_modules( + module_ctx, + enable_pipstar_extract = rp_config.bazel_8_or_later, + ) # Build all of the wheel modifications if the tag class is called. _whl_mods_impl(mods.whl_mods) @@ -385,13 +628,17 @@ def _pip_impl(module_ctx): groups = mods.hub_group_map.get(hub_name), ) - if bazel_features.external_deps.extension_metadata_has_reproducible: - # NOTE @aignas 2025-04-15: this is set to be reproducible, because the - # results after calling the PyPI index should be reproducible on each - # machine. - return module_ctx.extension_metadata(reproducible = True) + _create_unified_hub_repo(mods) + + # The code is smart to not return facts if we don't support the mechanism for that. + # Hence we should not pass it to the metadata + if mods.facts: + return module_ctx.extension_metadata( + reproducible = True, + facts = mods.facts, + ) else: - return None + return module_ctx.extension_metadata(reproducible = True) _default_attrs = { "arch_name": attr.string( @@ -405,12 +652,14 @@ Either this or {attr}`env` `platform_machine` key should be specified. """, ), "config_settings": attr.label_list( - mandatory = True, doc = """\ The list of labels to `config_setting` targets that need to be matched for the platform to be -selected. +selected. Mandatory if platform is specified. """, ), + "default_hub": attr.string( + doc = "The name of the concrete PyPI hub to use by default when {flag}`--venv=auto`.", + ), "env": attr.string_dict( doc = """\ The values to use for environment markers when evaluating an expression. @@ -429,10 +678,30 @@ Supported keys: * `platform_system`, defaults to a value inferred from the {attr}`os_name`. * `platform_version`, defaults to `0`. * `sys_platform`, defaults to a value inferred from the {attr}`os_name`. +""", + ), + "index_url": attr.string( + doc = """\ +The index URL to use as a default when downloading packages from PyPI. This is used if nothing is +specified via `--index-url` or `--extra-index-url` parameters in the `requirements.txt` file or via +the {attr}`pip.parse.extra_pip_args`. + +This value is going to be subject to `envsubst` substitutions if necessary, look at the +{attr}`pip.parse.envsubst` documentation for more information.. + +The indexes must support Simple API as described here: +https://packaging.python.org/en/latest/specifications/simple-repository-api/ + +Index metadata will be used to get `sha256` values for packages even if the +`sha256` values are not present in the requirements.txt lock file. + +Defaults to `https://pypi.org/simple`. -::::{note} -This is only used if the {envvar}`RULES_PYTHON_ENABLE_PIPSTAR` is enabled. -:::: +:::{versionadded} 2.0.0 +This has been added as a replacement for +{obj}`pip.parse.experimental_index_url` and +{obj}`pip.parse.experimental_extra_index_urls`. +::: """, ), "marker": attr.string( @@ -467,6 +736,23 @@ If you are defining custom platforms in your project and don't want things to cl [isolation] feature. [isolation]: https://bazel.build/rules/lib/globals/module#use_extension.isolate +""", + ), + "pyproject_toml": attr.label( + mandatory = False, + doc = """\ +Label pointing to pyproject.toml file to read the default Python version from. +When specified, reads the `requires-python` field from pyproject.toml and uses +it as the default python_version for all `pip.parse()` calls that don't +explicitly specify one. + +:::{note} +The version must be specified as `==X.Y.Z` (exact version with full semver). +This is designed to work with dependency management tools like Renovate. +::: + +:::{versionadded} VERSION_NEXT_FEATURE +::: """, ), "whl_abi_tags": attr.string_list( @@ -550,17 +836,11 @@ def _pip_parse_ext_attrs(**kwargs): attrs = dict({ "experimental_extra_index_urls": attr.string_list( doc = """\ -The extra index URLs to use for downloading wheels using bazel downloader. -Each value is going to be subject to `envsubst` substitutions if necessary. - -The indexes must support Simple API as described here: -https://packaging.python.org/en/latest/specifications/simple-repository-api/ +May be removed in future releases. -This is equivalent to `--extra-index-urls` `pip` option. - -:::{versionchanged} 1.1.0 -Starting with this version we will iterate over each index specified until -we find metadata for all references distributions. +:::{versionchanged} 2.0.0 +This is deprecated, please use {obj}`pip.default.index_url` or pass the `--index-url` parameter via the +lock-file or {obj}`pip.parse.extra_pip_args`. ::: """, default = [], @@ -568,25 +848,11 @@ we find metadata for all references distributions. "experimental_index_url": attr.string( default = kwargs.get("experimental_index_url", ""), doc = """\ -The index URL to use for downloading wheels using bazel downloader. This value is going -to be subject to `envsubst` substitutions if necessary. - -The indexes must support Simple API as described here: -https://packaging.python.org/en/latest/specifications/simple-repository-api/ - -In the future this could be defaulted to `https://pypi.org` when this feature becomes -stable. +May be removed in future releases. -This is equivalent to `--index-url` `pip` option. - -:::{versionchanged} 0.37.0 -If {attr}`download_only` is set, then `sdist` archives will be discarded and `pip.parse` will -operate in wheel-only mode. -::: - -:::{versionchanged} 1.4.0 -Index metadata will be used to deduct `sha256` values for packages even if the -`sha256` values are not present in the requirements.txt lock file. +:::{versionchanged} 2.0.0 +This is deprecated, please use {obj}`pip.default.index_url` or pass the `--index-url` parameter via the +lock-file or {obj}`pip.parse.extra_pip_args`. ::: """, ), @@ -612,6 +878,11 @@ https://packaging.python.org/en/latest/specifications/simple-repository-api/ doc = """ The name of the repo pip dependencies will be accessible from. +The hub name `"pypi"` is reserved for the automatically generated +[Unified @pypi Hub](unified-pypi-hub) repository. Please choose a different name +for your concrete hubs. See [Unified @pypi Hub](unified-pypi-hub) for how to +handle collisions. + This name must be unique between modules; unless your module is guaranteed to always be the root module, it's highly recommended to include your module name in the hub name. Repo mapping, `use_repo(..., pip="my_modules_pip_deps")`, can @@ -627,6 +898,12 @@ is not required. Each hub is a separate resolution of pip dependencies. This means if different programs need different versions of some library, separate hubs can be created, and each program can use its respective hub's targets. Targets from different hubs should not be used together. + +:::{versionchanged} 2.2.0 +Using the hub name `"pypi"` is deprecated and is changed to +`{module_name}_pypi` depending on the +{envvar}`RULES_PYTHON_PYPI_HUB_RESERVED` environment variable. +::: """, ), "parallel_download": attr.bool( @@ -646,8 +923,23 @@ find in case extra indexes are specified. """, default = True, ), + "pyproject_toml": attr.label( + mandatory = False, + doc = """\ +Label pointing to a pyproject.toml file to read the Python version from. +When specified, the `requires-python` field is used as the `python_version` +for this `pip.parse()` call, unless `python_version` is set explicitly. + +:::{note} +The version must be specified as `==X.Y.Z` (exact version with full semver). +::: + +:::{versionadded} VERSION_NEXT_FEATURE +::: +""", + ), "python_version": attr.string( - mandatory = True, + mandatory = False, doc = """ The Python version the dependencies are targetting, in Major.Minor format (e.g., "3.11") or patch level granularity (e.g. "3.11.1"). @@ -655,6 +947,15 @@ The Python version the dependencies are targetting, in Major.Minor format If an interpreter isn't explicitly provided (using `python_interpreter` or `python_interpreter_target`), then the version specified here must have a corresponding `python.toolchain()` configured. + +:::{seealso} +The {obj}`pyproject_toml` attribute for getting the version from a project file. +::: + +:::{versionchanged} VERSION_NEXT_FEATURE +No longer mandatory if the {obj}`pyproject_toml` attribute or +{obj}`pip.default.pyproject_toml` is specified. +::: """, ), "simpleapi_skip": attr.string_list( @@ -667,6 +968,32 @@ EXPERIMENTAL: this may be removed without notice. :::{versionadded} 1.4.0 ::: +""", + ), + "target_platforms": attr.string_list( + default = [], + doc = """\ +The list of platforms for which we would evaluate the requirements files. If you need to be able to +only evaluate for a particular platform (e.g. "linux_x86_64"), then put it in here. + +If you want `freethreaded` variant, then you can use `_freethreaded` suffix as `rules_python` is +defining target platforms for these variants in its `MODULE.bazel` file. The identifiers for this +function in general are the same as used in the {obj}`pip.default.platform` attribute. + +If you only care for the host platform and do not have a usecase to cross-build, then you can put in +a string `"{os}_{arch}"` as the value here. You could also use `"{os}_{arch}_freethreaded"` as well. + +:::{include} /_includes/experimental_api.md +::: + +:::{versionadded} 1.8.0 +::: +""", + ), + "uv_lock": attr.label( + doc = """\ +(label, optional): A label pointing to the uv.lock file. If provided, +the uv.lock file will be used as the primary source for package metadata. """, ), "whl_modifications": attr.label_keyed_string_dict( @@ -674,13 +1001,6 @@ EXPERIMENTAL: this may be removed without notice. doc = """\ A dict of labels to wheel names that is typically generated by the whl_modifications. The labels are JSON config files describing the modifications. -""", - ), - "_evaluate_markers_srcs": attr.label_list( - default = EVALUATE_MARKERS_SRCS, - doc = """\ -The list of labels to use as SRCS for the marker evaluation code. This ensures that the -code will be re-evaluated when any of files in the default changes. """, ), }, **ATTRS) @@ -773,7 +1093,33 @@ Apply any overrides (e.g. patches) to a given Python distribution defined by other tags in this extension.""", ) +_dep_tag = tag_class( + attrs = { + "extra_targets": attr.string_list( + doc = """\ +A list of extra target names in the package that are expected to be available. +See {obj}`pip.parse.extra_hub_aliases`. +""", + default = [], + ), + "name": attr.string( + doc = "The name of a pypi package. Note that the name is normalized.", + mandatory = True, + ), + }, + doc = """\ +Declare an abstract PyPI dependency to ensure its target structure exists in the unified hub. + +This is useful for targets or rules that need to depend on a package (e.g., `@pypi//numpy`) +but do not want to force a specific version or concrete requirements lock file on their +consumers. The concrete version and implementation must be provided by downstreams calling +`pip.parse`. If they are not, the target will still be defined, but it will result in an +execution-phase error when built. +""", +) + pypi = module_extension( + environ = ["RULES_PYTHON_PYPI_HUB_RESERVED"], doc = """\ This extension is used to make dependencies from pip available. @@ -787,6 +1133,14 @@ can be made to configure different Python versions, and will be grouped by the `hub_name` argument. This allows the same logical name, e.g. `@pip//numpy` to automatically resolve to different, Python version-specific, libraries. +A unified `@pypi` proxy repository is always generated (unless a hub is +explicitly named "pypi") to route dependencies dynamically. See +[Unified @pypi Hub](unified-pypi-hub) for details. + +Environment Variables: +- `RULES_PYTHON_PYPI_HUB_RESERVED`: Enable fallback renaming for reserved hub name collisions. + See the {envvar}`RULES_PYTHON_PYPI_HUB_RESERVED` documentation for details. + pip.whl_mods: This tag class is used to help create JSON files to describe modifications to the BUILD files for wheels. @@ -798,9 +1152,10 @@ the BUILD files for wheels. doc = """\ This tag class allows for more customization of how the configuration for the hub repositories is built. +It can also be used to designate the default hub for the automatically +generated [Unified @pypi Hub](unified-pypi-hub) using the `default_hub` +attribute. -:::{include} /_includes/experimtal_api.md -::: :::{seealso} The [environment markers][environment_markers] specification for the explanation of the @@ -813,6 +1168,7 @@ terms used in this extension. ::: """, ), + "dep": _dep_tag, "override": _override_tag, "parse": tag_class( attrs = _pip_parse_ext_attrs(), @@ -821,6 +1177,9 @@ This tag class is used to create a pip hub and all of the spokes that are part o This tag class reuses most of the attributes found in {bzl:obj}`pip_parse`. The exception is it does not use the arg 'repo_prefix'. We set the repository prefix for the user and the alias arg is always True in bzlmod. + +You can use the automatically generated [Unified @pypi Hub](unified-pypi-hub) +repository to route package dependencies dynamically at build time. """, ), "whl_mods": tag_class( diff --git a/python/private/pypi/flags.bzl b/python/private/pypi/flags.bzl index f88690d843..36cb5373ff 100644 --- a/python/private/pypi/flags.bzl +++ b/python/private/pypi/flags.bzl @@ -18,9 +18,8 @@ NOTE: The transitive loads of this should be kept minimal. This avoids loading unnecessary files when all that are needed are flag definitions. """ -load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo", "string_flag") +load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") load("//python/private:common_labels.bzl", "labels") -load("//python/private:enum.bzl", "enum") load(":env_marker_info.bzl", "EnvMarkerInfo") load( ":pep508_env.bzl", @@ -31,87 +30,16 @@ load( "sys_platform_select_map", ) -# Determines if we should use whls for third party -# -# buildifier: disable=name-conventions -UseWhlFlag = enum( - # Automatically decide the effective value based on environment, target - # platform and the presence of distributions for a particular package. - AUTO = "auto", - # Do not use `sdist` and fail if there are no available whls suitable for the target platform. - ONLY = "only", - # Do not use whl distributions and instead build the whls from `sdist`. - NO = "no", -) - -# Determines whether universal wheels should be preferred over arch platform specific ones. -# -# buildifier: disable=name-conventions -UniversalWhlFlag = enum( - # Prefer platform-specific wheels over universal wheels. - ARCH = "arch", - # Prefer universal wheels over platform-specific wheels. - UNIVERSAL = "universal", -) - -_STRING_FLAGS = [ - "dist", - "whl_plat", - "whl_plat_py3", - "whl_plat_py3_abi3", - "whl_plat_pycp3x", - "whl_plat_pycp3x_abi3", - "whl_plat_pycp3x_abicp", - "whl_py3", - "whl_py3_abi3", - "whl_pycp3x", - "whl_pycp3x_abi3", - "whl_pycp3x_abicp", -] - -INTERNAL_FLAGS = [ - "whl", -] + _STRING_FLAGS - def define_pypi_internal_flags(name): """define internal PyPI flags used in PyPI hub repository by pkg_aliases. Args: name: not used """ - for flag in _STRING_FLAGS: - string_flag( - name = "_internal_pip_" + flag, - build_setting_default = "", - values = [""], - visibility = ["//visibility:public"], - ) - - _allow_wheels_flag( - name = "_internal_pip_whl", - visibility = ["//visibility:public"], - ) - _default_env_marker_config( name = "_pip_env_marker_default_config", ) -def _allow_wheels_flag_impl(ctx): - input = ctx.attr._setting[BuildSettingInfo].value - value = "yes" if input in ["auto", "only"] else "no" - return [config_common.FeatureFlagInfo(value = value)] - -_allow_wheels_flag = rule( - implementation = _allow_wheels_flag_impl, - attrs = { - "_setting": attr.label(default = labels.PIP_WHL), - }, - doc = """ -This rule allows us to greatly reduce the number of config setting targets at no cost even -if we are duplicating some of the functionality of the `native.config_setting`. -""", -) - def _default_env_marker_config(**kwargs): _env_marker_config( os_name = select(os_name_select_map), diff --git a/python/private/pypi/generate_whl_library_build_bazel.bzl b/python/private/pypi/generate_whl_library_build_bazel.bzl index 3764e720c0..71eab26c0e 100644 --- a/python/private/pypi/generate_whl_library_build_bazel.bzl +++ b/python/private/pypi/generate_whl_library_build_bazel.bzl @@ -21,16 +21,13 @@ _RENDER = { "copy_files": render.dict, "data": render.list, "data_exclude": render.list, - "dependencies": render.list, - "dependencies_by_platform": lambda x: render.dict(x, value_repr = render.list), - "entry_points": render.dict, + "entry_points": render.dict_dict, "extras": render.list, "group_deps": render.list, "include": str, "requires_dist": render.list, "srcs_exclude": render.list, "tags": render.list, - "target_platforms": render.list, } # NOTE @aignas 2024-10-25: We have to keep this so that files in @@ -41,6 +38,12 @@ _TEMPLATE = """\ package(default_visibility = ["//visibility:public"]) +package_metadata( + name = "package_metadata", + purl = {purl}, + visibility = ["//:__subpackages__"], +) + {fn}( {kwargs} ) @@ -49,13 +52,17 @@ package(default_visibility = ["//visibility:public"]) def generate_whl_library_build_bazel( *, annotation = None, - default_python_version = None, + config_load, + purl = None, + requires_dist = [], **kwargs): """Generate a BUILD file for an unzipped Wheel Args: annotation: The annotation for the build file. - default_python_version: The python version to use to parse the METADATA. + config_load: {type}`str` The location from where to load the config. + purl: The purl. + requires_dist: {type}`list[str]` The list of dependencies from the METADATA file. **kwargs: Extra args serialized to be passed to the {obj}`whl_library_targets`. @@ -63,40 +70,18 @@ def generate_whl_library_build_bazel( A complete BUILD file as a string """ - loads = [] - if kwargs.get("tags"): - fn = "whl_library_targets" - - # legacy path - unsupported_args = [ - "requires", - "metadata_name", - "metadata_version", - "include", - ] - else: - fn = "whl_library_targets_from_requires" - unsupported_args = [ - "dependencies", - "dependencies_by_platform", - "target_platforms", - "default_python_version", - ] - dep_template = kwargs.get("dep_template") - loads.append( - """load("{}", "{}")""".format( - dep_template.format( - name = "", - target = "config.bzl", - ), - "whl_map", - ), - ) - kwargs["include"] = "whl_map" + loads = [ + """load("@package_metadata//rules:package_metadata.bzl", "package_metadata")""", + ] - for arg in unsupported_args: - if kwargs.get(arg): - fail("BUG, unsupported arg: '{}'".format(arg)) + fn = "whl_library_targets_from_requires" + if not requires_dist: + # no deps, we can leave the extra loads out + pass + else: + loads.append("""load("{}", "{}")""".format(config_load, "packages")) + kwargs["include"] = "packages" + kwargs["requires_dist"] = requires_dist loads.extend([ """load("@rules_python//python/private/pypi:whl_library_targets.bzl", "{}")""".format(fn), @@ -111,8 +96,6 @@ def generate_whl_library_build_bazel( kwargs["srcs_exclude"] = annotation.srcs_exclude_glob if annotation.additive_build_content: additional_content.append(annotation.additive_build_content) - if default_python_version: - kwargs["default_python_version"] = default_python_version contents = "\n".join( [ @@ -123,6 +106,7 @@ def generate_whl_library_build_bazel( "{} = {},".format(k, _RENDER.get(k, repr)(v)) for k, v in sorted(kwargs.items()) ])), + purl = repr(purl), ), ] + additional_content, ) diff --git a/python/private/pypi/group_library.bzl b/python/private/pypi/group_library.bzl deleted file mode 100644 index ff800e2f18..0000000000 --- a/python/private/pypi/group_library.bzl +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright 2024 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. - -"""group_library implementation for WORKSPACE setups.""" - -load(":generate_group_library_build_bazel.bzl", "generate_group_library_build_bazel") - -def _group_library_impl(rctx): - build_file_contents = generate_group_library_build_bazel( - repo_prefix = rctx.attr.repo_prefix, - groups = rctx.attr.groups, - ) - rctx.file("BUILD.bazel", build_file_contents) - -group_library = repository_rule( - attrs = { - "groups": attr.string_list_dict( - doc = "A mapping of group names to requirements within that group.", - ), - "repo_prefix": attr.string( - doc = "Prefix used for the whl_library created components of each group", - ), - }, - implementation = _group_library_impl, - doc = """ -Create a package containing only wrapper py_library and whl_library rules for implementing dependency groups. -This is an implementation detail of dependency groups and should not be used alone. - """, -) diff --git a/python/private/pypi/hub_builder.bzl b/python/private/pypi/hub_builder.bzl index b6088e4ded..01dc8494d6 100644 --- a/python/private/pypi/hub_builder.bzl +++ b/python/private/pypi/hub_builder.bzl @@ -1,11 +1,13 @@ """A hub repository builder for incrementally building the hub configuration.""" +load("//python/private:envsubst.bzl", "envsubst") load("//python/private:full_version.bzl", "full_version") load("//python/private:normalize_name.bzl", "normalize_name") +load("//python/private:repo_utils.bzl", "repo_utils") +load("//python/private:text_util.bzl", "render") load("//python/private:version.bzl", "version") load("//python/private:version_label.bzl", "version_label") load(":attrs.bzl", "use_isolated") -load(":evaluate_markers.bzl", "evaluate_markers_py", evaluate_markers_star = "evaluate_markers") load(":parse_requirements.bzl", "parse_requirements") load(":pep508_env.bzl", "env") load(":pep508_evaluate.bzl", "evaluate") @@ -27,9 +29,8 @@ def hub_builder( minor_mapping, available_interpreters, simpleapi_download_fn, - evaluate_markers_fn, logger, - simpleapi_cache = {}): + simpleapi_cache): """Return a hub builder instance Args: @@ -38,7 +39,6 @@ def hub_builder( config: The platform configuration. whl_overrides: {type}`dict[str, struct]` - per-wheel overrides. minor_mapping: {type}`dict[str, str]` the mapping between minor and full versions. - evaluate_markers_fn: the override function used to evaluate the markers. available_interpreters: {type}`dict[str, Label]` The dictionary of available interpreters that have been registered using the `python` bzlmod extension. The keys are in the form `python_{snake_case_version}_host`. This is to be @@ -57,22 +57,44 @@ def hub_builder( build = lambda: _build(self), pip_parse = lambda *a, **k: _pip_parse(self, *a, **k), - # build output + # Build output related + + # The set of package names to expose. Dict acting as set + # dict[str package, None] _exposed_packages = {}, # modified by _add_exposed_packages + # Map of the per-package aliases. + # The nested dict is a dict acting as a set. + # dict[str whl_name, dict[str alias_name, bool]] _extra_aliases = {}, # modified by _add_extra_aliases + # dict[str group_name, list[str]] _group_map = {}, # modified by _add_group_map + # Mapping of whl_library repo names and their kwargs. + # dict[str repo_name, dict[str, object] kwargs] _whl_libraries = {}, # modified by _add_whl_library + # Map of repos and their config settings, and repo the config + # setting originated from. + # dict[str whl_name, dict[str config_setting, str repo_name]] _whl_map = {}, # modified by _add_whl_library - # internal + + # Internal + + # dict[str python_version, dict[str platform, PlatformInfo]] + # where `PlatformInfo` is from `_platforms()` _platforms = {}, + # Supplemental index of `_group_map` + # dict[str whl_name, str group_name] _group_name_by_whl = {}, + # Functions to download according to the config + # dict[str python_version, callable] _get_index_urls = {}, + # Tells whether to use the downloader for a package. + # dict[str python_version, dict[str package_name, bool use_downloader]] _use_downloader = {}, _simpleapi_cache = simpleapi_cache, - # instance constants + + # Instance constants passed in by callers _config = config, _whl_overrides = whl_overrides, - _evaluate_markers_fn = evaluate_markers_fn, _logger = logger, _minor_mapping = minor_mapping, _available_interpreters = available_interpreters, @@ -85,24 +107,45 @@ def hub_builder( ### PUBLIC methods def _build(self): + ret = struct( + whl_map = {}, + group_map = {}, + extra_aliases = {}, + exposed_packages = [], + whl_libraries = {}, + ) + if self._logger.failed(): + return ret + whl_map = {} for key, settings in self._whl_map.items(): for setting, repo in settings.items(): whl_map.setdefault(key, {}).setdefault(repo, []).append(setting) return struct( + # The config settings for matching repo spokes. + # dict[str repo_name, dict[str repo_name, list[str]]] whl_map = whl_map, + # Maps a wheel to a list of groups + # dict[str group_name, list[str]] group_map = self._group_map, + # The per-package aliases for the hub to create. + # dict[str package, list[str]] extra_aliases = { whl: sorted(aliases) for whl, aliases in self._extra_aliases.items() }, + # The list of exposed packages in the hub. + # list[str] exposed_packages = sorted(self._exposed_packages), + + # Mapping of whl_library repo names and their kwargs. + # dict[str repo_name, dict[str, object] kwargs] whl_libraries = self._whl_libraries, ) -def _pip_parse(self, module_ctx, pip_attr): - python_version = pip_attr.python_version +def _pip_parse(self, module_ctx, pip_attr, python_version = None): + python_version = python_version or pip_attr.python_version if python_version in self._platforms: fail(( "Duplicate pip python version '{version}' for hub " + @@ -114,24 +157,55 @@ def _pip_parse(self, module_ctx, pip_attr): version = python_version, )) - self._platforms[python_version] = _platforms( - python_version = python_version, + full_python_version = full_version( + version = python_version, minor_mapping = self._minor_mapping, + fail_on_err = False, + ) + if not full_python_version: + # NOTE @aignas 2025-11-18: If the python version is not present in our + # minor_mapping, then we will not register any packages and then the + # select in the hub repository will fail, which will prompt the user to + # configure the toolchain correctly and move forward. + self._logger.info(lambda: ( + "Ignoring pip python version '{version}' for hub " + + "'{hub}' in module '{module}' because there is no registered " + + "toolchain for it." + ).format( + hub = self.name, + module = self.module_name, + version = python_version, + )) + return + + _set_get_index_urls(self, module_ctx, pip_attr) + self._platforms[python_version] = _platforms( + module_ctx, + python_version = full_python_version, config = self._config, + target_platforms = pip_attr.target_platforms or ["{os}_{arch}"], ) - _set_get_index_urls(self, pip_attr) _add_group_map(self, pip_attr.experimental_requirement_cycles) _add_extra_aliases(self, pip_attr.extra_hub_aliases) _create_whl_repos( self, module_ctx, pip_attr = pip_attr, + python_version = python_version, + enable_pipstar_extract = bool(self._config.enable_pipstar_extract or self._get_index_urls.get(python_version)), ) ### end of PUBLIC methods ### setters for build outputs def _add_exposed_packages(self, exposed_packages): + """Add packages that are exposed. + + Args: + self: implicitly added + exposed_packages: {type}`dict[str package, None]` a dict acting as + a set. The set of packages that should be exposed. + """ if self._exposed_packages: intersection = {} for pkg in exposed_packages: @@ -144,6 +218,13 @@ def _add_exposed_packages(self, exposed_packages): self._exposed_packages.update(exposed_packages) def _add_group_map(self, group_map): + """Adds a group mapping for cycle breaking. + + Args: + self: implicitly added. + group_map: {type}`dict[str name, list[str]]` + """ + # TODO @aignas 2024-04-05: how do we support different requirement # cycles for different abis/oses? For now we will need the users to # assume the same groups across all versions/platforms until we start @@ -163,42 +244,95 @@ def _add_group_map(self, group_map): }) def _add_extra_aliases(self, extra_hub_aliases): + """Adds per-package aliases to the hub. + + Args: + self: Implicitly added + extra_hub_aliases: {type}`dict[str package, list[str]]` Alias target + names to add to a package's hub BUILD file. + """ for whl_name, aliases in extra_hub_aliases.items(): self._extra_aliases.setdefault(whl_name, {}).update( {alias: True for alias in aliases}, ) +def _diff_dict(first, second): + """A simple utility to shallow compare dictionaries. + + Args: + first: The first dictionary to compare. + second: The second dictionary to compare. + + Returns: + A dictionary containing the differences, with keys "common", "different", + "extra", and "missing", or None if the dictionaries are identical. + """ + missing = {} + extra = { + key: value + for key, value in second.items() + if key not in first + } + common = {} + different = {} + + for key, value in first.items(): + if key not in second: + missing[key] = value + elif value == second[key]: + common[key] = value + else: + different[key] = (value, second[key]) + + if missing or extra or different: + return { + "common": common, + "different": different, + "extra": extra, + "missing": missing, + } + else: + return None + def _add_whl_library(self, *, python_version, whl, repo): + """Add a whl_library and kwargs to call it with for the hub. + + Args: + self: implicitly added + python_version: {type}`str` the python version to assume + whl: struct from `_whl_library_args()` + repo: struct from `_whl_repo` + """ if repo == None: # NOTE @aignas 2025-07-07: we guard against an edge-case where there # are more platforms defined than there are wheels for and users # disallow building from sdist. return - platforms = self._platforms[python_version] - # TODO @aignas 2025-06-29: we should not need the version in the repo_name if # we are using pipstar and we are downloading the wheel using the downloader + # + # However, for that we should first have a different way to reference closures with + # extras. For example, if some package depends on `foo[extra]` and another depends on + # `foo`, we should have 2 py_library targets. repo_name = "{}_{}_{}".format(self.name, version_label(python_version), repo.repo_name) if repo_name in self._whl_libraries: - fail("attempting to create a duplicate library {} for {}".format( - repo_name, - whl.name, - )) + diff = _diff_dict(self._whl_libraries[repo_name], repo.args) + if diff: + self._logger.fail(lambda: ( + "Attempting to create a duplicate library {repo_name} for {whl_name} with different arguments. Already existing declaration has:\n".format( + repo_name = repo_name, + whl_name = whl.name, + ) + "\n".join([ + " {}: {}".format(key, render.indent(render.dict(value)).lstrip()) + for key, value in diff.items() + if value + ]) + )) + return self._whl_libraries[repo_name] = repo.args - if not self._config.enable_pipstar and "experimental_target_platforms" in repo.args: - self._whl_libraries[repo_name] |= { - "experimental_target_platforms": sorted({ - # TODO @aignas 2025-07-07: this should be solved in a better way - platforms[candidate].triple.partition("_")[-1]: None - for p in repo.args["experimental_target_platforms"] - for candidate in platforms - if candidate.endswith(p) - }), - } - mapping = self._whl_map.setdefault(whl.name, {}) if repo.config_setting in mapping and mapping[repo.config_setting] != repo_name: fail( @@ -213,53 +347,58 @@ def _add_whl_library(self, *, python_version, whl, repo): ### end of setters, below we have various functions to implement the public methods -def _set_get_index_urls(self, pip_attr): - if not pip_attr.experimental_index_url: - if pip_attr.experimental_extra_index_urls: - fail("'experimental_extra_index_urls' is a no-op unless 'experimental_index_url' is set") - elif pip_attr.experimental_index_url_overrides: - fail("'experimental_index_url_overrides' is a no-op unless 'experimental_index_url' is set") - elif pip_attr.simpleapi_skip: - fail("'simpleapi_skip' is a no-op unless 'experimental_index_url' is set") - elif pip_attr.netrc: - fail("'netrc' is a no-op unless 'experimental_index_url' is set") - elif pip_attr.auth_patterns: - fail("'auth_patterns' is a no-op unless 'experimental_index_url' is set") - +def _set_get_index_urls(self, mctx, pip_attr): + # Resolve the index URL through envsubst so the ``$VAR`` / ``${VAR:-default}`` + # form is honored when deciding whether the experimental index-url mode is + # active. Without this, an unsubstituted template like ``$RULES_PYTHON_PIP_INDEX_URL`` + # is treated as truthy and the mode is forced on, even when the env var + # would expand to the empty string. + default_index_url = envsubst( + pip_attr.experimental_index_url, + pip_attr.envsubst, + mctx.getenv, + ) or self._config.index_url + default_extra_index_urls = pip_attr.experimental_extra_index_urls or [] + + if not default_index_url: # parallel_download is set to True by default, so we are not checking/validating it # here - return + return False python_version = pip_attr.python_version self._use_downloader.setdefault(python_version, {}).update({ normalize_name(s): False for s in pip_attr.simpleapi_skip }) - self._get_index_urls[python_version] = lambda ctx, distributions: self._simpleapi_download_fn( + self._get_index_urls[python_version] = lambda ctx, distributions, *, index_url = None, extra_index_urls = None: self._simpleapi_download_fn( ctx, attr = struct( - index_url = pip_attr.experimental_index_url, - extra_index_urls = pip_attr.experimental_extra_index_urls or [], + index_url = (index_url or default_index_url).rstrip("/"), + extra_index_urls = [ + x.rstrip("/") + for x in (extra_index_urls or default_extra_index_urls) + ], index_url_overrides = pip_attr.experimental_index_url_overrides or {}, - sources = [ - d - for d in distributions + sources = { + d: versions + for d, versions in distributions.items() if _use_downloader(self, python_version, d) - ], + }, envsubst = pip_attr.envsubst, # Auth related info - netrc = pip_attr.netrc, - auth_patterns = pip_attr.auth_patterns, + netrc = self._config.netrc or pip_attr.netrc, + auth_patterns = self._config.auth_patterns or pip_attr.auth_patterns, ), cache = self._simpleapi_cache, parallel_download = pip_attr.parallel_download, ) + return True -def _detect_interpreter(self, pip_attr): +def _detect_interpreter(self, pip_attr, python_version): python_interpreter_target = pip_attr.python_interpreter_target if python_interpreter_target == None and not pip_attr.python_interpreter: python_name = "python_{}_host".format( - pip_attr.python_version.replace(".", "_"), + python_version.replace(".", "_"), ) if python_name not in self._available_interpreters: fail(( @@ -269,7 +408,7 @@ def _detect_interpreter(self, pip_attr): "Expected to find {python_name} among registered versions:\n {labels}" ).format( hub_name = self.name, - version = pip_attr.python_version, + version = python_version, python_name = python_name, labels = " \n".join(self._available_interpreters), )) @@ -280,17 +419,25 @@ def _detect_interpreter(self, pip_attr): path = pip_attr.python_interpreter, ) -def _platforms(*, python_version, minor_mapping, config): +def _platforms(module_ctx, *, python_version, config, target_platforms): platforms = {} python_version = version.parse( - full_version( - version = python_version, - minor_mapping = minor_mapping, - ), + python_version, strict = True, ) + target_platforms = sorted({ + p.format( + os = repo_utils.get_platforms_os_name(module_ctx), + arch = repo_utils.get_platforms_cpu_name(module_ctx), + ): None + for p in target_platforms + }) + for platform, values in config.platforms.items(): + if target_platforms and platform not in target_platforms: + continue + # TODO @aignas 2025-07-07: this is probably doing the parsing of the version too # many times. abi = "{}{}{}.{}".format( @@ -325,61 +472,24 @@ def _platforms(*, python_version, minor_mapping, config): ) return platforms -def _evaluate_markers(self, pip_attr): - if self._evaluate_markers_fn: - return self._evaluate_markers_fn - - if self._config.enable_pipstar: - return lambda _, requirements: evaluate_markers_star( - requirements = requirements, - platforms = self._platforms[pip_attr.python_version], - ) - - interpreter = _detect_interpreter(self, pip_attr) - - # NOTE @aignas 2024-08-02: , we will execute any interpreter that we find either - # in the PATH or if specified as a label. We will configure the env - # markers when evaluating the requirement lines based on the output - # from the `requirements_files_by_platform` which should have something - # similar to: - # { - # "//:requirements.txt": ["cp311_linux_x86_64", ...] - # } - # - # We know the target python versions that we need to evaluate the - # markers for and thus we don't need to use multiple python interpreter - # instances to perform this manipulation. This function should be executed - # only once by the underlying code to minimize the overhead needed to - # spin up a Python interpreter. - return lambda module_ctx, requirements: evaluate_markers_py( - module_ctx, - requirements = { - k: { - p: self._platforms[pip_attr.python_version][p].triple - for p in plats - } - for k, plats in requirements.items() - }, - python_interpreter = interpreter.path, - python_interpreter_target = interpreter.target, - srcs = pip_attr._evaluate_markers_srcs, - logger = self._logger, - ) - def _create_whl_repos( self, module_ctx, *, - pip_attr): + pip_attr, + python_version, + enable_pipstar_extract = False): """create all of the whl repositories Args: self: the builder. module_ctx: {type}`module_ctx`. pip_attr: {type}`struct` - the struct that comes from the tag class iteration. + python_version: {type}`str` - the resolved python version for this pip.parse call. + enable_pipstar_extract: {type}`bool` - enable the pipstar extraction or not. """ logger = self._logger - platforms = self._platforms[pip_attr.python_version] + platforms = self._platforms[python_version] requirements_by_platform = parse_requirements( module_ctx, requirements_by_platform = requirements_files_by_platform( @@ -391,15 +501,16 @@ def _create_whl_repos( extra_pip_args = pip_attr.extra_pip_args, platforms = sorted(platforms), # here we only need keys python_version = full_version( - version = pip_attr.python_version, + version = python_version, minor_mapping = self._minor_mapping, ), logger = logger, ), + uv_lock = pip_attr.uv_lock, platforms = platforms, extra_pip_args = pip_attr.extra_pip_args, get_index_urls = self._get_index_urls.get(pip_attr.python_version), - evaluate_markers = _evaluate_markers(self, pip_attr), + toml_decode = getattr(self._config, "toml_decode", None), logger = logger, ) @@ -419,6 +530,9 @@ def _create_whl_repos( module_ctx, pip_attr = pip_attr, ) + + interpreter = _detect_interpreter(self, pip_attr, python_version) + for whl in requirements_by_platform: whl_library_args = common_args | _whl_library_args( self, @@ -428,29 +542,30 @@ def _create_whl_repos( for src in whl.srcs: repo = _whl_repo( src = src, + index_url = whl.index_url, whl_library_args = whl_library_args, download_only = pip_attr.download_only, netrc = self._config.netrc or pip_attr.netrc, - use_downloader = _use_downloader(self, pip_attr.python_version, whl.name), + use_downloader = src.url and _use_downloader(self, python_version, whl.name), auth_patterns = self._config.auth_patterns or pip_attr.auth_patterns, - python_version = _major_minor_version(pip_attr.python_version), + python_version = _major_minor_version(python_version), is_multiple_versions = whl.is_multiple_versions, - enable_pipstar = self._config.enable_pipstar, + interpreter = interpreter, + enable_pipstar_extract = enable_pipstar_extract, ) _add_whl_library( self, - python_version = pip_attr.python_version, + python_version = python_version, whl = whl, repo = repo, ) def _common_args(self, module_ctx, *, pip_attr): - interpreter = _detect_interpreter(self, pip_attr) - # Construct args separately so that the lock file can be smaller and does not include unused # attrs. whl_library_args = dict( dep_template = "@{}//{{name}}:{{target}}".format(self.name), + config_load = "@{}//:config.bzl".format(self.name), ) maybe_args = dict( # The following values are safe to omit if they have false like values @@ -460,11 +575,7 @@ def _common_args(self, module_ctx, *, pip_attr): environment = pip_attr.environment, envsubst = pip_attr.envsubst, pip_data_exclude = pip_attr.pip_data_exclude, - python_interpreter = interpreter.path, - python_interpreter_target = interpreter.target, ) - if not self._config.enable_pipstar: - maybe_args["experimental_target_platforms"] = pip_attr.experimental_target_platforms whl_library_args.update({k: v for k, v in maybe_args.items() if v}) maybe_args_with_default = dict( @@ -507,13 +618,15 @@ def _whl_repo( *, src, whl_library_args, + index_url, is_multiple_versions, download_only, netrc, auth_patterns, python_version, use_downloader, - enable_pipstar = False): + interpreter, + enable_pipstar_extract = False): args = dict(whl_library_args) args["requirement"] = src.requirement_line is_whl = src.filename.endswith(".whl") @@ -525,6 +638,12 @@ def _whl_repo( # need to pass the extra args there, so only pop this for whls args["extra_pip_args"] = src.extra_pip_args + if not (enable_pipstar_extract and is_whl): + if interpreter.path: + args["python_interpreter"] = interpreter.path + if interpreter.target: + args["python_interpreter_target"] = interpreter.target + if not src.url or (not is_whl and download_only): if download_only and use_downloader: # If the user did not allow using sdists and we are using the downloader @@ -552,21 +671,20 @@ def _whl_repo( args["netrc"] = netrc if auth_patterns: args["auth_patterns"] = auth_patterns + if index_url: + args["index_url"] = index_url args["urls"] = [src.url] args["sha256"] = src.sha256 args["filename"] = src.filename - if not enable_pipstar: - args["experimental_target_platforms"] = [ - # Get rid of the version for the target platforms because we are - # passing the interpreter any way. Ideally we should search of ways - # how to pass the target platforms through the hub repo. - p.partition("_")[2] - for p in src.target_platforms - ] + + # TODO @aignas 2025-11-02: once we have pipstar enabled we can add extra + # targets to each hub for each extra combination and solve this more cleanly as opposed to + # duplicating whl_library repositories. + target_platforms = src.target_platforms if is_multiple_versions else [] return struct( - repo_name = whl_repo_name(src.filename, src.sha256), + repo_name = whl_repo_name(src.filename, src.sha256, *target_platforms), args = args, config_setting = whl_config_setting( version = python_version, diff --git a/python/private/pypi/hub_repository.bzl b/python/private/pypi/hub_repository.bzl index 1d572d09e2..f915aa1c77 100644 --- a/python/private/pypi/hub_repository.bzl +++ b/python/private/pypi/hub_repository.bzl @@ -50,7 +50,7 @@ def _impl(rctx): "config.bzl", rctx.attr._config_template, substitutions = { - "%%WHL_MAP%%": render.dict(rctx.attr.whl_map, value_repr = lambda x: "None"), + "%%PACKAGES%%": render.dict(rctx.attr.whl_map, value_repr = lambda x: "None"), }, ) rctx.template("requirements.bzl", rctx.attr._requirements_bzl_template, substitutions = { @@ -100,7 +100,7 @@ in the pip.parse tag class. """, ), "_config_template": attr.label( - default = ":config.bzl.tmpl.bzlmod", + default = ":config.bzl.tmpl", ), "_requirements_bzl_template": attr.label( default = ":requirements.bzl.tmpl.bzlmod", diff --git a/python/private/pypi/labels.bzl b/python/private/pypi/labels.bzl index 22161b1496..8f91a03b4c 100644 --- a/python/private/pypi/labels.bzl +++ b/python/private/pypi/labels.bzl @@ -21,5 +21,4 @@ PY_LIBRARY_PUBLIC_LABEL = "pkg" PY_LIBRARY_IMPL_LABEL = "_pkg" DATA_LABEL = "data" DIST_INFO_LABEL = "dist_info" -WHEEL_ENTRY_POINT_PREFIX = "rules_python_wheel_entry_point" NODEPS_LABEL = "no_deps" diff --git a/python/private/pypi/missing_package.bzl b/python/private/pypi/missing_package.bzl new file mode 100644 index 0000000000..46823e813b --- /dev/null +++ b/python/private/pypi/missing_package.bzl @@ -0,0 +1,43 @@ +"""Rule for generating an execution-phase action failure when a PyPI package is missing.""" + +load("//python/private:py_info.bzl", "PyInfo") +load("//python/private:reexports.bzl", "BuiltinPyInfo") + +def _missing_package_error_impl(ctx): + out = ctx.actions.declare_file(ctx.label.name + ".error") + + if ctx.attr.hub_name: + hub_clause = ' when building under PyPI hub "{hub}". Try adding it to the requirements of this hub (e.g. requirements_lock or requirements_by_platform in pip.parse)'.format( + hub = ctx.attr.hub_name, + ) + else: + hub_clause = ' because no default PyPI hub was configured. Try designating a default hub via pip.default(default_hub = "...") or select a hub using --@rules_python//python/config_settings:venv' + + # Register an action that fails when Bazel attempts to stage/build this file + ctx.actions.run_shell( + outputs = [out], + command = "echo 'ERROR: PyPI package \"{pkg}\" is not available{hub_clause}.' >&2 && exit 1".format( + pkg = ctx.attr.package_name, + hub_clause = hub_clause, + ), + ) + + maybe_builtin = [BuiltinPyInfo(transitive_sources = depset([out]))] if BuiltinPyInfo != None else [] + + return [ + DefaultInfo( + files = depset([out]), + data_runfiles = ctx.runfiles([out]), + ), + PyInfo( + transitive_sources = depset([out]), + ), + ] + maybe_builtin + +missing_package_error = rule( + implementation = _missing_package_error_impl, + attrs = { + "hub_name": attr.string(mandatory = True), + "package_name": attr.string(mandatory = True), + }, +) diff --git a/python/private/pypi/namespace_pkg_tmpl.py b/python/private/pypi/namespace_pkg_tmpl.py index a21b846e76..80da1dfb2a 100644 --- a/python/private/pypi/namespace_pkg_tmpl.py +++ b/python/private/pypi/namespace_pkg_tmpl.py @@ -1,2 +1,2 @@ # __path__ manipulation added by bazel-contrib/rules_python to support namespace pkgs. -__path__ = __import__("pkgutil").extend_path(__path__, __name__) +__path__ = __import__("pkgutil").extend_path(__path__, __name__) # noqa: F821 diff --git a/python/private/pypi/parse_requirements.bzl b/python/private/pypi/parse_requirements.bzl index acf3b0c6ae..be3ad9a5e5 100644 --- a/python/private/pypi/parse_requirements.bzl +++ b/python/private/pypi/parse_requirements.bzl @@ -28,8 +28,11 @@ behavior. load("//python/private:normalize_name.bzl", "normalize_name") load("//python/private:repo_utils.bzl", "repo_utils") +load("//python/uv/private:uv_lock_to_requirements.bzl", "uv_lock_extras_map") # buildifier: disable=bzl-visibility +load(":argparse.bzl", "argparse") load(":index_sources.bzl", "index_sources") load(":parse_requirements_txt.bzl", "parse_requirements_txt") +load(":pep508_evaluate.bzl", "evaluate") load(":pep508_requirement.bzl", "requirement") load(":select_whl.bzl", "select_whl") @@ -38,84 +41,290 @@ def parse_requirements( *, requirements_by_platform = {}, extra_pip_args = [], - platforms = {}, + platforms, get_index_urls = None, - evaluate_markers = None, extract_url_srcs = True, + uv_lock = None, + toml_decode = None, logger): """Get the requirements with platforms that the requirements apply to. Args: ctx: A context that has .read function that would read contents from a label. - platforms: The target platform descriptions. + platforms: The target platform descriptions. Cannot be empty and needs to have + at least the host platform and the definitions. requirements_by_platform (label_keyed_string_dict): a way to have different package versions (or different packages) for different os, arch combinations. extra_pip_args (string list): Extra pip arguments to perform extra validations and to be joined with args found in files. - get_index_urls: Callable[[ctx, list[str]], dict], a callable to get all + get_index_urls: Callable[[ctx, dict[str, list[str]]], dict], a callable to get all of the distribution URLs from a PyPI index. Accepts ctx and distribution names to query. - evaluate_markers: A function to use to evaluate the requirements. - Accepts a dict where keys are requirement lines to evaluate against - the platforms stored as values in the input dict. Returns the same - dict, but with values being platforms that are compatible with the - requirements line. extract_url_srcs: A boolean to enable extracting URLs from requirement lines to enable using bazel downloader. + uv_lock: {type}`str | None` an optional label/file path to the uv.lock + file. The ctx.read function will be used to read the contents. + If provided, the function will use the uv.lock file as the primary + source for package metadata and perform a consistency check against + requirements files if both are provided. + toml_decode: {type}`callable | None` A function to decode TOML + content (e.g. `toml.decode`). Required when `uv_lock` is provided. logger: repo_utils.logger, a simple struct to log diagnostic messages. Returns: - {type}`dict[str, list[struct]]` where the key is the distribution name and the struct - contains the following attributes: - * `distribution`: {type}`str` The non-normalized distribution name. - * `srcs`: {type}`struct` The parsed requirement line for easier Simple - API downloading (see `index_sources` return value). - * `target_platforms`: {type}`list[str]` Target platforms that this package is for. - The format is `cp3{minor}_{os}_{arch}`. + {type}`list[struct]` where each struct contains the following attributes: + * `name`: {type}`str` The normalized distribution name. * `is_exposed`: {type}`bool` `True` if the package should be exposed via the hub repository. - * `extra_pip_args`: {type}`list[str]` pip args to use in case we are - not using the bazel downloader to download the archives. This should - be passed to {obj}`whl_library`. - * `whls`: {type}`list[struct]` The list of whl entries that can be - downloaded using the bazel downloader. - * `sdist`: {type}`list[struct]` The sdist that can be downloaded using - the bazel downloader. - - The second element is extra_pip_args should be passed to `whl_library`. + * `is_multiple_versions`: {type}`bool` `True` if multiple versions have been + specified for this package. + * `index_url`: {type}`str` The index URL used to download the package. + * `srcs`: {type}`list[struct]` A list of per-distribution source entries, each + containing: `distribution`, `extra_pip_args`, `requirement_line`, + `target_platforms`, `filename`, `sha256`, `url`, `yanked`. """ - evaluate_markers = evaluate_markers or (lambda _ctx, _requirements: {}) + if uv_lock and toml_decode: + uv_lock = toml_decode(ctx.read(uv_lock)) + return _parse_requirements_with_uv_lock( + ctx = ctx, + requirements_by_platform = requirements_by_platform, + extra_pip_args = extra_pip_args, + platforms = platforms, + get_index_urls = get_index_urls, + extract_url_srcs = extract_url_srcs, + uv_lock = uv_lock, + logger = logger, + ) + + return _parse_requirements_from_req_files( + ctx = ctx, + requirements_by_platform = requirements_by_platform, + extra_pip_args = extra_pip_args, + platforms = platforms, + get_index_urls = get_index_urls, + extract_url_srcs = extract_url_srcs, + logger = logger, + ) + +def _get_all_platforms(requirements_by_platform): + """Get the set of all platform names from requirements_by_platform.""" + all_platforms = {} + for plats in requirements_by_platform.values(): + for p in plats: + all_platforms[p] = None + return sorted(all_platforms) + +def _parse_requirements_with_uv_lock( + ctx, + *, + requirements_by_platform, + extra_pip_args, + platforms, + get_index_urls, + extract_url_srcs, + uv_lock, + logger): + """Parse requirements using uv.lock as the primary source.""" + if uv_lock: + return _parse_uv_lock_json( + uv_lock = uv_lock, + all_platforms = _get_all_platforms(requirements_by_platform) if requirements_by_platform else sorted(platforms.keys()), + platforms = platforms, + extra_pip_args = extra_pip_args, + logger = logger, + ) + + return _parse_requirements_from_req_files( + ctx = ctx, + requirements_by_platform = requirements_by_platform, + extra_pip_args = extra_pip_args, + platforms = platforms, + get_index_urls = get_index_urls, + extract_url_srcs = extract_url_srcs, + logger = logger, + ) + +def _parse_uv_lock_json(uv_lock, all_platforms, logger, extra_pip_args = None, platforms = {}): + """Parse uv.lock JSON and build the same return structs as parse_requirements. + + Args: + uv_lock: {type}`dict` The decoded uv.lock contents. + all_platforms: {type}`list[str]` The list of all platform names. + logger: {type}`struct` A logger for diagnostic messages. + extra_pip_args: {type}`list[str] | None` Extra pip arguments to pass through. + platforms: {type}`dict[str, struct]` A dict of platform name to platform info + (containing `.env` with PEP 508 marker environment). + + Returns: + {type}`list[struct]` The same format as {func}`parse_requirements`. + """ + extras_map = uv_lock_extras_map(uv_lock) + + uv_packages = {} + + if not platforms: + fail("BUG: platforms must be configured") + + for pkg in uv_lock["package"]: + name = pkg["name"] + version = pkg["version"] + norm_name = normalize_name(name) + entry = uv_packages.setdefault(norm_name, { + "distribution": name, + "resolved_srcs": [], + "versions": {}, + }) + entry["versions"][version] = None + source = pkg.get("source") or {} + registry = (source.get("registry") or "").rstrip("/") + if registry.startswith("http://") or registry.startswith("https://"): + index_url = "{}/{}".format(registry, norm_name.replace("_", "-")) + else: + index_url = "" + entry["index_url"] = index_url + + pkg_extras = sorted(extras_map.get(name, [])) + extra_str = "[{}]".format(",".join(pkg_extras)) if pkg_extras else "" + + markers = pkg.get("resolution-markers", []) + if markers: + marker_expr = " or ".join(markers) + pkg_platforms = [ + p + for p in all_platforms + if evaluate(marker_expr, env = platforms[p].env) + ] + else: + pkg_platforms = list(all_platforms) + + # Prepare candidates + candidates = [] + for wheel in pkg.get("wheels", []): + url = wheel["url"] + _, _, filename = url.rpartition("/") + sha256 = wheel.get("hash", "").replace("sha256:", "") + candidates.append(struct( + filename = filename, + url = url, + sha256 = sha256, + kind = "wheel", + )) + + sdist_struct = None + sdist = pkg.get("sdist", None) + if sdist: + url = sdist["url"] + _, _, filename = url.rpartition("/") + sha256 = sdist.get("hash", "").replace("sha256:", "") + sdist_struct = struct( + filename = filename, + url = url, + sha256 = sha256, + kind = "sdist", + ) + + git_struct = None + if pkg.get("source", {}).get("git"): + url = pkg["source"]["git"] + _, _, filename = url.rpartition("/") + git_struct = struct( + filename = filename, + url = url, + sha256 = "", + kind = "git", + source = pkg["source"], + ) + + plat_to_src = {} + for p in pkg_platforms: + platform = platforms.get(p) + if not platform: + continue + + best_wheel = None + if candidates: + best_wheel = select_whl( + whls = candidates, + python_version = platform.env.get("python_full_version", "3"), + whl_platform_tags = platform.whl_platform_tags, + whl_abi_tags = platform.whl_abi_tags, + implementation_name = platform.env.get("implementation_name", "cpython"), + limit = 1, + logger = logger, + ) + + if best_wheel: + plat_to_src[p] = best_wheel + elif sdist_struct: + plat_to_src[p] = sdist_struct + elif git_struct: + plat_to_src[p] = git_struct + + # Group platforms by resolved source + src_to_plats = {} + for p, src in plat_to_src.items(): + key = src.filename + src.sha256 + src_to_plats.setdefault(key, struct(src = src, plats = [])).plats.append(p) + + # Build resolved_srcs + for key, val in src_to_plats.items(): + src = val.src + plats = sorted(val.plats) + requirement_line = "{name}{extras}=={version}".format( + name = name, + extras = extra_str, + version = version, + ) + entry["resolved_srcs"].append(struct( + distribution = name, + extra_pip_args = extra_pip_args or [], + requirement_line = requirement_line, + target_platforms = plats, + filename = src.filename, + sha256 = src.sha256, + url = src.url, + yanked = None, + )) + + ret = [] + for norm_name, info in sorted(uv_packages.items()): + versions = sorted(info["versions"].keys()) + item = struct( + name = norm_name, + is_exposed = True, + is_multiple_versions = len(versions) > 1, + index_url = info["index_url"], + srcs = info["resolved_srcs"], + ) + ret.append(item) + + logger.debug(lambda: "Parsed {} packages from uv.lock".format(len(ret))) + return ret + +def _parse_requirements_from_req_files( + ctx, + *, + requirements_by_platform, + extra_pip_args, + platforms, + get_index_urls, + extract_url_srcs, + logger): + """Parse requirements from requirements.txt files (existing behavior).""" options = {} requirements = {} + all_files_parsed = {} + index_url = None + extra_index_urls = [] for file, plats in requirements_by_platform.items(): logger.trace(lambda: "Using {} for {}".format(file, plats)) contents = ctx.read(file) - # Parse the requirements file directly in starlark to get the information - # needed for the whl_library declarations later. parse_result = parse_requirements_txt(contents) - # Replicate a surprising behavior that WORKSPACE builds allowed: - # Defining a repo with the same name multiple times, but only the last - # definition is respected. - # The requirement lines might have duplicate names because lines for extras - # are returned as just the base package name. e.g., `foo[bar]` results - # in an entry like `("foo", "foo[bar] == 1.0 ...")`. - # Lines with different markers are not condidered duplicates. - requirements_dict = {} - for entry in sorted( - parse_result.requirements, - # Get the longest match and fallback to original WORKSPACE sorting, - # which should get us the entry with most extras. - # - # FIXME @aignas 2024-05-13: The correct behaviour might be to get an - # entry with all aggregated extras, but it is unclear if we - # should do this now. - key = lambda x: (len(x[1].partition("==")[0]), x), - ): - req = requirement(entry[1]) - requirements_dict[(req.name, req.version, req.marker)] = entry + if file not in all_files_parsed: + all_files_parsed[file] = parse_result.requirements tokenized_options = [] for opt in parse_result.options: @@ -123,84 +332,115 @@ def parse_requirements( tokenized_options.append(p) pip_args = tokenized_options + extra_pip_args + + # Parse the index URL from the requirement files once per file + index_url = argparse.index_url(pip_args, index_url) + extra_index_urls = argparse.extra_index_url(pip_args, []) + if argparse.platform(pip_args, []): + # No use of downloader if the user specifies "--platform" pip arg. This means that + # they intend to use pip to download the wheels + # + # TODO @aignas 2026-04-11: consider removing this line in the next major release + # (3.0). + get_index_urls = None + + # Pre-parse requirements once per file to avoid redundant parsing in loops + parsed_reqs = [(entry, requirement(entry[1])) for entry in parse_result.requirements] + for plat in plats: - requirements[plat] = requirements_dict.values() + plat_env = platforms.get(plat) + + requirements[plat] = [ + entry + for entry, req in parsed_reqs + if not req.marker or (plat_env and evaluate(req.marker, env = plat_env.env)) + ] + options[plat] = pip_args + index_url = argparse.index_url(pip_args, index_url) + extra_index_urls = argparse.extra_index_url(pip_args, []) + platform = argparse.platform(pip_args, []) + if platform: + get_index_urls = None + + reqs_by_name = {} requirements_by_platform = {} - reqs_with_env_markers = {} - for target_platform, reqs_ in requirements.items(): - extra_pip_args = options[target_platform] + for plat, parse_results in requirements.items(): + requirements_dict = {} + for entry in sorted( + parse_results, + key = lambda x: (len(x[1].partition("==")[0]), x), + ): + req_line = entry[1] + req = requirement(req_line) + + requirements_dict[req.name] = entry + + extra_pip_args_for_plat = options[plat] - for distribution, requirement_line in reqs_: - for_whl = requirements_by_platform.setdefault( + for distribution, requirement_line in requirements_dict.values(): + for_whl = reqs_by_name.setdefault( normalize_name(distribution), {}, ) - if ";" in requirement_line: - reqs_with_env_markers.setdefault(requirement_line, []).append(target_platform) - for_req = for_whl.setdefault( - (requirement_line, ",".join(extra_pip_args)), + (requirement_line, ",".join(extra_pip_args_for_plat)), struct( distribution = distribution, srcs = index_sources(requirement_line), requirement_line = requirement_line, target_platforms = [], - extra_pip_args = extra_pip_args, + extra_pip_args = extra_pip_args_for_plat, ), ) - for_req.target_platforms.append(target_platform) - - # This may call to Python, so execute it early (before calling to the - # internet below) and ensure that we call it only once. - # - # NOTE @aignas 2024-07-13: in the future, if this is something that we want - # to do, we could use Python to parse the requirement lines and infer the - # URL of the files to download things from. This should be important for - # VCS package references. - env_marker_target_platforms = evaluate_markers(ctx, reqs_with_env_markers) - logger.trace(lambda: "Evaluated env markers from:\n{}\n\nTo:\n{}".format( - reqs_with_env_markers, - env_marker_target_platforms, - )) + for_req.target_platforms.append(plat) index_urls = {} if get_index_urls: + distributions = {} + for entries in all_files_parsed.values(): + for entry in entries: + name, req_line = entry + srcs = index_sources(req_line) + if srcs.url: + continue + versions = distributions.setdefault(normalize_name(name), {}) + versions[srcs.version] = None + + distributions = {k: sorted(v.keys()) for k, v in distributions.items()} + index_urls = get_index_urls( ctx, - # Use list({}) as a way to have a set - list({ - req.distribution: None - for reqs in requirements_by_platform.values() - for req in reqs.values() - if not req.srcs.url - }), + distributions, + index_url = index_url, + extra_index_urls = extra_index_urls, ) ret = [] - for name, reqs in sorted(requirements_by_platform.items()): + for name, reqs in sorted(reqs_by_name.items()): requirement_target_platforms = {} for r in reqs.values(): - target_platforms = env_marker_target_platforms.get(r.requirement_line, r.target_platforms) - for p in target_platforms: + for p in r.target_platforms: requirement_target_platforms[p] = None + pkg_sources = index_urls.get(name) + package_srcs = _package_srcs( + name = name, + reqs = reqs, + pkg_sources = pkg_sources, + platforms = platforms, + extract_url_srcs = extract_url_srcs, + logger = logger, + ) + item = struct( - # Return normalized names name = normalize_name(name), is_exposed = len(requirement_target_platforms) == len(requirements), is_multiple_versions = len(reqs.values()) > 1, - srcs = _package_srcs( - name = name, - reqs = reqs, - index_urls = index_urls, - platforms = platforms, - env_marker_target_platforms = env_marker_target_platforms, - extract_url_srcs = extract_url_srcs, - logger = logger, - ), + index_url = pkg_sources.index_url if pkg_sources else "", + srcs = package_srcs, ) ret.append(item) if not item.is_exposed and logger: @@ -218,45 +458,42 @@ def _package_srcs( *, name, reqs, - index_urls, + pkg_sources, platforms, logger, - env_marker_target_platforms, extract_url_srcs): """A function to return sources for a particular package.""" srcs = {} for r in sorted(reqs.values(), key = lambda r: r.requirement_line): - if ";" in r.requirement_line: - target_platforms = env_marker_target_platforms.get(r.requirement_line, []) - else: - target_platforms = r.target_platforms extra_pip_args = tuple(r.extra_pip_args) - for target_platform in target_platforms: + for target_platform in r.target_platforms: if platforms and target_platform not in platforms: fail("The target platform '{}' could not be found in {}".format( target_platform, platforms.keys(), )) - dist = _add_dists( + dist, can_fallback = _add_dists( requirement = r, target_platform = platforms.get(target_platform), - index_urls = index_urls.get(name), + index_urls = pkg_sources, logger = logger, ) logger.debug(lambda: "The whl dist is: {}".format(dist.filename if dist else dist)) if extract_url_srcs and dist: req_line = r.srcs.requirement - else: + elif can_fallback or (not extract_url_srcs and dist): dist = struct( url = "", filename = "", sha256 = "", - yanked = False, + yanked = None, ) req_line = r.srcs.requirement_line + else: + continue key = ( dist.filename, @@ -342,6 +579,14 @@ def _add_dists(*, requirement, index_urls, target_platform, logger = None): index_urls: The result of simpleapi_download. target_platform: The target_platform information. logger: A logger for printing diagnostic info. + + Returns: + (dist, can_fallback_to_pip): a struct with distribution details and how to fetch + it and a boolean flag to tell the other layers if we should add an entry to + fallback for pip if there are no supported whls found - if there is an sdist, we + can attempt the fallback, otherwise better to not, because the pip command will + fail and the error message will be confusing. What is more that would lead to + breakage of the bazel query. """ if requirement.srcs.url: @@ -349,23 +594,20 @@ def _add_dists(*, requirement, index_urls, target_platform, logger = None): logger.debug(lambda: "Could not detect the filename from the URL, falling back to pip: {}".format( requirement.srcs.url, )) - return None + return None, True # Handle direct URLs in requirements dist = struct( url = requirement.srcs.url, filename = requirement.srcs.filename, sha256 = requirement.srcs.shas[0] if requirement.srcs.shas else "", - yanked = False, + yanked = None, ) - if dist.filename.endswith(".whl"): - return dist - else: - return dist + return dist, False if not index_urls: - return None + return None, True whls = [] sdist = None @@ -383,12 +625,12 @@ def _add_dists(*, requirement, index_urls, target_platform, logger = None): # See https://packaging.python.org/en/latest/specifications/simple-repository-api/#adding-yank-support-to-the-simple-api maybe_whl = index_urls.whls.get(sha256) - if maybe_whl and not maybe_whl.yanked: + if maybe_whl and maybe_whl.yanked == None: whls.append(maybe_whl) continue maybe_sdist = index_urls.sdists.get(sha256) - if maybe_sdist and not maybe_sdist.yanked: + if maybe_sdist and maybe_sdist.yanked == None: sdist = maybe_sdist continue @@ -396,7 +638,7 @@ def _add_dists(*, requirement, index_urls, target_platform, logger = None): yanked = {} for dist in whls + [sdist]: - if dist and dist.yanked: + if dist and dist.yanked != None: yanked.setdefault(dist.yanked, []).append(dist.filename) if yanked: logger.warn(lambda: "\n".join([ @@ -408,7 +650,14 @@ def _add_dists(*, requirement, index_urls, target_platform, logger = None): if not target_platform: # The pipstar platforms are undefined here, so we cannot do any matching - return sdist + return sdist, True + + if not whls and not sdist: + # If there are no suitable wheels to handle for now allow fallback to pip, it + # may be a little bit more helpful when debugging? Most likely something is + # going a bit wrong here, should we raise an error because the sha256 have most + # likely mismatched? We are already printing a warning above. + return None, True # Select a single wheel that can work on the target_platform return select_whl( @@ -418,4 +667,4 @@ def _add_dists(*, requirement, index_urls, target_platform, logger = None): whl_abi_tags = target_platform.whl_abi_tags, whl_platform_tags = target_platform.whl_platform_tags, logger = logger, - ) or sdist + ) or sdist, sdist != None diff --git a/python/private/pypi/parse_simpleapi_html.bzl b/python/private/pypi/parse_simpleapi_html.bzl index a41f0750c4..7f0d2776d7 100644 --- a/python/private/pypi/parse_simpleapi_html.bzl +++ b/python/private/pypi/parse_simpleapi_html.bzl @@ -16,88 +16,129 @@ Parse SimpleAPI HTML in Starlark. """ -def parse_simpleapi_html(*, url, content): +load("//python/private:normalize_name.bzl", "normalize_name") +load(":version_from_filename.bzl", "version_from_filename") + +def parse_simpleapi_html(*, content, parse_index = False): """Get the package URLs for given shas by parsing the Simple API HTML. Args: - url(str): The URL that the HTML content can be downloaded from. - content(str): The Simple API HTML content. + content: {type}`str` The Simple API HTML content. + parse_index: {type}`bool` whether to parse the content as the index page of the PyPI index, + e.g. the `https://pypi.org/simple/`. This only has the URLs for the individual package. Returns: - A list of structs with: - * filename: The filename of the artifact. - * version: The version of the artifact. - * url: The URL to download the artifact. - * sha256: The sha256 of the artifact. - * metadata_sha256: The whl METADATA sha256 if we can download it. If this is - present, then the 'metadata_url' is also present. Defaults to "". - * metadata_url: The URL for the METADATA if we can download it. Defaults to "". + If it is the index page, return the map of package to URL it can be queried from. + Otherwise, a list of structs with: + * filename: {type}`str` The filename of the artifact. + * version: {type}`str` The version of the artifact. + * url: {type}`str` The URL to download the artifact. + * sha256: {type}`str` The sha256 of the artifact. + * metadata_sha256: {type}`str` The whl METADATA sha256 if we can download it. If this is + present, then the 'metadata_url' is also present. Defaults to "". + * metadata_url: {type}`str` The URL for the METADATA if we can download it. Defaults to "". + * yanked: {type}`str | None` the yank reason if the package is yanked. If it is not yanked, + then it will be `None`. An empty string yank reason means that the package is yanked but + the reason is not provided. """ sdists = {} whls = {} - lines = content.split("= (2, 0): # We don't expect to have version 2.0 here, but have this check in place just in case. # https://packaging.python.org/en/latest/specifications/simple-repository-api/#versioning-pypi-s-simple-api fail("Unsupported API version: {}".format(api_version)) - # Each line follows the following pattern - # filename
- sha256s_by_version = {} - for line in lines[1:]: - dist_url, _, tail = line.partition("#sha256=") - dist_url = _absolute_url(url, dist_url) - - sha256, _, tail = tail.partition("\"") - - # See https://packaging.python.org/en/latest/specifications/simple-repository-api/#adding-yank-support-to-the-simple-api - yanked = "data-yanked" in line - - head, _, _ = tail.rpartition("") - maybe_metadata, _, filename = head.rpartition(">") - version = _version(filename) + packages = {} + + # 2. Iterate using find() to avoid huge list allocations from .split(" tag first, then find the end of the opening + # tag using rfind. This correctly handles attributes that + # contain > characters, e.g. data-requires-python=">=3.6". + end_tag = content.find("", start_tag) + if end_tag == -1: + break + tag_end = content.rfind(">", start_tag, end_tag) + if tag_end == -1 or tag_end <= start_tag: + cursor = end_tag + 4 + continue + + # Extract only the necessary slices + filename = content[tag_end + 1:end_tag].strip() + attr_part = content[start_tag + 3:tag_end] + + # Update cursor for next iteration + cursor = end_tag + 4 + + attrs = _parse_attrs(attr_part) + href = attrs.get("href", "") + if not href: + continue + + if parse_index: + pkg_name = filename + packages[normalize_name(pkg_name)] = href + continue + + # 3. Efficient Attribute Parsing + dist_url, _, sha256 = href.partition("#sha256=") + + # Handle Yanked status + yanked = None + if "data-yanked" in attrs: + yanked = _unescape_pypi_html(attrs["data-yanked"]) + + version = version_from_filename(filename) sha256s_by_version.setdefault(version, []).append(sha256) + # 4. Optimized Metadata Check (PEP 714) metadata_sha256 = "" metadata_url = "" - for metadata_marker in ["data-core-metadata", "data-dist-info-metadata"]: - metadata_marker = metadata_marker + "=\"sha256=" - if metadata_marker in maybe_metadata: - # Implement https://peps.python.org/pep-0714/ - _, _, tail = maybe_metadata.partition(metadata_marker) - metadata_sha256, _, _ = tail.partition("\"") - metadata_url = dist_url + ".metadata" - break + + # Dist-info is more common in modern PyPI + m_val = attrs.get("data-dist-info-metadata") or attrs.get("data-core-metadata") + if m_val and m_val != "false": + _, _, metadata_sha256 = m_val.partition("sha256=") + metadata_url = dist_url + ".metadata" + + # 5. Result object + dist = struct( + filename = filename, + version = version, + url = dist_url, + sha256 = sha256, + metadata_sha256 = metadata_sha256, + metadata_url = metadata_url, + yanked = yanked, + ) if filename.endswith(".whl"): - whls[sha256] = struct( - filename = filename, - version = version, - url = dist_url, - sha256 = sha256, - metadata_sha256 = metadata_sha256, - metadata_url = _absolute_url(url, metadata_url) if metadata_url else "", - yanked = yanked, - ) + whls[sha256] = dist else: - sdists[sha256] = struct( - filename = filename, - version = version, - url = dist_url, - sha256 = sha256, - metadata_sha256 = "", - metadata_url = "", - yanked = yanked, - ) + sdists[sha256] = dist + + if parse_index: + return packages return struct( sdists = sdists, @@ -105,68 +146,76 @@ def parse_simpleapi_html(*, url, content): sha256s_by_version = sha256s_by_version, ) -_SDIST_EXTS = [ - ".tar", # handles any compression - ".zip", -] +def _parse_attrs(attr_string): + """Parses attributes from a pre-sliced string.""" + attrs = {} + parts = attr_string.split('"') -def _version(filename): - # See https://packaging.python.org/en/latest/specifications/binary-distribution-format/#binary-distribution-format + for i in range(0, len(parts) - 1, 2): + raw_key = parts[i].strip() + if not raw_key: + continue - _, _, tail = filename.partition("-") - version, _, _ = tail.partition("-") - if version != tail: - # The format is {name}-{version}-{whl_specifiers}.whl - return version + key_parts = raw_key.split(" ") + current_key = key_parts[-1].rstrip("=") - # NOTE @aignas 2025-03-29: most of the files are wheels, so this is not the common path + # Batch handle booleans + for j in range(len(key_parts) - 1): + b = key_parts[j].strip() + if b: + attrs[b] = "" - # {name}-{version}.{ext} - for ext in _SDIST_EXTS: - version, _, _ = version.partition(ext) # build or name + attrs[current_key] = parts[i + 1] - return version + # Final trailing boolean check + last = parts[-1].strip() + if last: + for b in last.split(" "): + if b: + attrs[b] = "" + return attrs -def _get_root_directory(url): - scheme_end = url.find("://") - if scheme_end == -1: - fail("Invalid URL format") +def _unescape_pypi_html(text): + """Unescape HTML text. - scheme = url[:scheme_end] - host_end = url.find("/", scheme_end + 3) - if host_end == -1: - host_end = len(url) - host = url[scheme_end + 3:host_end] + Decodes standard HTML entities used in the Simple API. + Specifically targets characters used in URLs and attribute values. - return "{}://{}".format(scheme, host) - -def _is_downloadable(url): - """Checks if the URL would be accepted by the Bazel downloader. + Args: + text: {type}`str` The text to replace. - This is based on Bazel's HttpUtils::isUrlSupportedByDownloader + Returns: + A string with unescaped characters """ - return url.startswith("http://") or url.startswith("https://") or url.startswith("file://") - -def _absolute_url(index_url, candidate): - if candidate == "": - return candidate - - if _is_downloadable(candidate): - return candidate - - if candidate.startswith("/"): - # absolute path - root_directory = _get_root_directory(index_url) - return "{}{}".format(root_directory, candidate) - - if candidate.startswith(".."): - # relative path with up references - candidate_parts = candidate.split("..") - last = candidate_parts[-1] - for _ in range(len(candidate_parts) - 1): - index_url, _, _ = index_url.rstrip("/").rpartition("/") - - return "{}/{}".format(index_url, last.strip("/")) - # relative path without up-references - return "{}/{}".format(index_url.rstrip("/"), candidate) + # 1. Short circuit for the most common case + if not text or "&" not in text: + return text + + # 2. Check for the most frequent PEP 503 entities first (version constraints). + # Re-ordering based on frequency reduces unnecessary checks for rare entities. + if ">" in text: + text = text.replace(">", ">") + if "<" in text: + text = text.replace("<", "<") + + # 3. Grouped check for numeric entities. + # If '&#' isn't there, we skip 4 distinct string scans. + if "&#" in text: + if "'" in text: + text = text.replace("'", "'") + if "'" in text: + text = text.replace("'", "'") + if " " in text: + text = text.replace(" ", "\n") + if " " in text: + text = text.replace(" ", "\r") + + if """ in text: + text = text.replace(""", '"') + + # 4. Handle ampersands last to prevent double-decoding. + if "&" in text: + text = text.replace("&", "&") + + return text diff --git a/python/private/pypi/patch_whl.bzl b/python/private/pypi/patch_whl.bzl index 7af9c4da2f..e8d76eeba5 100644 --- a/python/private/pypi/patch_whl.bzl +++ b/python/private/pypi/patch_whl.bzl @@ -12,25 +12,26 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""A small utility to patch a file in the repository context and repackage it using a Python interpreter - -Note, because we are patching a wheel file and we need a new RECORD file, this -function will print a diff of the RECORD and will ask the user to include a -RECORD patch in their patches that they maintain. This is to ensure that we can -satisfy the following usecases: -* Patch an invalid RECORD file. -* Patch files within a wheel. - -If we were silently regenerating the RECORD file, we may be vulnerable to supply chain -attacks (it is a very small chance) and keeping the RECORD patches next to the -other patches ensures that the users have overview on exactly what has changed -within the wheel. +"""A small utility to patch a file in the repository context natively. + +This replaces the previous approach that depended on running Python to +repack patched wheels. Instead, after extracting and patching, we fix +the RECORD file natively in Starlark and repack using system zip. """ +load("@rules_python_internal//:rules_python_config.bzl", rp_config = "config") +load("//python/private:repo_utils.bzl", "repo_utils") load(":parse_whl_name.bzl", "parse_whl_name") -load(":pypi_repo_utils.bzl", "pypi_repo_utils") -_rules_python_root = Label("//:BUILD.bazel") +_DUMMY_SHA256 = "sha256=0" + +_RECORD_EXCLUDES = { + "INSTALLER": None, + "RECORD": None, + "RECORD.jws": None, + "RECORD.p7s": None, + "REQUESTED": None, +} def patched_whl_name(original_whl_name): """Return the new filename to output the patched wheel. @@ -45,14 +46,6 @@ def patched_whl_name(original_whl_name): version = parsed_whl.version suffix = "patched" if "+" in version: - # This already has some local version, so we just append one more - # identifier here. We comply with the spec and mark the file as patched - # by adding a local version identifier at the end. - # - # By doing this we can still install the package using most of the package - # managers - # - # See https://packaging.python.org/en/latest/specifications/version-specifiers/#local-version-identifiers version = "{}.{}".format(version, suffix) else: version = "{}+{}".format(version, suffix) @@ -65,33 +58,30 @@ def patched_whl_name(original_whl_name): platform_tag = parsed_whl.platform_tag, ) -def patch_whl(rctx, *, python_interpreter, whl_path, patches, **kwargs): - """Patch a whl file and repack it to ensure that the RECORD metadata stays correct. +def patch_whl(rctx, *, whl_path, patches): + """Patch a whl file and repack it natively. + + The wheel is extracted, patched, missing RECORD entries are added + with dummy sha256 and size values, and finally the wheel is + repacked using system zip. Args: rctx: repository_ctx - python_interpreter: the python interpreter to use. whl_path: The whl file name to be patched. patches: a label-keyed-int dict that has the patch files as keys and the patch_strip as the value. - **kwargs: extras passed to repo_utils.execute_checked. Returns: value of the repackaging action. """ - - # extract files into the current directory for patching as rctx.patch - # does not support patching in another directory. whl_input = rctx.path(whl_path) - # symlink to a zip file to use bazel's extract so that we can use bazel's - # repository_ctx patch implementation. The whl file may be in a different - # external repository. - whl_file_zip = whl_input.basename + ".zip" - rctx.symlink(whl_input, whl_file_zip) - rctx.extract(whl_file_zip) - if not rctx.delete(whl_file_zip): - fail("Failed to remove the symlink after extracting") + repo_utils.extract( + rctx, + archive = whl_input, + supports_whl_extraction = rp_config.supports_whl_extraction, + extract_needs_chmod = rp_config.extract_needs_chmod, + ) if not patches: fail("Trying to patch wheel without any patches") @@ -99,43 +89,159 @@ def patch_whl(rctx, *, python_interpreter, whl_path, patches, **kwargs): for patch_file, patch_strip in patches.items(): rctx.patch(patch_file, strip = patch_strip) - record_patch = rctx.path("RECORD.patch") + _fix_record(rctx) + whl_patched = patched_whl_name(whl_input.basename) - pypi_repo_utils.execute_checked( + rctx.delete(whl_input) + _repack_whl(rctx, output = whl_patched) + + return rctx.path(whl_patched) + +def _fix_record(rctx): + """Add missing file entries to RECORD with dummy sha256 and file size.""" + for entry in rctx.path(".").readdir(): + if not entry.basename.endswith(".dist-info"): + continue + + record_path = entry.get_child("RECORD") + if not record_path.exists: + continue + + all_files = _collect_files(rctx) + record_rel = repo_utils.repo_root_relative_path(rctx, record_path) + + new_content = fix_record_content( + record_content = rctx.read(record_path), + all_files = all_files, + record_rel = record_rel, + ) + if new_content != None: + rctx.file(record_path, new_content) + +def fix_record_content(record_content, all_files, record_rel): + """Add missing file entries to RECORD content with dummy sha256 and size. + + Args: + record_content: {type}`str` The existing RECORD file content. + all_files: {type}`dict[str, bool]` All files in the directory, keys + are repo-root-relative paths. + record_rel: {type}`str` The repo-root-relative path to the RECORD file. + + Returns: + {type}`str | None` The new RECORD content if entries were added, + or None if no changes were needed. + """ + has_trailing_newline = record_content.endswith("\n") + lines = record_content.split("\n") + + is_all_quoted = True + has_content = False + existing = {} + for line in lines: + stripped = line.strip() + if not stripped: + continue + has_content = True + if not stripped.startswith('"'): + is_all_quoted = False + parts = stripped.split(",") + if len(parts) >= 1: + fname = parts[0].strip('"') + existing[fname] = True + + if not has_content: + is_all_quoted = False + + existing[record_rel] = True + + added = [] + for fpath in sorted(all_files.keys()): + if fpath in existing: + continue + basename = fpath.split("/")[-1] if "/" in fpath else fpath + if basename in _RECORD_EXCLUDES: + continue + if fpath.endswith(".whl"): + continue + added.append(fpath) + + if not added: + return None + + new_lines = list(lines) + if not has_trailing_newline: + new_lines.append("") + if is_all_quoted: + for fpath in added: + new_lines.append('"{file}",{sha256},0'.format( + file = fpath, + sha256 = _DUMMY_SHA256, + )) + else: + for fpath in added: + new_lines.append("{file},{sha256},0".format( + file = fpath, + sha256 = _DUMMY_SHA256, + )) + new_lines.append("") + + return "\n".join(new_lines) + +def _collect_files(rctx): + """Collect all file paths relative to the repo root (iterative).""" + result = {} + paths = [(rctx.path("."), "")] + for _ in range(10000000): + if not paths: + break + path, prefix = paths.pop() + for entry in path.readdir(): + rel = prefix + "/" + entry.basename if prefix else entry.basename + if entry.is_dir: + paths.append((entry, rel)) + else: + result[rel] = True + return result + +def _repack_whl(rctx, *, output): + """Repack the current directory into a wheel file using system zip.""" + os_name = repo_utils.get_platforms_os_name(rctx) + if os_name == "windows": + _repack_whl_windows(rctx, output = output) + else: + _repack_whl_unix(rctx, output = output) + +def _repack_whl_unix(rctx, *, output): + repo_utils.execute_checked( rctx, - python = python_interpreter, - srcs = [ - Label("//python/private/pypi:repack_whl.py"), - Label("//tools:wheelmaker.py"), - ], + op = "PatchWhl", arguments = [ - "-m", - "python.private.pypi.repack_whl", - "--record-patch", - record_patch, - whl_input, - whl_patched, + "zip", + "-0", + "-X", + str(output), + "-r", + ".", ], - environment = { - "PYTHONPATH": str(rctx.path(_rules_python_root).dirname), - }, - **kwargs ) - if record_patch.exists: - record_patch_contents = rctx.read(record_patch) - warning_msg = """WARNING: the resultant RECORD file of the patch wheel is different +def _repack_whl_windows(rctx, *, output): + powershell_exe = rctx.which("powershell.exe") or rctx.which("powershell") + if not powershell_exe: + fail("powershell not found on PATH") - If you are patching on Windows, you may see this warning because of - a known issue (bazel-contrib/rules_python#1639) with file endings. + script_path = rctx.path(Label("//python/private/pypi:repack_whl.ps1")) - If you would like to silence the warning, you can apply the patch that is stored in - {record_patch}. The contents of the file are below: -{record_patch_contents}""".format( - record_patch = record_patch, - record_patch_contents = record_patch_contents, - ) - print(warning_msg) # buildifier: disable=print - - return rctx.path(whl_patched) + repo_utils.execute_checked( + rctx, + op = "PatchWhl", + arguments = [ + powershell_exe, + "-NoProfile", + "-File", + str(script_path), + "-Output", + str(output), + ], + ) diff --git a/python/private/pypi/pep508_deps.bzl b/python/private/pypi/pep508_deps.bzl index e73f747bed..c004334d96 100644 --- a/python/private/pypi/pep508_deps.bzl +++ b/python/private/pypi/pep508_deps.bzl @@ -16,6 +16,7 @@ """ load("//python/private:normalize_name.bzl", "normalize_name") +load(":pep508_env.bzl", "create_env") load(":pep508_evaluate.bzl", "evaluate") load(":pep508_requirement.bzl", "requirement") @@ -115,7 +116,9 @@ def _resolve_extras(self_name, reqs, extras): # extras The empty string in the set is just a way to make the handling # of no extras and a single extra easier and having a set of {"", "foo"} # is equivalent to having {"foo"}. - extras = extras or [""] + # + # Use a dict as a set here to simplify operations. + extras = {x: None for x in (extras or [""])} self_reqs = [] for req in reqs: @@ -128,26 +131,37 @@ def _resolve_extras(self_name, reqs, extras): # easy to handle, lets do it. # # TODO @aignas 2023-12-08: add a test - extras = extras + req.extras + extras = extras | {x: None for x in req.extras} else: # process these in a separate loop self_reqs.append(req) - # A double loop is not strictly optimal, but always correct without recursion - for req in self_reqs: - if [True for extra in extras if evaluate(req.marker, env = {"extra": extra})]: - extras = extras + req.extras - else: - continue + for _ in range(10000): + # handles packages with up to 10000 recursive extras + new_extras = {} + for req in self_reqs: + if _evaluate_any(req, extras): + new_extras.update({x: None for x in req.extras}) + else: + continue - # Iterate through all packages to ensure that we include all of the extras from previously - # visited packages. - for req_ in self_reqs: - if [True for extra in extras if evaluate(req.marker, env = {"extra": extra})]: - extras = extras + req_.extras + num_extras_before = len(extras) + extras = extras | new_extras + num_extras_after = len(new_extras) + + if num_extras_before == num_extras_after: + break # Poor mans set - return sorted({x: None for x in extras}) + return sorted(extras) + +def _evaluate_any(req, extras): + env = create_env() + for extra in extras: + if evaluate(req.marker, env = env | {"extra": extra}): + return True + + return False def _add_reqs(deps, deps_select, dep, reqs, *, extras): for req in reqs: @@ -155,18 +169,23 @@ def _add_reqs(deps, deps_select, dep, reqs, *, extras): _add(deps, deps_select, dep) return + env = create_env() markers = {} + found_unconditional = False for req in reqs: for x in extras: - m = evaluate(req.marker, env = {"extra": x}, strict = False) + m = evaluate(req.marker, env = env | {"extra": x}, strict = False) if m == False: continue elif m == True: _add(deps, deps_select, dep) + found_unconditional = True break else: markers[m] = None continue + if found_unconditional: + break - if markers: + if markers and not found_unconditional: _add(deps, deps_select, dep, sorted(markers)) diff --git a/python/private/pypi/pep508_env.bzl b/python/private/pypi/pep508_env.bzl index 5031ebae12..9fe9dcaada 100644 --- a/python/private/pypi/pep508_env.bzl +++ b/python/private/pypi/pep508_env.bzl @@ -15,6 +15,7 @@ """This module is for implementing PEP508 environment definition. """ +load("//python/private:normalize_name.bzl", "normalize_name") load("//python/private:version.bzl", "version") _DEFAULT = "//conditions:default" @@ -215,9 +216,11 @@ def env(*, env = None, os, arch, python_version = "", extra = None): def create_env(): return { - # This is split by topic + # Per-variable normalization functions. Each entry maps a marker + # variable name to a function (value) -> normalized_value. "_aliases": { - "platform_machine": platform_machine_aliases, + "extra": normalize_name, + "platform_machine": lambda x: platform_machine_aliases.get(x, x), }, } diff --git a/python/private/pypi/pep508_evaluate.bzl b/python/private/pypi/pep508_evaluate.bzl index fe2cac965a..61e461a54f 100644 --- a/python/private/pypi/pep508_evaluate.bzl +++ b/python/private/pypi/pep508_evaluate.bzl @@ -300,12 +300,10 @@ def marker_expr(left, op, right, *, env, strict = True): left = left.strip("\"") if _ENV_ALIASES in env: - # On Windows, Linux, OSX different values may mean the same hardware, - # e.g. Python on Windows returns arm64, but on Linux returns aarch64. - # e.g. Python on Windows returns amd64, but on Linux returns x86_64. - # - # The following normalizes the values - left = env.get(_ENV_ALIASES, {}).get(var_name, {}).get(left, left) + # Normalize the literal value using per-variable normalization + # functions. This handles platform aliases (e.g. arm64 -> aarch64) + # and PEP 685 extra name normalization (e.g. db-backend -> db_backend). + left = env.get(_ENV_ALIASES, {}).get(var_name, lambda x: x)(left) else: var_name = left @@ -314,7 +312,7 @@ def marker_expr(left, op, right, *, env, strict = True): if _ENV_ALIASES in env: # See the note above on normalization - right = env.get(_ENV_ALIASES, {}).get(var_name, {}).get(right, right) + right = env.get(_ENV_ALIASES, {}).get(var_name, lambda x: x)(right) if var_name in _NON_VERSION_VAR_NAMES: return _env_expr(left, op, right) diff --git a/python/private/pypi/pep508_requirement.bzl b/python/private/pypi/pep508_requirement.bzl index b5be17f890..7552642572 100644 --- a/python/private/pypi/pep508_requirement.bzl +++ b/python/private/pypi/pep508_requirement.bzl @@ -45,7 +45,7 @@ def requirement(spec): extras_unparsed, _, _ = extras_unparsed.partition("]") for char in _STRIP: requires, _, _ = requires.partition(char) - extras = extras_unparsed.replace(" ", "").split(",") + extras = [normalize_name(e) for e in extras_unparsed.replace(" ", "").split(",") if e] name = requires.strip(" ") name = normalize_name(name) diff --git a/python/private/pypi/pip_compile.bzl b/python/private/pypi/pip_compile.bzl index 28923005df..58f7ba3a59 100644 --- a/python/private/pypi/pip_compile.bzl +++ b/python/private/pypi/pip_compile.bzl @@ -22,6 +22,15 @@ make it possible to have multiple tools inside the `pypi` directory load("//python:py_binary.bzl", _py_binary = "py_binary") load("//python:py_test.bzl", _py_test = "py_test") +# The `data` attribute does not allow duplicate labels, but user-provided `data` +# can overlap with labels added from attributes like `src`. +def _dedupe_data(data): + res = [] + for d in data: + if d not in res: + res.append(d) + return res + def pip_compile( name, srcs = None, @@ -39,6 +48,7 @@ def pip_compile( visibility = ["//visibility:private"], tags = None, constraints = [], + data = [], **kwargs): """Generates targets for managing pip dependencies with pip-compile (piptools). @@ -81,6 +91,7 @@ def pip_compile( tags: tagging attribute common to all build rules, passed to both the _test and .update rules. visibility: passed to both the _test and .update rules. constraints: a list of files containing constraints to pass to pip-compile with `--constraint`. + data: A list of labels to include as part of the `data` attribute in the generated `py_binary`. **kwargs: other bazel attributes passed to the "_test" rule. """ if len([x for x in [srcs, src, requirements_in] if x != None]) > 1: @@ -95,16 +106,18 @@ def pip_compile( requirements_txt = name + ".txt" if requirements_txt == None else requirements_txt + data = data or [] + # "Default" target produced by this macro # Allow a compile_pip_requirements rule to include another one in the data # for a requirements file that does `-r ../other/requirements.txt` native.filegroup( name = name, - srcs = kwargs.pop("data", []) + [requirements_txt], + srcs = data + [requirements_txt], visibility = visibility, ) - data = [name, requirements_txt] + srcs + [f for f in (requirements_linux, requirements_darwin, requirements_windows) if f != None] + constraints + data = _dedupe_data(data + [name, requirements_txt] + srcs + [f for f in (requirements_linux, requirements_darwin, requirements_windows) if f != None] + constraints) # Use the Label constructor so this is expanded in the context of the file # where it appears, which is to say, in @rules_python @@ -173,6 +186,7 @@ def pip_compile( name = name + ".update", env = env, python_version = kwargs.get("python_version", None), + target_compatible_with = kwargs.get("target_compatible_with", []), **attrs ) diff --git a/python/private/pypi/pip_repository.bzl b/python/private/pypi/pip_repository.bzl index 2cf20cd5a7..5fcd351958 100644 --- a/python/private/pypi/pip_repository.bzl +++ b/python/private/pypi/pip_repository.bzl @@ -18,12 +18,24 @@ load("@bazel_skylib//lib:sets.bzl", "sets") load("//python/private:normalize_name.bzl", "normalize_name") load("//python/private:repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "repo_utils") load("//python/private:text_util.bzl", "render") -load(":evaluate_markers.bzl", "evaluate_markers_py", EVALUATE_MARKERS_SRCS = "SRCS") load(":parse_requirements.bzl", "host_platform", "parse_requirements", "select_requirement") +load(":pep508_env.bzl", "env") load(":pip_repository_attrs.bzl", "ATTRS") +load(":pypi_repo_utils.bzl", "pypi_repo_utils") load(":render_pkg_aliases.bzl", "render_pkg_aliases") load(":requirements_files_by_platform.bzl", "requirements_files_by_platform") +_CONFIG_REPO_TEMPLATE = """"{name}__config" + whl_config_repo( + name = "{name}__config", + repo_prefix = "{name}_", + groups = all_requirement_groups, + whl_map = {{ + p: "" + for p in all_whl_requirements_by_package + }}, + )""" + def _get_python_interpreter_attr(rctx): """A helper function for getting the `python_interpreter` attribute or it's default @@ -54,7 +66,7 @@ def use_isolated(ctx, attr): use_isolated = attr.isolated # The environment variable will take precedence over the attribute - isolated_env = ctx.os.environ.get("RULES_PYTHON_PIP_ISOLATED", None) + isolated_env = ctx.getenv("RULES_PYTHON_PIP_ISOLATED", None) if isolated_env != None: if isolated_env.lower() in ("0", "false"): use_isolated = False @@ -72,6 +84,32 @@ exports_files(["requirements.bzl"]) def _pip_repository_impl(rctx): logger = repo_utils.logger(rctx) + python_interpreter = pypi_repo_utils.resolve_python_interpreter( + rctx, + python_interpreter = rctx.attr.python_interpreter, + python_interpreter_target = rctx.attr.python_interpreter_target, + ) + result = pypi_repo_utils.execute_checked( + rctx, + python = python_interpreter, + srcs = [], + op = "GetPythonVersion", + arguments = ["-c", "import sys; print(sys.version.split()[0])"], + ) + python_version = result.stdout.strip().splitlines()[-1] + platforms = [ + "linux_aarch64", + "linux_arm", + "linux_ppc", + "linux_riscv64", + "linux_s390x", + "linux_x86_64", + "osx_aarch64", + "osx_x86_64", + "windows_x86_64", + ] + + marker_env = env(os = rctx.os.name, arch = rctx.os.arch, python_version = python_version) requirements_by_platform = parse_requirements( rctx, requirements_by_platform = requirements_files_by_platform( @@ -81,30 +119,17 @@ def _pip_repository_impl(rctx): requirements_osx = rctx.attr.requirements_darwin, requirements_windows = rctx.attr.requirements_windows, extra_pip_args = rctx.attr.extra_pip_args, - platforms = [ - "linux_aarch64", - "linux_arm", - "linux_ppc", - "linux_s390x", - "linux_x86_64", - "osx_aarch64", - "osx_x86_64", - "windows_x86_64", - ], + platforms = platforms, ), extra_pip_args = rctx.attr.extra_pip_args, - evaluate_markers = lambda rctx, requirements: evaluate_markers_py( - rctx, - requirements = { - # NOTE @aignas 2025-07-07: because we don't distinguish between - # freethreaded and non-freethreaded, it is a 1:1 mapping. - req: {p: p for p in plats} - for req, plats in requirements.items() - }, - python_interpreter = rctx.attr.python_interpreter, - python_interpreter_target = rctx.attr.python_interpreter_target, - srcs = rctx.attr._evaluate_markers_srcs, - ), + platforms = { + p: struct( + env = marker_env, + whl_abi_tags = [], + whl_platform_tags = [], + ) + for p in platforms + }, extract_url_srcs = False, logger = logger, ) @@ -156,7 +181,7 @@ def _pip_repository_impl(rctx): imports = [ # NOTE: Maintain the order consistent with `buildifier` 'load("@rules_python//python:pip.bzl", "pip_utils")', - 'load("@rules_python//python/pip_install:pip_repository.bzl", "group_library", "whl_library")', + 'load("@rules_python//python/pip_install:pip_repository.bzl", "whl_config_repo", "whl_library")', ] annotations = {} @@ -185,15 +210,13 @@ def _pip_repository_impl(rctx): if rctx.attr.python_interpreter_target: config["python_interpreter_target"] = str(rctx.attr.python_interpreter_target) - if rctx.attr.experimental_target_platforms: - config["experimental_target_platforms"] = rctx.attr.experimental_target_platforms macro_tmpl = "@%s//{}:{}" % rctx.attr.name aliases = render_pkg_aliases( aliases = { pkg: rctx.attr.name + "_" + pkg - for pkg in bzl_packages or [] + for pkg in bzl_packages }, extra_hub_aliases = rctx.attr.extra_hub_aliases, requirement_cycles = requirement_cycles, @@ -202,14 +225,22 @@ def _pip_repository_impl(rctx): rctx.file(path, contents) rctx.file("BUILD.bazel", _BUILD_FILE_CONTENTS) + if rctx.attr.use_hub_alias_dependencies: + rctx.template( + "config.bzl", + rctx.attr._config_template, + substitutions = { + "%%PACKAGES%%": render.dict({ + pkg: None + for pkg in bzl_packages + }, value_repr = lambda x: "None"), + }, + ) + config_repo_template = repr(rctx.attr.name) + else: + config_repo_template = _CONFIG_REPO_TEMPLATE.format(name = rctx.attr.name) + rctx.template("requirements.bzl", rctx.attr._template, substitutions = { - " # %%GROUP_LIBRARY%%": """\ - group_repo = "{name}__groups" - group_library( - name = group_repo, - repo_prefix = "{name}_", - groups = all_requirement_groups, - )""".format(name = rctx.attr.name) if not rctx.attr.use_hub_alias_dependencies else "", "%%ALL_DATA_REQUIREMENTS%%": render.list([ macro_tmpl.format(p, "data") for p in bzl_packages @@ -225,6 +256,7 @@ def _pip_repository_impl(rctx): }), "%%ANNOTATIONS%%": render.dict(dict(sorted(annotations.items()))), "%%CONFIG%%": render.dict(dict(sorted(config.items()))), + "%%CONFIG_REPO%%": config_repo_template, "%%EXTRA_PIP_ARGS%%": json.encode(options), "%%IMPORTS%%": "\n".join(imports), "%%MACRO_TMPL%%": macro_tmpl, @@ -245,20 +277,15 @@ pip_repository = repository_rule( doc = """\ Optional annotations to apply to packages. Keys should be package names, with capitalization matching the input requirements file, and values should be -generated using the `package_name` macro. For example usage, see [this WORKSPACE -file](https://github.com/bazel-contrib/rules_python/blob/main/examples/pip_repository_annotations/WORKSPACE). +generated using the `package_name` macro. """, ), + _config_template = attr.label( + default = ":config.bzl.tmpl", + ), _template = attr.label( default = ":requirements.bzl.tmpl.workspace", ), - _evaluate_markers_srcs = attr.label_list( - default = EVALUATE_MARKERS_SRCS, - doc = """\ -The list of labels to use as SRCS for the marker evaluation code. This ensures that the -code will be re-evaluated when any of files in the default changes. -""", - ), **ATTRS ), doc = """Accepts a locked/compiled requirements file and installs the dependencies listed within. @@ -325,30 +352,6 @@ functionality for exposing [entry points][whl_ep] as `py_binary` targets as well [whl_ep]: https://packaging.python.org/specifications/entry-points/ -```starlark -load("@pypi//:requirements.bzl", "entry_point") - -alias( - name = "pip-compile", - actual = entry_point( - pkg = "pip-tools", - script = "pip-compile", - ), -) -``` - -Note that for packages whose name and script are the same, only the name of the package -is needed when calling the `entry_point` macro. - -```starlark -load("@pip//:requirements.bzl", "entry_point") - -alias( - name = "flake8", - actual = entry_point("flake8"), -) -``` - :::{rubric} Vendoring the requirements.bzl file :heading-level: 3 ::: diff --git a/python/private/pypi/pkg_aliases.bzl b/python/private/pypi/pkg_aliases.bzl index ac063fac48..b1c29c95ee 100644 --- a/python/private/pypi/pkg_aliases.bzl +++ b/python/private/pypi/pkg_aliases.bzl @@ -46,13 +46,17 @@ load( ) _NO_MATCH_ERROR_TEMPLATE = """\ -No matching wheel for current configuration's Python version. +No matching wheel for current configuration's Python version and platform. The current build configuration's Python version doesn't match any of the Python wheels available for this distribution. This distribution supports the following Python configuration settings: {config_settings} +As configured by the `pip.parse.target_platforms` attribute. Note that +`requirements_by_platform` only affects the Bazel host platform unless +`target_platforms` is also set. + To determine the current configuration's Python version, run: `bazel config ` (shown further below) @@ -181,7 +185,7 @@ def multiplatform_whl_aliases( aliases: {type}`str | dict[struct | str, str]`: The aliases to process. Any aliases that have the filename set will be converted to a dict of config settings to repo names. The - struct is created by {func}`whl_config_setting`. + struct is created by {bzl:obj}`whl_config_setting`. Returns: A dict with of config setting labels to repo names or the repo name itself. @@ -197,6 +201,7 @@ def multiplatform_whl_aliases( ret[alias] = repo continue + # This is if we are using `whl_config_setting` struct config_settings = get_config_settings( target_platforms = alias.target_platforms, python_version = alias.version, diff --git a/python/private/pypi/pypi_cache.bzl b/python/private/pypi/pypi_cache.bzl new file mode 100644 index 0000000000..d3a3034a79 --- /dev/null +++ b/python/private/pypi/pypi_cache.bzl @@ -0,0 +1,347 @@ +"""A cache for the PyPI index contents evaluation. + +This is design to work as the following: +- in-memory cache for results of PyPI index queries, so that we are not calling PyPI multiple times + for the same package for different hub repos. + +In the future the same will be used to: +- Store PyPI index query results as facts in the MODULE.bazel.lock file +""" + +load(":version_from_filename.bzl", "version_from_filename") + +# This value should be changed whenever the storage format changes. +# Changing it simply means the information cached in the lockfile has to be +# recomputed. +_FACT_VERSION = "v1" + +def pypi_cache(mctx = None, store = None): + """The cache for PyPI index queries. + + Currently the key is of the following structure: + (url, real_url, versions) + + Args: + mctx: The module context + store: The in-memory store, should implement dict interface for get and setdefault + + Returns: + A cache struct + """ + mcache = memory_cache(store) + fcache = facts_cache(getattr(mctx, "facts", None)) + + # buildifier: disable=uninitialized + self = struct( + _mcache = mcache, + _facts = fcache, + setdefault = lambda key, parsed_result: _pypi_cache_setdefault(self, key, parsed_result), + get = lambda key: _pypi_cache_get(self, key), + get_facts = lambda: _pypi_cache_get_facts(self), + ) + + # buildifier: enable=uninitialized + return self + +def _pypi_cache_setdefault(self, key, parsed_result): + """Store the value if not yet cached. + + Args: + self: {type}`struct` The self of this implementation. + key: {type}`str` The cache key, can be any string. + parsed_result: {type}`struct` The result of `parse_simpleapi_html` function. + + index_url and distribution is used to write to the MODULE.bazel.lock file as facts + real_index_url and distribution is used to write to in-memory cache to ensure that there are + no duplicate calls to the PyPI indexes + + Returns: + The `parse_result`. + """ + index_url, real_url, versions = key + self._mcache.setdefault(real_url, parsed_result) + if not versions or not self._facts: + return parsed_result + + # Filter the packages to only what is needed before writing to the facts cache + filtered = _filter_packages(parsed_result, versions) + return self._facts.setdefault(index_url, filtered) + +def _pypi_cache_get(self, key): + """Return the parsed result from the cache. + + Args: + self: {type}`struct` The self of this implementation. + key: {type}`str` The cache key, can be any string. + + Returns: + The {type}`struct` or `None` based on if the result is in the cache or not. + """ + index_url, real_url, versions = key + + # When retrieving from memory cache, filter down to only what is needed. If the + # cache is empty, we will attempt to read from facts, however, reading from memory + # first allows us to not parse the contents of the lock file that may add up. + cached = _filter_packages(self._mcache.get(real_url), versions) + if not self._facts: + return cached + + if not cached and versions: + # Could not get from in-memory, read from lockfile facts + cached = self._facts.get(index_url, versions) + else: + # We might be using something from memory that is not yet stored in facts (e.g. we processed + # the requirements.txt for one Python version and the deps got cached, but new python + # version means different deps, which may add extras. + self._facts.setdefault(index_url, cached) + + return cached + +def _pypi_cache_get_facts(self): + if not self._facts: + return {} + + return self._facts.facts + +def memory_cache(cache = None): + """SimpleAPI cache for making fewer calls. + + We are using the `real_url` as the key in the cache functions on purpose in order to get the + best possible cache hits. + + Args: + cache: the storage to store things in memory. + + Returns: + struct with 2 methods, `get` and `setdefault`. + """ + if cache == None: + cache = {} + + return struct( + get = lambda real_url: cache.get(real_url), + setdefault = lambda real_url, value: cache.setdefault(real_url, value), + ) + +def _filter_packages(dists, requested_versions): + if dists == None or not requested_versions: + return dists + + if type(dists) == "dict": + result = { + pkg: url + for pkg, url in dists.items() + if pkg in requested_versions + } + return result if result else None + + sha256s_by_version = {} + whls = {} + sdists = {} + + for sha256, d in dists.sdists.items(): + if d.version not in requested_versions: + continue + + sdists[sha256] = d + sha256s_by_version.setdefault(d.version, []).append(sha256) + + for sha256, d in dists.whls.items(): + if d.version not in requested_versions: + continue + + whls[sha256] = d + sha256s_by_version.setdefault(d.version, []).append(sha256) + + if not whls and not sdists: + # TODO @aignas 2026-03-08: add logging + #print("WARN: no dists matched for versions {}".format(requested_versions)) + return None + + return struct( + whls = whls, + sdists = sdists, + sha256s_by_version = { + k: sorted(v) + for k, v in sha256s_by_version.items() + }, + ) + +def facts_cache(known_facts, facts_version = _FACT_VERSION): + """The facts cache. + + Here we have a way to store things as facts and the main thing to keep in mind is that we should + not use the real_url in case it contains credentials in it (e.g. is of form `https://:@`). + + Args: + known_facts: An opaque object coming from {obj}`module_ctx.facts`. + facts_version: {type}`str` the version of the facts schema, used for short-circuiting. + + Returns: + A struct that has: + * `get` method for getting values from the facts cache. + * `setdefault` method for setting values in the cache. + * `facts` attribute that should be passed to the {obj}`module_ctx.extension_metadata` to persist facts. + """ + if known_facts == None: + return None + + facts = {} + + return struct( + get = lambda index_url, versions: _get_from_facts( + facts, + known_facts, + index_url, + versions, + facts_version, + ), + setdefault = lambda url, value: _store_facts(facts, facts_version, url, value), + known_facts = known_facts, + facts = facts, + ) + +def _get_from_facts(facts, known_facts, index_url, requested_versions, facts_version): + if known_facts.get("fact_version") != facts_version: + # cannot trust known facts, different version that we know how to parse + return None + + if type(requested_versions) == "dict": + result = _filter_packages( + dists = known_facts.get("index_urls", {}).get(index_url, {}), + requested_versions = requested_versions, + ) + if result: + if len(result) != len(requested_versions): + # If the results are incomplete, return None, so that we can + # fetch sources from the internet again. + return None + + # Only persist the accessed (requested) packages. Packages that + # exist in known_facts but are not in requested_versions (e.g. + # removed from all requirements files) are dropped from the + # computed facts so they get cleaned up from the lockfile. + _store_facts(facts, facts_version, index_url, result) + return result + + known_sources = {} + + root_url, _, distribution = index_url.rstrip("/").rpartition("/") + distribution = distribution.rstrip("/") + root_url = root_url.rstrip("/") + + retrieved_versions = {} + + for url, sha256 in known_facts.get("dist_hashes", {}).get(root_url, {}).get(distribution, {}).items(): + filename = known_facts.get("dist_filenames", {}).get(root_url, {}).get(distribution, {}).get(sha256) + if not filename: + _, _, filename = url.rpartition("/") + + version = version_from_filename(filename) + if version not in requested_versions: + # TODO @aignas 2026-01-21: do the check by requested shas at some point + # We don't have sufficient info in the lock file, need to call the API + # + continue + + retrieved_versions[version] = True + + if filename.endswith(".whl"): + dists = known_sources.setdefault("whls", {}) + else: + dists = known_sources.setdefault("sdists", {}) + + known_sources.setdefault("sha256s_by_version", {}).setdefault(version, []).append(sha256) + + dists.setdefault(sha256, struct( + sha256 = sha256, + filename = filename, + version = version, + metadata_url = "", + metadata_sha256 = "", + url = url, + yanked = known_facts.get("dist_yanked", {}).get(root_url, {}).get(distribution, {}).get(sha256), + )) + + if not known_sources: + # We found nothing in facts + return None + + if len(requested_versions) != len(retrieved_versions): + # If the results are incomplete, then return None, so that we can fetch sources from the + # internet again. + return None + + output = struct( + whls = known_sources.get("whls", {}), + sdists = known_sources.get("sdists", {}), + sha256s_by_version = { + k: sorted(v) + for k, v in known_sources.get("sha256s_by_version", {}).items() + }, + ) + + # Persist these facts for the next run because we have used them. + return _store_facts(facts, facts_version, index_url, output) + +def _store_facts(facts, fact_version, index_url, value): + """Store values as facts in the lock file. + + The main idea is to ensure that the lock file is small and it is only + storing what we would need to fetch from the internet. Any derivative + information we can get from this that can be achieved using pure Starlark + functions should be done in Starlark. + """ + if not value: + return value + + facts["fact_version"] = fact_version + + if type(value) == "dict": + # facts: { + # "index_urls": { + # "": { + # "": "", + # }, + # }, + # }, + for pkg, url in value.items(): + facts.setdefault("index_urls", {}).setdefault(index_url, {})[pkg] = url + return value + + root_url, _, distribution = index_url.rstrip("/").rpartition("/") + distribution = distribution.rstrip("/") + root_url = root_url.rstrip("/") + + # The schema is + # facts: { + # "dist_hashes": { + # "": { + # "": { + # "": "", + # }, + # }, + # }, + # "dist_filenames": { + # "": { + # "": { + # "": "", # if it is different from the URL + # }, + # }, + # }, + # "dist_yanked": { + # "": { + # "": { + # "": "", # if the package is yanked + # }, + # }, + # }, + # }, + for sha256, d in (value.sdists | value.whls).items(): + facts.setdefault("dist_hashes", {}).setdefault(root_url, {}).setdefault(distribution, {}).setdefault(d.url, sha256) + if not d.url.endswith(d.filename): + facts.setdefault("dist_filenames", {}).setdefault(root_url, {}).setdefault(distribution, {}).setdefault(d.url, d.filename) + if d.yanked != None: + facts.setdefault("dist_yanked", {}).setdefault(root_url, {}).setdefault(distribution, {}).setdefault(sha256, d.yanked) + + return value diff --git a/python/private/pypi/pypi_repo_utils.bzl b/python/private/pypi/pypi_repo_utils.bzl index bb2acc850a..8ec7bd1dbe 100644 --- a/python/private/pypi/pypi_repo_utils.bzl +++ b/python/private/pypi/pypi_repo_utils.bzl @@ -16,6 +16,7 @@ load("@bazel_skylib//lib:types.bzl", "types") load("//python/private:repo_utils.bzl", "repo_utils") +load("//python/private:util.bzl", "is_importable_name") def _get_python_interpreter_attr(mrctx, *, python_interpreter = None): """A helper function for getting the `python_interpreter` attribute or it's default @@ -107,9 +108,8 @@ def _construct_pypath(mrctx, *, entries): def _execute_prep(mrctx, *, python, srcs, **kwargs): for src in srcs: # This will ensure that we will re-evaluate the bzlmod extension or - # refetch the repository_rule when the srcs change. This should work on - # Bazel versions without `mrctx.watch` as well. - repo_utils.watch(mrctx, mrctx.path(src)) + # refetch the repository_rule when the srcs change. + mrctx.watch(mrctx.path(src)) environment = kwargs.pop("environment", {}) pythonpath = environment.get("PYTHONPATH", "") @@ -162,9 +162,45 @@ def _execute_checked_stdout(mrctx, *, python, srcs, **kwargs): **_execute_prep(mrctx, python = python, srcs = srcs, **kwargs) ) +def _find_namespace_package_files(rctx, install_dir): + """Finds all `__init__.py` files that belong to namespace packages. + + A `__init__.py` file belongs to a namespace package if it contains `__path__ =`, + `pkgutil`, and `extend_path(`. + + Args: + rctx (repository_ctx): The repository context. + install_dir (path): The path to the install directory. + + Returns: + list[str]: A list of relative paths to `__init__.py` files that belong + to namespace packages. + """ + + namespace_package_files = [] + for top_level_dir in install_dir.readdir(): + if not is_importable_name(top_level_dir.basename): + continue + init_py = top_level_dir.get_child("__init__.py") + if not init_py.exists: + continue + content = rctx.read(init_py) + + # Look for code resembling the pkgutil namespace setup code: + # __path__ = __import__("pkgutil").extend_path(__path__, __name__) + if ("__path__ =" in content and + "pkgutil" in content and + "extend_path(" in content): + namespace_package_files.append( + repo_utils.repo_root_relative_path(rctx, init_py), + ) + + return namespace_package_files + pypi_repo_utils = struct( construct_pythonpath = _construct_pypath, execute_checked = _execute_checked, execute_checked_stdout = _execute_checked_stdout, + find_namespace_package_files = _find_namespace_package_files, resolve_python_interpreter = _resolve_python_interpreter, ) diff --git a/python/private/pypi/repack_whl.ps1 b/python/private/pypi/repack_whl.ps1 new file mode 100644 index 0000000000..2f3b4ada0f --- /dev/null +++ b/python/private/pypi/repack_whl.ps1 @@ -0,0 +1,7 @@ +param( + [string]$Output +) + +$files = Get-ChildItem -Path . -Exclude 'tmp.zip', $Output +Compress-Archive -Path $files -DestinationPath 'tmp.zip' -Force +Move-Item -Path 'tmp.zip' -Destination $Output -Force diff --git a/python/private/pypi/repack_whl.py b/python/private/pypi/repack_whl.py deleted file mode 100644 index 519631f272..0000000000 --- a/python/private/pypi/repack_whl.py +++ /dev/null @@ -1,186 +0,0 @@ -# Copyright 2023 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. - -""" -Regenerate a whl file after patching and cleanup the patched contents. - -This script will take contents of the current directory and create a new wheel -out of it and will remove all files that were written to the wheel. -""" - -from __future__ import annotations - -import argparse -import csv -import difflib -import logging -import pathlib -import sys -import tempfile - -from tools.wheelmaker import _WhlFile - -# NOTE: Implement the following matching of what goes into the RECORD -# https://peps.python.org/pep-0491/#the-dist-info-directory -_EXCLUDES = [ - "RECORD", - "INSTALLER", - "RECORD.jws", - "RECORD.p7s", - "REQUESTED", -] - -_DISTINFO = "dist-info" - - -def _unidiff_output(expected, actual, record): - """ - Helper function. Returns a string containing the unified diff of two - multiline strings. - """ - - expected = expected.splitlines(1) - actual = actual.splitlines(1) - - diff = difflib.unified_diff( - expected, actual, fromfile=f"a/{record}", tofile=f"b/{record}" - ) - - return "".join(diff) - - -def _files_to_pack(dir: pathlib.Path, want_record: str) -> list[pathlib.Path]: - """Check that the RECORD file entries are correct and print a unified diff on failure.""" - - # First get existing files by using the RECORD file - got_files = [] - got_distinfos = [] - for row in csv.reader(want_record.splitlines()): - rec = row[0] - path = dir / rec - - if not path.exists(): - # skip files that do not exist as they won't be present in the final - # RECORD file. - continue - - if not path.parent.name.endswith(_DISTINFO): - got_files.append(path) - elif path.name not in _EXCLUDES: - got_distinfos.append(path) - - # Then get extra files present in the directory but not in the RECORD file - extra_files = [] - extra_distinfos = [] - for path in dir.rglob("*"): - if path.is_dir(): - continue - - elif path.parent.name.endswith(_DISTINFO): - if path.name in _EXCLUDES: - # NOTE: we implement the following matching of what goes into the RECORD - # https://peps.python.org/pep-0491/#the-dist-info-directory - continue - elif path not in got_distinfos: - extra_distinfos.append(path) - - elif path not in got_files: - extra_files.append(path) - - # sort the extra files for reproducibility - extra_files.sort() - extra_distinfos.sort() - - # This order ensures that the structure of the RECORD file is always the - # same and ensures smaller patchsets to the RECORD file in general - return got_files + extra_files + got_distinfos + extra_distinfos - - -def main(sys_argv): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "whl_path", - type=pathlib.Path, - help="The original wheel file that we have patched.", - ) - parser.add_argument( - "--record-patch", - type=pathlib.Path, - help="The output path that we are going to write the RECORD file patch to.", - ) - parser.add_argument( - "output", - type=pathlib.Path, - help="The output path that we are going to write a new file to.", - ) - args = parser.parse_args(sys_argv) - - cwd = pathlib.Path.cwd() - logging.debug("=" * 80) - logging.debug("Repackaging the wheel") - logging.debug("=" * 80) - - with tempfile.TemporaryDirectory(dir=cwd) as tmpdir: - patched_wheel_dir = cwd / tmpdir - logging.debug(f"Created a tmpdir: {patched_wheel_dir}") - - excludes = [args.whl_path, patched_wheel_dir] - - logging.debug("Moving whl contents to the newly created tmpdir") - for p in cwd.glob("*"): - if p in excludes: - logging.debug(f"Ignoring: {p}") - continue - - rel_path = p.relative_to(cwd) - dst = p.rename(patched_wheel_dir / rel_path) - logging.debug(f"mv {p} -> {dst}") - - distinfo_dir = next(iter(patched_wheel_dir.glob("*dist-info"))) - logging.debug(f"Found dist-info dir: {distinfo_dir}") - record_path = distinfo_dir / "RECORD" - record_contents = record_path.read_text() if record_path.exists() else "" - distribution_prefix = distinfo_dir.with_suffix("").name - - with _WhlFile( - args.output, mode="w", distribution_prefix=distribution_prefix - ) as out: - for p in _files_to_pack(patched_wheel_dir, record_contents): - rel_path = p.relative_to(patched_wheel_dir) - out.add_file(str(rel_path), p) - - logging.debug(f"Writing RECORD file") - got_record = out.add_recordfile().decode("utf-8", "surrogateescape") - - if got_record == record_contents: - logging.info(f"Created a whl file: {args.output}") - return - - record_diff = _unidiff_output( - record_contents, - got_record, - out.distinfo_path("RECORD"), - ) - args.record_patch.write_text(record_diff) - logging.warning( - f"Please apply patch to the RECORD file ({args.record_patch}):\n{record_diff}" - ) - - -if __name__ == "__main__": - logging.basicConfig( - format="%(module)s: %(levelname)s: %(message)s", level=logging.DEBUG - ) - - sys.exit(main(sys.argv[1:])) diff --git a/python/private/pypi/requirements.bzl.tmpl.workspace b/python/private/pypi/requirements.bzl.tmpl.workspace index 2f4bcd6916..f61a5271f6 100644 --- a/python/private/pypi/requirements.bzl.tmpl.workspace +++ b/python/private/pypi/requirements.bzl.tmpl.workspace @@ -52,7 +52,8 @@ def install_deps(**whl_library_kwargs): for requirement in group_requirements } - # %%GROUP_LIBRARY%% + config_repo = %%CONFIG_REPO%% + config_load = "@{}//:config.bzl".format(config_repo) # Install wheels which may be participants in a group whl_config = dict(_config) @@ -68,5 +69,6 @@ def install_deps(**whl_library_kwargs): group_name = group_name, group_deps = group_deps, annotation = _get_annotation(requirement), + config_load = config_load, **whl_config ) diff --git a/python/private/pypi/requirements_files_by_platform.bzl b/python/private/pypi/requirements_files_by_platform.bzl index 356bd4416e..dcdb6128a7 100644 --- a/python/private/pypi/requirements_files_by_platform.bzl +++ b/python/private/pypi/requirements_files_by_platform.bzl @@ -14,6 +14,7 @@ """Get the requirement files by platform.""" +load(":argparse.bzl", "argparse") load(":whl_target_platforms.bzl", "whl_target_platforms") def _default_platforms(*, filter, platforms): @@ -46,33 +47,9 @@ def _default_platforms(*, filter, platforms): return match def _platforms_from_args(extra_pip_args): - platform_values = [] - - if not extra_pip_args: - return platform_values - - for arg in extra_pip_args: - if platform_values and platform_values[-1] == "": - platform_values[-1] = arg - continue - - if arg == "--platform": - platform_values.append("") - continue - - if not arg.startswith("--platform"): - continue - - _, _, plat = arg.partition("=") - if not plat: - _, _, plat = arg.partition(" ") - if plat: - platform_values.append(plat) - else: - platform_values.append("") - + platform_values = argparse.platform(extra_pip_args, []) if not platform_values: - return [] + return platform_values platforms = { p.target_platform: None @@ -140,9 +117,12 @@ def requirements_files_by_platform( platforms_from_args = _platforms_from_args(extra_pip_args) if logger: - logger.debug(lambda: "Platforms from pip args: {}".format(platforms_from_args)) + logger.debug(lambda: "Platforms from pip args: {} (from {})".format(platforms_from_args, extra_pip_args)) - default_platforms = platforms + input_platforms = platforms + default_platforms = [_platform(p, python_version) for p in platforms] + if logger: + logger.debug(lambda: "Input platforms: {}".format(input_platforms)) if platforms_from_args: lock_files = [ @@ -174,6 +154,7 @@ def requirements_files_by_platform( platform for filter_or_platform in specifier.split(",") for platform in (_default_platforms(filter = filter_or_platform, platforms = platforms) if filter_or_platform.endswith("*") else [filter_or_platform]) + if _platform(platform, python_version) in default_platforms ] for file, specifier in requirements_by_platform.items() }.items() @@ -227,11 +208,11 @@ def requirements_files_by_platform( configured_platforms[p] = file elif logger: - logger.warn(lambda: "File {} will be ignored because there are no configured platforms: {}".format( + logger.info(lambda: "File {} will be ignored because there are no configured platforms: {} out of {}".format( file, default_platforms, + input_platforms, )) - continue if logger: logger.debug(lambda: "Configured platforms for file {} are {}".format(file, plats)) @@ -256,4 +237,8 @@ def requirements_files_by_platform( for plat, file in requirements.items(): ret.setdefault(file, []).append(_platform(plat, python_version = python_version)) + for file, _plats in files_by_platform: + if file not in ret and _plats != None: + ret[file] = [] + return ret diff --git a/python/private/pypi/requirements_parser/resolve_target_platforms.py b/python/private/pypi/requirements_parser/resolve_target_platforms.py index accacf5bfa..96607ee240 100755 --- a/python/private/pypi/requirements_parser/resolve_target_platforms.py +++ b/python/private/pypi/requirements_parser/resolve_target_platforms.py @@ -21,7 +21,6 @@ import pathlib from packaging.requirements import Requirement - from python.private.pypi.whl_installer.platform import Platform INPUT_HELP = """\ diff --git a/python/private/pypi/simpleapi_download.bzl b/python/private/pypi/simpleapi_download.bzl index 52ff02a178..5377a08093 100644 --- a/python/private/pypi/simpleapi_download.bzl +++ b/python/private/pypi/simpleapi_download.bzl @@ -16,12 +16,11 @@ A file that houses private functions used in the `bzlmod` extension with the same name. """ -load("@bazel_features//:features.bzl", "bazel_features") load("//python/private:auth.bzl", _get_auth = "get_auth") load("//python/private:envsubst.bzl", "envsubst") load("//python/private:normalize_name.bzl", "normalize_name") -load("//python/private:text_util.bzl", "render") load(":parse_simpleapi_html.bzl", "parse_simpleapi_html") +load(":urllib.bzl", "urllib") def simpleapi_download( ctx, @@ -34,29 +33,34 @@ def simpleapi_download( _fail = fail): """Download Simple API HTML. + First it queries all of the indexes for available packages and then it downloads the contents of + the per-package URLs and sha256 values. This is to enable us to use bazel_downloader with + `requirements.txt` files. As a side effect we also are able to "cross-compile" by fetching the + right wheel for the right target platform through the information that we retrieve here. + Args: ctx: The module_ctx or repository_ctx. attr: Contains the parameters for the download. They are grouped into a struct for better clarity. It must have attributes: - * index_url: str, the index. - * index_url_overrides: dict[str, str], the index overrides for - separate packages. - * extra_index_urls: Extra index URLs that will be looked up after - the main is looked up. + * index_url: str, the index, or if `extra_index_urls` are passed, the default index. + * index_url_overrides: dict[str, str], the index overrides for separate packages. + * extra_index_urls: Will be looked at in the order they are defined and the first match + wins. This is similar to what uv does, see + https://docs.astral.sh/uv/concepts/indexes/#searching-across-multiple-indexes. + PRs for implementing other strategies are welcome. * sources: list[str], the sources to download things for. Each value is the contents of requirements files. * envsubst: list[str], the envsubst vars for performing substitution in index url. * netrc: The netrc parameter for ctx.download, see http_file for docs. * auth_patterns: The auth_patterns parameter for ctx.download, see http_file for docs. - cache: A dictionary that can be used as a cache between calls during a - single evaluation of the extension. We use a dictionary as a cache - so that we can reuse calls to the simple API when evaluating the - extension. Using the canonical_id parameter of the module_ctx would - deposit the simple API responses to the bazel cache and that is - undesirable because additions to the PyPI index would not be - reflected when re-evaluating the extension unless we do - `bazel clean --expunge`. + cache: An opaque object used to cache call results. For implementation + see ./pypi_cache.bzl file. We use the canonical_id parameter for the key + value to ensure that distribution fetches from different indexes do not cause + cache collisions, because the index may return different locations from where + the files should be downloaded. We are not using the built-in cache in the + `download` function because the index may get updated at any time and we need + to be able to refresh the data. parallel_download: A boolean to enable usage of bazel 7.1 non-blocking downloads. read_simpleapi: a function for reading and parsing of the SimpleAPI contents. Used in tests. @@ -66,118 +70,149 @@ def simpleapi_download( Returns: dict of pkg name to the parsed HTML contents - a list of structs. """ + if not attr.sources: + return {} + index_url_overrides = { normalize_name(p): i for p, i in (attr.index_url_overrides or {}).items() } + sources = { + normalize_name(pkg): versions + for pkg, versions in attr.sources.items() + } - download_kwargs = {} - if bazel_features.external_deps.download_has_block_param: - download_kwargs["block"] = not parallel_download - - # NOTE @aignas 2024-03-31: we are not merging results from multiple indexes - # to replicate how `pip` would handle this case. - contents = {} - index_urls = [attr.index_url] + attr.extra_index_urls read_simpleapi = read_simpleapi or _read_simpleapi - found_on_index = {} - warn_overrides = False ctx.report_progress("Fetch package lists from PyPI index") - for i, index_url in enumerate(index_urls): - if i != 0: - # Warn the user about a potential fix for the overrides - warn_overrides = True - async_downloads = {} - sources = [pkg for pkg in attr.sources if pkg not in found_on_index] - for pkg in sources: - pkg_normalized = normalize_name(pkg) - result = read_simpleapi( - ctx = ctx, - url = "{}/{}/".format( - index_url_overrides.get(pkg_normalized, index_url).rstrip("/"), - pkg, - ), - attr = attr, - cache = cache, - get_auth = get_auth, - **download_kwargs - ) - if hasattr(result, "wait"): - # We will process it in a separate loop: - async_downloads[pkg] = struct( - pkg_normalized = pkg_normalized, - wait = result.wait, - ) - elif result.success: - contents[pkg_normalized] = result.output - found_on_index[pkg] = index_url - - if not async_downloads: - continue + # NOTE: we are not merging results from multiple indexes to replicate how `pip` would + # handle this case. What we do is we select a particular index to download the packages + dist_urls = _get_dist_urls( + ctx, + default_index = attr.index_url, + index_urls = attr.extra_index_urls, + index_url_overrides = index_url_overrides, + sources = sources, + read_simpleapi = read_simpleapi, + cache = cache, + get_auth = get_auth, + attr = attr, + block = not parallel_download, + _fail = _fail, + ) + + ctx.report_progress("Fetching package URLs from PyPI index") + downloads = {} + contents = {} + for pkg, url in dist_urls.items(): + result = read_simpleapi( + ctx = ctx, + attr = attr, + url = url, + cache = cache, + versions = sources[pkg], + get_auth = get_auth, + block = not parallel_download, + parse_index = False, + ) + if hasattr(result, "wait"): + # We will process it in a separate loop: + downloads[pkg] = result + else: + contents[pkg] = _with_index_url(url, result.output) + + for pkg, d in downloads.items(): # If we use `block` == False, then we need to have a second loop that is # collecting all of the results as they were being downloaded in parallel. - for pkg, download in async_downloads.items(): - result = download.wait() - - if result.success: - contents[download.pkg_normalized] = result.output - found_on_index[pkg] = index_url - - failed_sources = [pkg for pkg in attr.sources if pkg not in found_on_index] - if failed_sources: - pkg_index_urls = { - pkg: index_url_overrides.get( - normalize_name(pkg), - index_urls, - ) - for pkg in failed_sources - } + contents[pkg] = _with_index_url(dist_urls[pkg], d.wait().output) - _fail( - """ -Failed to download metadata of the following packages from urls: -{pkg_index_urls} + return contents -If you would like to skip downloading metadata for these packages please add 'simpleapi_skip={failed_sources}' to your 'pip.parse' call. -""".format( - pkg_index_urls = render.dict(pkg_index_urls), - failed_sources = render.list(failed_sources), - ), +def _get_dist_urls(ctx, *, default_index, index_urls, index_url_overrides, sources, read_simpleapi, attr, block, _fail = fail, **kwargs): + # Ensure the value is not frozen + index_urls = [] + (index_urls or []) + if default_index not in index_urls: + index_urls.append(default_index) + + index_url_overrides = index_url_overrides or {} + if index_url_overrides or len(index_urls) == 1: + # Let's not call the index at all and just assume that all of the overrides have been + # specified or there is only a single index and there is no need to download anything + return { + pkg: _normalize_url("{}/{}/".format( + index_url_overrides.get(pkg, default_index), + pkg.replace("_", "-"), # Use the official normalization for URLs + )) + for pkg in sources + } + + downloads = {} + results = {} + + for index_url in index_urls: + download = read_simpleapi( + ctx = ctx, + attr = attr, + url = _normalize_url("{index_url}/".format(index_url = index_url)), + parse_index = True, + versions = {pkg: None for pkg in sources}, + block = block, + **kwargs ) - return None + if hasattr(download, "wait"): + downloads[index_url] = download + else: + results[index_url] = download - if warn_overrides: - index_url_overrides = { - pkg: found_on_index[pkg] - for pkg in attr.sources - if found_on_index[pkg] != attr.index_url - } + for index_url, download in downloads.items(): + results[index_url] = download.wait() - if index_url_overrides: - # buildifier: disable=print - print("You can use the following `index_url_overrides` to avoid the 404 warnings:\n{}".format( - render.dict(index_url_overrides), + found_on_index = {} + for index_url, result in results.items(): + for pkg in sources: + if pkg in found_on_index: + # We have already found the package, skip searching for it in + # other indexes. + # + # If we wanted to merge all of the index results, we would have to continue here + # and in the outer function process merging of the results. + continue + + found = result.output.get(pkg) + if not found: + continue + + # Ignore the URL here because we know how to construct it. + + found_on_index[pkg] = _normalize_url("{}/{}/".format( + index_url, + pkg.replace("_", "-"), # Use the official normalization for URLs )) - return contents + return found_on_index + +def _normalize_url(url): + return urllib.strip_empty_path_segments(url) -def _read_simpleapi(ctx, url, attr, cache, get_auth = None, **download_kwargs): +def _read_simpleapi(ctx, url, attr, cache, versions, parse_index, get_auth = None, **download_kwargs): """Read SimpleAPI. Args: ctx: The module_ctx or repository_ctx. - url: str, the url parameter that can be passed to ctx.download. + url: {type}`str`, the url parameter that can be passed to ctx.download. attr: The attribute that contains necessary info for downloading. The following attributes must be present: - * envsubst: The envsubst values for performing substitutions in the URL. - * netrc: The netrc parameter for ctx.download, see http_file for docs. + * envsubst: {type}`dict[str, str]` for performing substitutions in the URL. + * netrc: The netrc parameter for ctx.download, see {obj}`http_file` for docs. * auth_patterns: The auth_patterns parameter for ctx.download, see - http_file for docs. - cache: A dict for storing the results. + {obj}`http_file` for docs. + cache: {type}`struct` the `pypi_cache` instance. + versions: {type}`list[str] The versions that have been requested. get_auth: A function to get auth information. Used in tests. + parse_index: {type}`bool` Whether to parse the content as a root index page + (e.g. `/simple/`) instead of a package-specific page. **download_kwargs: Any extra params to ctx.download. Note that output and auth will be passed for you. @@ -185,20 +220,12 @@ def _read_simpleapi(ctx, url, attr, cache, get_auth = None, **download_kwargs): A similar object to what `download` would return except that in result.out will be the parsed simple api contents. """ - # NOTE @aignas 2024-03-31: some of the simple APIs use relative URLs for - # the whl location and we cannot handle multiple URLs at once by passing - # them to ctx.download if we want to correctly handle the relative URLs. - # TODO: Add a test that env subbed index urls do not leak into the lock file. - - real_url = strip_empty_path_segments(envsubst( - url, - attr.envsubst, - ctx.getenv if hasattr(ctx, "getenv") else ctx.os.environ.get, - )) + real_url = _normalize_url(envsubst(url, attr.envsubst, ctx.getenv)) - cache_key = real_url - if cache_key in cache: - return struct(success = True, output = cache[cache_key]) + cache_key = (url, real_url, versions) + cached_result = cache.get(cache_key) + if cached_result: + return struct(success = True, output = cached_result) output_str = envsubst( url, @@ -222,48 +249,51 @@ def _read_simpleapi(ctx, url, attr, cache, get_auth = None, **download_kwargs): url = [real_url], output = output, auth = get_auth(ctx, [real_url], ctx_attr = attr), - allow_fail = True, **download_kwargs ) if download_kwargs.get("block") == False: # Simulate the same API as ctx.download has return struct( - wait = lambda: _read_index_result(ctx, download.wait(), output, real_url, cache, cache_key), + wait = lambda: _read_index_result( + ctx, + result = download.wait(), + output = output, + cache = cache, + cache_key = cache_key, + parse_index = parse_index, + ), ) - return _read_index_result(ctx, download, output, real_url, cache, cache_key) - -def strip_empty_path_segments(url): - """Removes empty path segments from a URL. Does nothing for urls with no scheme. - - Public only for testing. - - Args: - url: The url to remove empty path segments from - - Returns: - The url with empty path segments removed and any trailing slash preserved. - If the url had no scheme it is returned unchanged. - """ - scheme, _, rest = url.partition("://") - if rest == "": - return url - stripped = "/".join([p for p in rest.split("/") if p]) - if url.endswith("/"): - return "{}://{}/".format(scheme, stripped) - else: - return "{}://{}".format(scheme, stripped) + return _read_index_result( + ctx, + result = download, + output = output, + cache = cache, + cache_key = cache_key, + parse_index = parse_index, + ) -def _read_index_result(ctx, result, output, url, cache, cache_key): +def _read_index_result(ctx, *, result, output, cache, cache_key, parse_index): if not result.success: return struct(success = False) content = ctx.read(output) - output = parse_simpleapi_html(url = url, content = content) + output = parse_simpleapi_html(content = content, parse_index = parse_index) if output: cache.setdefault(cache_key, output) - return struct(success = True, output = output, cache_key = cache_key) + return struct(success = True, output = output) else: return struct(success = False) + +def _with_index_url(index_url, values): + if not values: + return values + + return struct( + sdists = values.sdists, + whls = values.whls, + sha256s_by_version = values.sha256s_by_version, + index_url = index_url, + ) diff --git a/python/private/pypi/unified_hub_repo.bzl b/python/private/pypi/unified_hub_repo.bzl new file mode 100644 index 0000000000..cbe150575a --- /dev/null +++ b/python/private/pypi/unified_hub_repo.bzl @@ -0,0 +1,81 @@ +"""Repository rule for creating the Unified PyPI Hub.""" + +load("//python/private:text_util.bzl", "render") + +_ROOT_BUILD_TMPL = """\ +load("@rules_python//python/private/pypi:unified_hub_setup.bzl", "define_venv_flag_config_settings") + +package(default_visibility = ["//visibility:public"]) + +define_venv_flag_config_settings( + name = "venv_config_settings", + hubs = {hubs}, +) +""" + +_PKG_BUILD_TMPL = """\ +load("@rules_python//python/private/pypi:unified_hub_setup.bzl", "define_pypi_package_targets") + +package(default_visibility = ["//visibility:public"]) + +define_pypi_package_targets( + name = "{pkg_name}", + default_hub = {default_hub}, + extra_aliases = {extra_aliases}, + hubs = {hubs}, + pkg_hubs = {pkg_hubs}, +) +""" + +def _unified_hub_repo_impl(rctx): + hubs = rctx.attr.hubs + default_hub = rctx.attr.default_hub or None + + # 1. Generate Root BUILD.bazel with shared config settings + rctx.file( + "BUILD.bazel", + _ROOT_BUILD_TMPL.format(hubs = hubs), + ) + + # 2. Organize extra aliases by package + extra_aliases_by_pkg = {} + for qual_alias, alias_hubs in rctx.attr.extra_aliases.items(): + if ":" not in qual_alias: + fail("extra_aliases keys must be in 'pkg:alias' format.") + pkg, alias = qual_alias.split(":", 1) + extra_aliases_by_pkg.setdefault(pkg, {})[alias] = alias_hubs + + # 3. Generate package subpackages + for pkg_name, pkg_hubs in rctx.attr.packages.items(): + extra_aliases = extra_aliases_by_pkg.get(pkg_name, {}) + rctx.file( + pkg_name + "/BUILD.bazel", + _PKG_BUILD_TMPL.format( + default_hub = render.str(default_hub), + extra_aliases = extra_aliases, + hubs = hubs, + pkg_hubs = pkg_hubs, + pkg_name = pkg_name, + ), + ) + +unified_hub_repo = repository_rule( + implementation = _unified_hub_repo_impl, + attrs = { + "default_hub": attr.string( + doc = "The PyPI hub to use when no other hub's conditions match.", + ), + "extra_aliases": attr.string_list_dict( + doc = "Dictionary mapping 'package:alias' to a list of hubs that support it.", + ), + "hubs": attr.string_list( + mandatory = True, + doc = "List of all concrete PyPI hub names.", + ), + "packages": attr.string_list_dict( + mandatory = True, + doc = "Dictionary mapping package names to a list of hubs that contain them.", + ), + }, + doc = "Private repository rule creating the automatic Unified PyPI Hub.", +) diff --git a/python/private/pypi/unified_hub_setup.bzl b/python/private/pypi/unified_hub_setup.bzl new file mode 100644 index 0000000000..b19410fd4b --- /dev/null +++ b/python/private/pypi/unified_hub_setup.bzl @@ -0,0 +1,110 @@ +"""Helper functions for setting up targets within the Unified PyPI Hub repository.""" + +load( + "@rules_python//python/private/pypi:labels.bzl", + "DATA_LABEL", + "DIST_INFO_LABEL", + "EXTRACTED_WHEEL_FILES", + "PY_LIBRARY_PUBLIC_LABEL", + "WHEEL_FILE_PUBLIC_LABEL", +) +load("@rules_python//python/private/pypi:missing_package.bzl", "missing_package_error") + +def define_venv_flag_config_settings(name, hubs): + """Defines the root config_settings for each PyPI spoke hub. + + Args: + name: unused macro name required by buildifier. + hubs: list of concrete hub names. + """ + for hub in hubs: + native.config_setting( + name = "_is_venv_" + hub, + flag_values = {"@rules_python//python/config_settings:venv": hub}, + ) + +_STANDARD_ALIASES = [ + PY_LIBRARY_PUBLIC_LABEL, + WHEEL_FILE_PUBLIC_LABEL, + DATA_LABEL, + DIST_INFO_LABEL, + EXTRACTED_WHEEL_FILES, +] + +def define_pypi_package_targets(name, pkg_hubs, extra_aliases, hubs, default_hub = None): + """Define the targets for a PyPI package in the unified PyPI hub. + + Args: + name: normalized PyPI package name, serving as the main target name. + pkg_hubs: list of hubs that contain this package. + extra_aliases: dict mapping extra alias names to lists of hubs that support them. + hubs: list of all concrete hub names. + default_hub: the hub to use by default. + """ + pkg_name = name + + # Main apparent package target delegates to :pkg + native.alias( + name = pkg_name, + actual = ":pkg", + ) + + all_aliases = _STANDARD_ALIASES + sorted(extra_aliases.keys()) + missing_errors = {} + + for alias_name in all_aliases: + select_map = {} + for hub in hubs: + is_supported = ( + (alias_name in _STANDARD_ALIASES and hub in pkg_hubs) or + (alias_name not in _STANDARD_ALIASES and hub in extra_aliases.get(alias_name, [])) + ) + + if is_supported: + select_map["//:_is_venv_" + hub] = "@{hub}//{pkg}:{alias}".format( + hub = hub, + pkg = pkg_name, + alias = alias_name, + ) + else: + err_target = "_missing_{alias}_in_{hub}".format(alias = alias_name, hub = hub) + if err_target not in missing_errors: + missing_errors[err_target] = { + "hub_name": hub, + "package_name": pkg_name if alias_name in _STANDARD_ALIASES else (pkg_name + ":" + alias_name), + } + select_map["//:_is_venv_" + hub] = ":{}".format(err_target) + + # //conditions:default fallback + default_supported = ( + default_hub and + ((alias_name in _STANDARD_ALIASES and default_hub in pkg_hubs) or + (alias_name not in _STANDARD_ALIASES and default_hub in extra_aliases.get(alias_name, []))) + ) + + if default_supported: + select_map["//conditions:default"] = "@{hub}//{pkg}:{alias}".format( + hub = default_hub, + pkg = pkg_name, + alias = alias_name, + ) + else: + err_target = "_missing_{alias}_in_default".format(alias = alias_name) + if err_target not in missing_errors: + missing_errors[err_target] = { + "hub_name": default_hub or "", + "package_name": pkg_name if alias_name in _STANDARD_ALIASES else (pkg_name + ":" + alias_name), + } + select_map["//conditions:default"] = ":{}".format(err_target) + + native.alias( + name = alias_name, + actual = select(select_map), + ) + + # Generate missing package error targets + for err_name, err_args in missing_errors.items(): + missing_package_error( + name = err_name, + **err_args + ) diff --git a/python/private/pypi/urllib.bzl b/python/private/pypi/urllib.bzl new file mode 100644 index 0000000000..ea4cd32cc9 --- /dev/null +++ b/python/private/pypi/urllib.bzl @@ -0,0 +1,82 @@ +"""Utilities for getting an absolute URL from index_url and the URL we find on PyPI index.""" + +def _get_root_directory(url): + scheme_end = url.find("://") + if scheme_end == -1: + fail("Invalid URL format: '{}'".format(url)) + + scheme = url[:scheme_end] + host_end = url.find("/", scheme_end + 3) + if host_end == -1: + host_end = len(url) + host = url[scheme_end + 3:host_end] + + return "{}://{}".format(scheme, host) + +def _is_downloadable(url): + """Checks if the URL would be accepted by the Bazel downloader. + + This is based on Bazel's HttpUtils::isUrlSupportedByDownloader + """ + return url.startswith("http://") or url.startswith("https://") or url.startswith("file://") + +def _absolute_url(index_url, candidate): + """Convert into an absolute URL. + + Args: + index_url: The index URL where the file has been found. + candidate: The candidate URL which may be not absolute. + + Returns: + An absolute URL + """ + if candidate == "": + return candidate + + if _is_downloadable(candidate): + return candidate + + if candidate.startswith("/"): + # absolute path + root_directory = _get_root_directory(index_url) + return "{}{}".format(root_directory, candidate) + + if candidate.startswith(".."): + # relative path with up references + candidate_parts = candidate.split("..") + last = candidate_parts[-1] + for _ in range(len(candidate_parts) - 1): + index_url, _, _ = index_url.rstrip("/").rpartition("/") + + return "{}/{}".format(index_url, last.strip("/")) + + # relative path without up-references + return "{}/{}".format(index_url.rstrip("/"), candidate) + +def _strip_empty_path_segments(url): + """Removes empty path segments from a URL. Does nothing for urls with no scheme. + + Public only for testing. + + Args: + url: The url to remove empty path segments from + + Returns: + The url with empty path segments removed and any trailing slash preserved. + If the url had no scheme it is returned unchanged. + """ + scheme, _, rest = url.partition("://") + if rest == "": + return url + stripped = "/".join([p for p in rest.split("/") if p]) + if url.endswith("/"): + return "{}://{}/".format(scheme, stripped) + else: + return "{}://{}".format(scheme, stripped) + +urllib = struct( + is_absolute = _is_downloadable, + # Ensure that we strip empty path segments when making an absolute URL + absolute_url = lambda index_url, candidate: _strip_empty_path_segments(_absolute_url(index_url, candidate)), + strip_empty_path_segments = _strip_empty_path_segments, +) diff --git a/python/private/pypi/venv_entry_point.bzl b/python/private/pypi/venv_entry_point.bzl new file mode 100644 index 0000000000..32cb6f55a5 --- /dev/null +++ b/python/private/pypi/venv_entry_point.bzl @@ -0,0 +1,50 @@ +"""Rule for generating venv entry point scripts.""" + +load("//python/private:attributes.bzl", "WINDOWS_CONSTRAINTS_ATTRS") +load("//python/private:common.bzl", "is_windows_platform") +load("//python/private:rule_builders.bzl", "ruleb") + +def _venv_entry_point_impl(ctx): + is_windows = is_windows_platform(ctx) + + out_name = ctx.label.name + python_exe = "" + if is_windows: + out_name += ".bat" + python_exe = "pythonw.exe" if ctx.attr.group == "gui_scripts" else "python.exe" + + out = ctx.actions.declare_file(out_name) + + ctx.actions.expand_template( + template = ctx.file._template, + output = out, + substitutions = { + "{ATTRIBUTE}": ctx.attr.attribute, + "{MODULE}": ctx.attr.module, + "{PYTHON_EXE}": python_exe, + }, + is_executable = True, + ) + + return [DefaultInfo( + files = depset([out]), + executable = out, + )] + +_builder = ruleb.Rule( + implementation = _venv_entry_point_impl, + executable = True, +) +_builder.attrs.update({ + "attribute": attr.string(mandatory = False, doc = "The attribute to call"), + "extras": attr.string(mandatory = False, doc = "The extras for the entry point"), + "group": attr.string(mandatory = False, doc = "The entry point group (e.g. console_scripts)"), + "module": attr.string(mandatory = True, doc = "The module to import"), + "_template": attr.label( + default = Label("//python/private/pypi:venv_entry_point_template"), + allow_single_file = True, + ), +}) +_builder.attrs.update(WINDOWS_CONSTRAINTS_ATTRS) + +venv_entry_point = _builder.build() diff --git a/python/private/pypi/venv_entry_point_template.bat b/python/private/pypi/venv_entry_point_template.bat new file mode 100644 index 0000000000..36a7dd3b41 --- /dev/null +++ b/python/private/pypi/venv_entry_point_template.bat @@ -0,0 +1,8 @@ +@setlocal enabledelayedexpansion & "%~dp0{PYTHON_EXE}" -x "%~f0" %* & exit /b !ERRORLEVEL! +# -*- coding: utf-8 -*- +import re +import sys +from {MODULE} import {ATTRIBUTE} +if __name__ == "__main__": + sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0]) + sys.exit({ATTRIBUTE}()) diff --git a/python/private/pypi/venv_entry_point_template.sh b/python/private/pypi/venv_entry_point_template.sh new file mode 100644 index 0000000000..d40c31adc4 --- /dev/null +++ b/python/private/pypi/venv_entry_point_template.sh @@ -0,0 +1,10 @@ +#!/bin/sh +'''exec' "$(dirname "$0")/python3" "$0" "$@" +' ''' +# -*- coding: utf-8 -*- +import re +import sys +from {MODULE} import {ATTRIBUTE} +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit({ATTRIBUTE}()) diff --git a/python/private/pypi/venv_rewrite_shebang.bzl b/python/private/pypi/venv_rewrite_shebang.bzl new file mode 100644 index 0000000000..c653211850 --- /dev/null +++ b/python/private/pypi/venv_rewrite_shebang.bzl @@ -0,0 +1,82 @@ +"""Rule for rewriting portable shebangs.""" + +load("//python/private:attributes.bzl", "WINDOWS_CONSTRAINTS_ATTRS") +load("//python/private:common.bzl", "is_windows_platform", "runfiles_root_path") +load("//python/private:py_info.bzl", "PyInfoBuilder", "VenvSymlinkEntry", "VenvSymlinkKind") +load("//python/private:rule_builders.bzl", "ruleb") + +def _venv_rewrite_shebang_impl(ctx): + is_windows = is_windows_platform(ctx) + + out_name = ctx.label.name + if is_windows: + out_name += ".bat" + + out_file = ctx.actions.declare_file(out_name) + in_file = ctx.file.src + + action_args = ctx.actions.args() + rewriter_file = ctx.files._venv_shebang_rewriter[0] + inputs = depset([in_file, rewriter_file]) + + if rewriter_file.path.endswith(".ps1"): + action_exe = "powershell.exe" + action_args.add_all([ + "-ExecutionPolicy", + "Bypass", + "-NoProfile", + "-File", + rewriter_file, + ]) + else: + action_exe = ctx.attr._venv_shebang_rewriter[DefaultInfo].files_to_run + + action_args.add(in_file) + action_args.add(out_file) + action_args.add("windows" if is_windows else "unix") + + ctx.actions.run( + inputs = inputs, + outputs = [out_file], + executable = action_exe, + arguments = [action_args], + mnemonic = "PyVenvRewriteBin", + progress_message = "Rewriting venv bin script %{input}", + toolchain = None, + ) + + symlink = VenvSymlinkEntry( + kind = VenvSymlinkKind.BIN, + link_to_path = runfiles_root_path(ctx, out_file.short_path), + link_to_file = out_file, + venv_path = out_name, + package = ctx.attr.package, + version = ctx.attr.version, + files = depset([out_file]), + ) + builder = PyInfoBuilder.new() + builder.venv_symlinks.add([symlink]) + py_info = builder.build() + + return [ + DefaultInfo(files = depset([out_file]), executable = out_file), + py_info, + ] + +_builder = ruleb.Rule( + implementation = _venv_rewrite_shebang_impl, + executable = True, +) +_builder.attrs.update({ + "package": attr.string(), + "src": attr.label(mandatory = True, allow_single_file = True), + "version": attr.string(), + "_venv_shebang_rewriter": attr.label( + default = "//python/private/pypi:venv_shebang_rewriter", + allow_files = True, + cfg = "exec", + ), +}) +_builder.attrs.update(WINDOWS_CONSTRAINTS_ATTRS) + +venv_rewrite_shebang = _builder.build() diff --git a/python/private/pypi/venv_shebang_rewriter.ps1 b/python/private/pypi/venv_shebang_rewriter.ps1 new file mode 100644 index 0000000000..fb6077b407 --- /dev/null +++ b/python/private/pypi/venv_shebang_rewriter.ps1 @@ -0,0 +1,44 @@ +[CmdletBinding()] +param( + [Parameter(Position=0, Mandatory=$true)] + [string]$InFile, + + [Parameter(Position=1, Mandatory=$true)] + [string]$OutFile, + + [Parameter(Position=2, Mandatory=$true)] + [string]$TargetOs +) + +$ErrorActionPreference = "Stop" + +$firstLine = Get-Content -Path $InFile -TotalCount 1 -ErrorAction SilentlyContinue +$content = Get-Content -Path $InFile | Select-Object -Skip 1 + +$Utf8NoBom = New-Object System.Text.UTF8Encoding $False + +if ($TargetOs -eq "windows") { + if ($firstLine -match "^#!pythonw") { + $pythonExe = "pythonw.exe" + } else { + $pythonExe = "python.exe" + } + # A Batch-Python polyglot. Batch executes the first line and exits, + # while Python (via -x) ignores the first line and executes the rest. + $wrapper = "@setlocal enabledelayedexpansion & `"%~dp0$pythonExe`" -x `"%~f0`" %* & exit /b !ERRORLEVEL!" + [System.IO.File]::WriteAllText($OutFile, $wrapper + "`r`n", $Utf8NoBom) +} else { + # A Shell-Python polyglot. The shell executes the triple-quoted 'exec' + # command, re-running the script with python3 from the scripts directory. + # Python ignores the triple-quoted string and continues. + $wrapper = @" +#!/bin/sh +'''exec' "`$(dirname "`$0")/python3" "`$0" "`$@" +' ''' +"@ + [System.IO.File]::WriteAllText($OutFile, $wrapper + "`n", $Utf8NoBom) +} + +if ($null -ne $content) { + [System.IO.File]::AppendAllLines($OutFile, [string[]]$content, $Utf8NoBom) +} diff --git a/python/private/pypi/venv_shebang_rewriter.sh b/python/private/pypi/venv_shebang_rewriter.sh new file mode 100755 index 0000000000..d4391d3352 --- /dev/null +++ b/python/private/pypi/venv_shebang_rewriter.sh @@ -0,0 +1,27 @@ +#!/bin/sh +set -eu + +IN="$1" +OUT="$2" +TARGET_OS="$3" + +FIRST_LINE=$(head -n 1 "$IN") + +if [ "$TARGET_OS" = "windows" ]; then + case "$FIRST_LINE" in + "#!pythonw"*) PYTHON_EXE="pythonw.exe" ;; + *) PYTHON_EXE="python.exe" ;; + esac + # A Batch-Python polyglot. Batch executes the first line and exits, + # while Python (via -x) ignores the first line and executes the rest. + printf "@setlocal enabledelayedexpansion & \"%%~dp0$PYTHON_EXE\" -x \"%%~f0\" %%* & exit /b !ERRORLEVEL!\r\n" > "$OUT" +else + printf "#!/bin/sh\n" > "$OUT" + # A Shell-Python polyglot. The shell executes the triple-quoted 'exec' + # command, re-running the script with python3 from the scripts directory. + # Python ignores the triple-quoted string and continues. + printf "'''exec' \"\$(dirname \"\$0\")/python3\" \"\$0\" \"\$@\"\n' '''\n" >> "$OUT" +fi + +tail -n +2 "$IN" >> "$OUT" +chmod +x "$OUT" diff --git a/python/private/pypi/version_from_filename.bzl b/python/private/pypi/version_from_filename.bzl new file mode 100644 index 0000000000..d0b6e3105d --- /dev/null +++ b/python/private/pypi/version_from_filename.bzl @@ -0,0 +1,42 @@ +"""Parse the version of the thing just from the filename. This is useful for selecting files based on the requested version.""" + +_SDIST_EXTS = [ + ".tar", # handles any compression + ".zip", +] + +def version_from_filename(filename, _fail = None): + """Parse the version of the filename. + + Args: + filename: {type}`str` the filename. + _fail: The fail function. + + Returns: + A string version or None if we could not parse the version. + """ + # See https://packaging.python.org/en/latest/specifications/binary-distribution-format/#binary-distribution-format + + if filename.endswith(".whl"): + # The format is {name}-{version}-{whl_specifiers}.whl + _, _, version = filename.partition("-") + version, _, _ = version.partition("-") + return version + + # NOTE @aignas 2025-03-29: most of the files are wheels, so this is not the common path + + # {name}-{version}.{ext} + head = "" + for ext in _SDIST_EXTS: + head, _, _ = filename.rpartition(ext) # build or name + if head: + break + + if not head: + if _fail: + _fail("Unsupported sdist extension: {filename}".format(filename = filename)) + return None + + # Based on PEP440 the version number cannot include dashes + _, _, version = head.rpartition("-") + return version diff --git a/python/private/pypi/whl_config_repo.bzl b/python/private/pypi/whl_config_repo.bzl new file mode 100644 index 0000000000..b7cea5a8b7 --- /dev/null +++ b/python/private/pypi/whl_config_repo.bzl @@ -0,0 +1,46 @@ +"""whl_config_library implementation for WORKSPACE setups.""" + +load("//python/private:text_util.bzl", "render") +load(":generate_group_library_build_bazel.bzl", "generate_group_library_build_bazel") + +def _whl_config_repo_impl(rctx): + build_file_contents = generate_group_library_build_bazel( + repo_prefix = rctx.attr.repo_prefix, + groups = rctx.attr.groups, + ) + rctx.file("_groups/BUILD.bazel", build_file_contents) + rctx.file("BUILD.bazel", "") + rctx.template( + "config.bzl", + rctx.attr._config_template, + substitutions = { + "%%PACKAGES%%": render.dict(rctx.attr.whl_map or {}, value_repr = lambda x: "None"), + }, + ) + +whl_config_repo = repository_rule( + attrs = { + "groups": attr.string_list_dict( + doc = "A mapping of group names to requirements within that group.", + ), + "repo_prefix": attr.string( + doc = "Prefix used for the whl_library created components of each group", + ), + "whl_map": attr.string_dict( + doc = """\ +The wheel map where values are json.encoded strings of the whl_map constructed +in the pip.parse tag class. +""", + ), + "_config_template": attr.label( + default = ":config.bzl.tmpl", + ), + }, + doc = """ +Create a package containing only wrapper py_library and whl_library rules for implementing dependency groups. +This is an implementation detail of dependency groups and should not be used alone. + +PRIVATE USE ONLY, only used in WORKSPACE. + """, + implementation = _whl_config_repo_impl, +) diff --git a/python/private/pypi/whl_extract.bzl b/python/private/pypi/whl_extract.bzl new file mode 100644 index 0000000000..0d61b9a07b --- /dev/null +++ b/python/private/pypi/whl_extract.bzl @@ -0,0 +1,108 @@ +"""A simple whl extractor.""" + +load("@rules_python_internal//:rules_python_config.bzl", rp_config = "config") +load("//python/private:repo_utils.bzl", "repo_utils") +load(":whl_metadata.bzl", "find_whl_metadata") + +def whl_extract(rctx, *, whl_path, logger): + """Extract whls in Starlark. + + Args: + rctx: the repository ctx. + whl_path: the whl path to extract. + logger: The logger to use + """ + install_dir_path = rctx.path("site-packages") + repo_utils.extract( + rctx, + archive = whl_path, + output = install_dir_path, + supports_whl_extraction = rp_config.supports_whl_extraction, + extract_needs_chmod = rp_config.extract_needs_chmod, + ) + + metadata_file = find_whl_metadata( + install_dir = install_dir_path, + logger = logger, + ) + + # Get the .dist_info dir name + dist_info_dir = metadata_file.dirname + rctx.file( + dist_info_dir.get_child("INSTALLER"), + "https://github.com/bazel-contrib/rules_python#pipstar", + ) + + # Get the .dist_info dir name + data_dir = dist_info_dir.dirname.get_child(dist_info_dir.basename[:-len(".dist-info")] + ".data") + if data_dir.exists: + for prefix, dest_prefix in { + # https://docs.python.org/3/library/sysconfig.html#posix-prefix + # We are taking this from the legacy whl installer config + "data": "data", + "headers": "include", + # In theory there may be directory collisions here, so it would be best to + # merge the paths here. We are doing for quite a few levels deep. What is + # more, this code has to be reasonably efficient because some packages like + # to not put everything to the top level, but to indicate explicitly if + # something is in `platlib` or `purelib` (e.g. libclang wheel). + "platlib": "site-packages", + "purelib": "site-packages", + "scripts": "bin", + }.items(): + src = data_dir.get_child(prefix) + if not src.exists: + # The prefix does not exist in the wheel, we can continue + continue + + dest_dir = rctx.path(dest_prefix) + repo_utils.mkdir(rctx, dest_dir) + for (src, dest) in merge_trees(src, dest_dir): + logger.debug(lambda: "Renaming: {} -> {}".format(src, dest)) + repo_utils.rename(rctx, src, dest) + + # Ensure that there is no data dir left + rctx.delete(data_dir) + +def merge_trees(src, dest): + """Merge src into the destination path. + + This will attempt to merge-move src files to the destination directory if there are + existing files. Fails at directory depth is 10000 or if there are collisions. + + Args: + src: {type}`path` a src path to rename. + dest: {type}`path` a dest path to rename to. + + Returns: + A list of tuples for src and destination paths. + """ + ret = [] + remaining = [(src, dest)] + collisions = [] + for _ in range(10000): + if collisions or not remaining: + break + + tmp = [] + for (s, d) in remaining: + if not d.exists: + ret.append((s, d)) + continue + + if not s.is_dir or not d.is_dir: + collisions.append(s) + continue + + for file_or_dir in s.readdir(): + tmp.append((file_or_dir, d.get_child(file_or_dir.basename))) + + remaining = tmp + + if remaining: + fail("Exceeded maximum directory depth of 10000 during tree merge.") + + if collisions: + fail("Detected collisions between {} and {}: {}".format(src, dest, collisions)) + + return ret diff --git a/python/private/pypi/whl_installer/BUILD.bazel b/python/private/pypi/whl_installer/BUILD.bazel index 5fb617004d..1912b34af8 100644 --- a/python/private/pypi/whl_installer/BUILD.bazel +++ b/python/private/pypi/whl_installer/BUILD.bazel @@ -5,9 +5,6 @@ py_library( name = "lib", srcs = [ "arguments.py", - "namespace_pkgs.py", - "platform.py", - "wheel.py", "wheel_installer.py", ], visibility = [ diff --git a/python/private/pypi/whl_installer/arguments.py b/python/private/pypi/whl_installer/arguments.py index 57dae45ae9..8471c94ffe 100644 --- a/python/private/pypi/whl_installer/arguments.py +++ b/python/private/pypi/whl_installer/arguments.py @@ -14,11 +14,8 @@ import argparse import json -import pathlib from typing import Any, Dict, Set -from python.private.pypi.whl_installer.platform import Platform - def parser(**kwargs: Any) -> argparse.ArgumentParser: """Create a parser for the wheel_installer tool.""" @@ -41,17 +38,6 @@ def parser(**kwargs: Any) -> argparse.ArgumentParser: action="store", help="Extra arguments to pass down to pip.", ) - parser.add_argument( - "--platform", - action="extend", - type=Platform.from_string, - help="Platforms to target dependencies. Can be used multiple times.", - ) - parser.add_argument( - "--enable-pipstar", - action="store_true", - help="Disable certain code paths if we expect to process the whl in Starlark.", - ) parser.add_argument( "--pip_data_exclude", action="store", @@ -68,11 +54,6 @@ def parser(**kwargs: Any) -> argparse.ArgumentParser: help="Use 'pip download' instead of 'pip wheel'. Disables building wheels from source, but allows use of " "--platform, --python-version, --implementation, and --abi in --extra_pip_args.", ) - parser.add_argument( - "--whl-file", - type=pathlib.Path, - help="Extract a whl file to be used within Bazel.", - ) return parser diff --git a/python/private/pypi/whl_installer/namespace_pkgs.py b/python/private/pypi/whl_installer/namespace_pkgs.py deleted file mode 100644 index b415844ace..0000000000 --- a/python/private/pypi/whl_installer/namespace_pkgs.py +++ /dev/null @@ -1,121 +0,0 @@ -# Copyright 2023 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. - -"""Utility functions to discover python package types""" -import os -import textwrap -from pathlib import Path # supported in >= 3.4 -from typing import List, Optional, Set - - -def implicit_namespace_packages( - directory: str, ignored_dirnames: Optional[List[str]] = None -) -> Set[Path]: - """Discovers namespace packages implemented using the 'native namespace packages' method. - - AKA 'implicit namespace packages', which has been supported since Python 3.3. - See: https://packaging.python.org/guides/packaging-namespace-packages/#native-namespace-packages - - Args: - directory: The root directory to recursively find packages in. - ignored_dirnames: A list of directories to exclude from the search - - Returns: - The set of directories found under root to be packages using the native namespace method. - """ - namespace_pkg_dirs: Set[Path] = set() - standard_pkg_dirs: Set[Path] = set() - directory_path = Path(directory) - ignored_dirname_paths: List[Path] = [Path(p) for p in ignored_dirnames or ()] - # Traverse bottom-up because a directory can be a namespace pkg because its child contains module files. - for dirpath, dirnames, filenames in map( - lambda t: (Path(t[0]), *t[1:]), os.walk(directory_path, topdown=False) - ): - if "__init__.py" in filenames: - standard_pkg_dirs.add(dirpath) - continue - elif ignored_dirname_paths: - is_ignored_dir = dirpath in ignored_dirname_paths - child_of_ignored_dir = any( - d in dirpath.parents for d in ignored_dirname_paths - ) - if is_ignored_dir or child_of_ignored_dir: - continue - - dir_includes_py_modules = _includes_python_modules(filenames) - parent_of_namespace_pkg = any( - Path(dirpath, d) in namespace_pkg_dirs for d in dirnames - ) - parent_of_standard_pkg = any( - Path(dirpath, d) in standard_pkg_dirs for d in dirnames - ) - parent_of_pkg = parent_of_namespace_pkg or parent_of_standard_pkg - if ( - (dir_includes_py_modules or parent_of_pkg) - and - # The root of the directory should never be an implicit namespace - dirpath != directory_path - ): - namespace_pkg_dirs.add(dirpath) - return namespace_pkg_dirs - - -def add_pkgutil_style_namespace_pkg_init(dir_path: Path) -> None: - """Adds 'pkgutil-style namespace packages' init file to the given directory - - See: https://packaging.python.org/guides/packaging-namespace-packages/#pkgutil-style-namespace-packages - - Args: - dir_path: The directory to create an __init__.py for. - - Raises: - ValueError: If the directory already contains an __init__.py file - """ - ns_pkg_init_filepath = os.path.join(dir_path, "__init__.py") - - if os.path.isfile(ns_pkg_init_filepath): - raise ValueError("%s already contains an __init__.py file." % dir_path) - - with open(ns_pkg_init_filepath, "w") as ns_pkg_init_f: - # See https://packaging.python.org/guides/packaging-namespace-packages/#pkgutil-style-namespace-packages - ns_pkg_init_f.write( - textwrap.dedent( - """\ - # __path__ manipulation added by bazel-contrib/rules_python to support namespace pkgs. - __path__ = __import__('pkgutil').extend_path(__path__, __name__) - """ - ) - ) - - -def _includes_python_modules(files: List[str]) -> bool: - """ - In order to only transform directories that Python actually considers namespace pkgs - we need to detect if a directory includes Python modules. - - Which files are loadable as modules is extension based, and the particular set of extensions - varies by platform. - - See: - 1. https://github.com/python/cpython/blob/7d9d25dbedfffce61fc76bc7ccbfa9ae901bf56f/Lib/importlib/machinery.py#L19 - 2. PEP 420 -- Implicit Namespace Packages, Specification - https://www.python.org/dev/peps/pep-0420/#specification - 3. dynload_shlib.c and dynload_win.c in python/cpython. - """ - module_suffixes = { - ".py", # Source modules - ".pyc", # Compiled bytecode modules - ".so", # Unix extension modules - ".pyd", # https://docs.python.org/3/faq/windows.html#is-a-pyd-file-the-same-as-a-dll - } - return any(Path(f).suffix in module_suffixes for f in files) diff --git a/python/private/pypi/whl_installer/platform.py b/python/private/pypi/whl_installer/platform.py deleted file mode 100644 index ff267fe4aa..0000000000 --- a/python/private/pypi/whl_installer/platform.py +++ /dev/null @@ -1,300 +0,0 @@ -# Copyright 2024 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. - -"""Utility class to inspect an extracted wheel directory""" - -import platform -import sys -from dataclasses import dataclass -from enum import Enum -from typing import Any, Dict, Iterator, List, Optional, Tuple, Union - - -class OS(Enum): - linux = 1 - osx = 2 - windows = 3 - darwin = osx - win32 = windows - - @classmethod - def interpreter(cls) -> "OS": - "Return the interpreter operating system." - return cls[sys.platform.lower()] - - def __str__(self) -> str: - return self.name.lower() - - -class Arch(Enum): - x86_64 = 1 - x86_32 = 2 - aarch64 = 3 - ppc = 4 - ppc64le = 5 - s390x = 6 - arm = 7 - amd64 = x86_64 - arm64 = aarch64 - i386 = x86_32 - i686 = x86_32 - x86 = x86_32 - - @classmethod - def interpreter(cls) -> "Arch": - "Return the currently running interpreter architecture." - # FIXME @aignas 2023-12-13: Hermetic toolchain on Windows 3.11.6 - # is returning an empty string here, so lets default to x86_64 - return cls[platform.machine().lower() or "x86_64"] - - def __str__(self) -> str: - return self.name.lower() - - -def _as_int(value: Optional[Union[OS, Arch]]) -> int: - """Convert one of the enums above to an int for easier sorting algorithms. - - Args: - value: The value of an enum or None. - - Returns: - -1 if we get None, otherwise, the numeric value of the given enum. - """ - if value is None: - return -1 - - return int(value.value) - - -def host_interpreter_version() -> Tuple[int, int]: - return (sys.version_info.minor, sys.version_info.micro) - - -@dataclass(frozen=True) -class Platform: - os: Optional[OS] = None - arch: Optional[Arch] = None - minor_version: Optional[int] = None - micro_version: Optional[int] = None - - @classmethod - def all( - cls, - want_os: Optional[OS] = None, - minor_version: Optional[int] = None, - micro_version: Optional[int] = None, - ) -> List["Platform"]: - return sorted( - [ - cls( - os=os, - arch=arch, - minor_version=minor_version, - micro_version=micro_version, - ) - for os in OS - for arch in Arch - if not want_os or want_os == os - ] - ) - - @classmethod - def host(cls) -> List["Platform"]: - """Use the Python interpreter to detect the platform. - - We extract `os` from sys.platform and `arch` from platform.machine - - Returns: - A list of parsed values which makes the signature the same as - `Platform.all` and `Platform.from_string`. - """ - minor, micro = host_interpreter_version() - return [ - Platform( - os=OS.interpreter(), - arch=Arch.interpreter(), - minor_version=minor, - micro_version=micro, - ) - ] - - def __lt__(self, other: Any) -> bool: - """Add a comparison method, so that `sorted` returns the most specialized platforms first.""" - if not isinstance(other, Platform) or other is None: - raise ValueError(f"cannot compare {other} with Platform") - - self_arch, self_os = _as_int(self.arch), _as_int(self.os) - other_arch, other_os = _as_int(other.arch), _as_int(other.os) - - if self_os == other_os: - return self_arch < other_arch - else: - return self_os < other_os - - def __str__(self) -> str: - if self.minor_version is None: - return f"{self.os}_{self.arch}" - - minor_version = self.minor_version - micro_version = self.micro_version - - if micro_version is None: - return f"cp3{minor_version}_{self.os}_{self.arch}" - else: - return f"cp3{minor_version}.{micro_version}_{self.os}_{self.arch}" - - @classmethod - def from_string(cls, platform: Union[str, List[str]]) -> List["Platform"]: - """Parse a string and return a list of platforms""" - platform = [platform] if isinstance(platform, str) else list(platform) - ret = set() - for p in platform: - if p == "host": - ret.update(cls.host()) - continue - - abi, _, tail = p.partition("_") - if not abi.startswith("cp"): - # The first item is not an abi - tail = p - abi = "" - os, _, arch = tail.partition("_") - arch = arch or "*" - - if abi: - tail = abi[len("cp3") :] - minor_version, _, micro_version = tail.partition(".") - minor_version = int(minor_version) - if micro_version == "": - micro_version = None - else: - micro_version = int(micro_version) - else: - minor_version = None - micro_version = None - - if arch != "*": - ret.add( - cls( - os=OS[os] if os != "*" else None, - arch=Arch[arch], - minor_version=minor_version, - micro_version=micro_version, - ) - ) - - else: - ret.update( - cls.all( - want_os=OS[os] if os != "*" else None, - minor_version=minor_version, - micro_version=micro_version, - ) - ) - - return sorted(ret) - - # NOTE @aignas 2023-12-05: below is the minimum number of accessors that are defined in - # https://peps.python.org/pep-0496/ to make rules_python generate dependencies. - # - # WARNING: It may not work in cases where the python implementation is different between - # different platforms. - - # derived from OS - @property - def os_name(self) -> str: - if self.os == OS.linux or self.os == OS.osx: - return "posix" - elif self.os == OS.windows: - return "nt" - else: - return "" - - @property - def sys_platform(self) -> str: - if self.os == OS.linux: - return "linux" - elif self.os == OS.osx: - return "darwin" - elif self.os == OS.windows: - return "win32" - else: - return "" - - @property - def platform_system(self) -> str: - if self.os == OS.linux: - return "Linux" - elif self.os == OS.osx: - return "Darwin" - elif self.os == OS.windows: - return "Windows" - else: - return "" - - # derived from OS and Arch - @property - def platform_machine(self) -> str: - """Guess the target 'platform_machine' marker. - - NOTE @aignas 2023-12-05: this may not work on really new systems, like - Windows if they define the platform markers in a different way. - """ - if self.arch == Arch.x86_64: - return "x86_64" - elif self.arch == Arch.x86_32 and self.os != OS.osx: - return "i386" - elif self.arch == Arch.x86_32: - return "" - elif self.arch == Arch.aarch64 and self.os == OS.linux: - return "aarch64" - elif self.arch == Arch.aarch64: - # Assuming that OSX and Windows use this one since the precedent is set here: - # https://github.com/cgohlke/win_arm64-wheels - return "arm64" - elif self.os != OS.linux: - return "" - elif self.arch == Arch.ppc: - return "ppc" - elif self.arch == Arch.ppc64le: - return "ppc64le" - elif self.arch == Arch.s390x: - return "s390x" - else: - return "" - - def env_markers(self, extra: str) -> Dict[str, str]: - # If it is None, use the host version - if self.minor_version is None: - minor, micro = host_interpreter_version() - else: - minor, micro = self.minor_version, self.micro_version - - micro = micro or 0 - - return { - "extra": extra, - "os_name": self.os_name, - "sys_platform": self.sys_platform, - "platform_machine": self.platform_machine, - "platform_system": self.platform_system, - "platform_release": "", # unset - "platform_version": "", # unset - "python_version": f"3.{minor}", - "implementation_version": f"3.{minor}.{micro}", - "python_full_version": f"3.{minor}.{micro}", - # we assume that the following are the same as the interpreter used to setup the deps: - # "implementation_name": "cpython" - # "platform_python_implementation: "CPython", - } diff --git a/python/private/pypi/whl_installer/wheel.py b/python/private/pypi/whl_installer/wheel.py deleted file mode 100644 index 25003e6280..0000000000 --- a/python/private/pypi/whl_installer/wheel.py +++ /dev/null @@ -1,332 +0,0 @@ -# Copyright 2023 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. - -"""Utility class to inspect an extracted wheel directory""" - -import email -import re -from collections import defaultdict -from dataclasses import dataclass -from pathlib import Path -from typing import Dict, List, Optional, Set, Tuple - -import installer -from packaging.requirements import Requirement -from pip._vendor.packaging.utils import canonicalize_name - -from python.private.pypi.whl_installer.platform import ( - Platform, - host_interpreter_version, -) - - -@dataclass(frozen=True) -class FrozenDeps: - deps: List[str] - deps_select: Dict[str, List[str]] - - -class Deps: - """Deps is a dependency builder that has a build() method to return FrozenDeps.""" - - def __init__( - self, - name: str, - requires_dist: List[str], - *, - extras: Optional[Set[str]] = None, - platforms: Optional[Set[Platform]] = None, - ): - """Create a new instance and parse the requires_dist - - Args: - name (str): The name of the whl distribution - requires_dist (list[Str]): The Requires-Dist from the METADATA of the whl - distribution. - extras (set[str], optional): The list of requested extras, defaults to None. - platforms (set[Platform], optional): The list of target platforms, defaults to - None. If the list of platforms has multiple `minor_version` values, it - will change the code to generate the select statements using - `@rules_python//python/config_settings:is_python_3.y` conditions. - """ - self.name: str = Deps._normalize(name) - self._platforms: Set[Platform] = platforms or set() - self._target_versions = { - (p.minor_version, p.micro_version) for p in platforms or {} - } - if platforms and len(self._target_versions) > 1: - # TODO @aignas 2024-06-23: enable this to be set via a CLI arg - # for being more explicit. - self._default_minor_version, _ = host_interpreter_version() - else: - self._default_minor_version = None - - if None in self._target_versions and len(self._target_versions) > 2: - raise ValueError( - f"all python versions need to be specified explicitly, got: {platforms}" - ) - - # Sort so that the dictionary order in the FrozenDeps is deterministic - # without the final sort because Python retains insertion order. That way - # the sorting by platform is limited within the Platform class itself and - # the unit-tests for the Deps can be simpler. - reqs = sorted( - (Requirement(wheel_req) for wheel_req in requires_dist), - key=lambda x: f"{x.name}:{sorted(x.extras)}", - ) - - want_extras = self._resolve_extras(reqs, extras) - - # Then add all of the requirements in order - self._deps: Set[str] = set() - self._select: Dict[Platform, Set[str]] = defaultdict(set) - - reqs_by_name = {} - for req in reqs: - reqs_by_name.setdefault(req.name, []).append(req) - - for req_name, reqs in reqs_by_name.items(): - self._add_req(req_name, reqs, want_extras) - - def _add(self, dep: str, platform: Optional[Platform]): - dep = Deps._normalize(dep) - - # Self-edges are processed in _resolve_extras - if dep == self.name: - return - - if not platform: - self._deps.add(dep) - - # If the dep is in the platform-specific list, remove it from the select. - pop_keys = [] - for p, deps in self._select.items(): - if dep not in deps: - continue - - deps.remove(dep) - if not deps: - pop_keys.append(p) - - for p in pop_keys: - self._select.pop(p) - return - - if dep in self._deps: - # If the dep is already in the main dependency list, no need to add it in the - # platform-specific dependency list. - return - - # Add the platform-specific dep - self._select[platform].add(dep) - - @staticmethod - def _normalize(name: str) -> str: - return re.sub(r"[-_.]+", "_", name).lower() - - def _resolve_extras( - self, reqs: List[Requirement], want_extras: Optional[Set[str]] - ) -> Set[str]: - """Resolve extras which are due to depending on self[some_other_extra]. - - Some packages may have cyclic dependencies resulting from extras being used, one example is - `etils`, where we have one set of extras as aliases for other extras - and we have an extra called 'all' that includes all other extras. - - Example: github.com/google/etils/blob/a0b71032095db14acf6b33516bca6d885fe09e35/pyproject.toml#L32. - - When the `requirements.txt` is generated by `pip-tools`, then it is likely that - this step is not needed, but for other `requirements.txt` files this may be useful. - - NOTE @aignas 2023-12-08: the extra resolution is not platform dependent, - but in order for it to become platform dependent we would have to have - separate targets for each extra in extras. - """ - - # Resolve any extra extras due to self-edges, empty string means no - # extras The empty string in the set is just a way to make the handling - # of no extras and a single extra easier and having a set of {"", "foo"} - # is equivalent to having {"foo"}. - extras: Set[str] = want_extras or {""} - - self_reqs = [] - for req in reqs: - if Deps._normalize(req.name) != self.name: - continue - - if req.marker is None: - # I am pretty sure we cannot reach this code as it does not - # make sense to specify packages in this way, but since it is - # easy to handle, lets do it. - # - # TODO @aignas 2023-12-08: add a test - extras = extras | req.extras - else: - # process these in a separate loop - self_reqs.append(req) - - # A double loop is not strictly optimal, but always correct without recursion - for req in self_reqs: - if any(req.marker.evaluate({"extra": extra}) for extra in extras): - extras = extras | req.extras - else: - continue - - # Iterate through all packages to ensure that we include all of the extras from previously - # visited packages. - for req_ in self_reqs: - if any(req_.marker.evaluate({"extra": extra}) for extra in extras): - extras = extras | req_.extras - - return extras - - def _add_req(self, req_name, reqs: List[Requirement], extras: Set[str]) -> None: - platforms_to_add = set() - for req in reqs: - if req.marker is None: - self._add(req.name, None) - return - - if not self._platforms: - if any(req.marker.evaluate({"extra": extra}) for extra in extras): - self._add(req.name, None) - return - - for plat in self._platforms: - if plat in platforms_to_add: - # marker evaluation is more expensive than this check - continue - - added = False - for extra in extras: - if added: - break - - if req.marker.evaluate(plat.env_markers(extra)): - platforms_to_add.add(plat) - added = True - break - - if not self._platforms: - return - - if len(platforms_to_add) == len(self._platforms): - # the dep is in all target platforms, let's just add it to the regular - # list - self._add(req_name, None) - return - - for plat in platforms_to_add: - if self._default_minor_version is not None: - self._add(req_name, plat) - - if ( - self._default_minor_version is None - or plat.minor_version == self._default_minor_version - ): - self._add(req_name, Platform(os=plat.os, arch=plat.arch)) - - def build(self) -> FrozenDeps: - return FrozenDeps( - deps=sorted(self._deps), - deps_select={str(p): sorted(deps) for p, deps in self._select.items()}, - ) - - -class Wheel: - """Representation of the compressed .whl file""" - - def __init__(self, path: Path): - self._path = path - - @property - def path(self) -> Path: - return self._path - - @property - def name(self) -> str: - # TODO Also available as installer.sources.WheelSource.distribution - name = str(self.metadata["Name"]) - return canonicalize_name(name) - - @property - def metadata(self) -> email.message.Message: - with installer.sources.WheelFile.open(self.path) as wheel_source: - metadata_contents = wheel_source.read_dist_info("METADATA") - metadata = installer.utils.parse_metadata_file(metadata_contents) - return metadata - - @property - def version(self) -> str: - # TODO Also available as installer.sources.WheelSource.version - return str(self.metadata["Version"]) - - def entry_points(self) -> Dict[str, Tuple[str, str]]: - """Returns the entrypoints defined in the current wheel - - See https://packaging.python.org/specifications/entry-points/ for more info - - Returns: - Dict[str, Tuple[str, str]]: A mapping of the entry point's name to it's module and attribute - """ - with installer.sources.WheelFile.open(self.path) as wheel_source: - if "entry_points.txt" not in wheel_source.dist_info_filenames: - return dict() - - entry_points_mapping = dict() - entry_points_contents = wheel_source.read_dist_info("entry_points.txt") - entry_points = installer.utils.parse_entrypoints(entry_points_contents) - for script, module, attribute, script_section in entry_points: - if script_section == "console": - entry_points_mapping[script] = (module, attribute) - - return entry_points_mapping - - def dependencies( - self, - extras_requested: Set[str] = None, - platforms: Optional[Set[Platform]] = None, - ) -> FrozenDeps: - return Deps( - self.name, - extras=extras_requested, - platforms=platforms, - requires_dist=self.metadata.get_all("Requires-Dist", []), - ).build() - - def unzip(self, directory: str) -> None: - installation_schemes = { - "purelib": "/site-packages", - "platlib": "/site-packages", - "headers": "/include", - "scripts": "/bin", - "data": "/data", - } - destination = installer.destinations.SchemeDictionaryDestination( - installation_schemes, - # TODO Should entry_point scripts also be handled by installer rather than custom code? - interpreter="/dev/null", - script_kind="posix", - destdir=directory, - bytecode_optimization_levels=[], - ) - - with installer.sources.WheelFile.open(self.path) as wheel_source: - installer.install( - source=wheel_source, - destination=destination, - additional_metadata={ - "INSTALLER": b"https://github.com/bazel-contrib/rules_python", - }, - ) diff --git a/python/private/pypi/whl_installer/wheel_installer.py b/python/private/pypi/whl_installer/wheel_installer.py index a6a9dd0429..9aa3a4a375 100644 --- a/python/private/pypi/whl_installer/wheel_installer.py +++ b/python/private/pypi/whl_installer/wheel_installer.py @@ -18,16 +18,12 @@ import glob import json import os -import re import subprocess import sys from pathlib import Path from tempfile import NamedTemporaryFile -from typing import Dict, List, Optional, Set, Tuple -from pip._vendor.packaging.utils import canonicalize_name - -from python.private.pypi.whl_installer import arguments, wheel +from python.private.pypi.whl_installer import arguments def _configure_reproducible_wheels() -> None: @@ -55,74 +51,6 @@ def _configure_reproducible_wheels() -> None: os.environ["PYTHONHASHSEED"] = "0" -def _parse_requirement_for_extra( - requirement: str, -) -> Tuple[Optional[str], Optional[Set[str]]]: - """Given a requirement string, returns the requirement name and set of extras, if extras specified. - Else, returns (None, None) - """ - - # https://www.python.org/dev/peps/pep-0508/#grammar - extras_pattern = re.compile( - r"^\s*([0-9A-Za-z][0-9A-Za-z_.\-]*)\s*\[\s*([0-9A-Za-z][0-9A-Za-z_.\-]*(?:\s*,\s*[0-9A-Za-z][0-9A-Za-z_.\-]*)*)\s*\]" - ) - - matches = extras_pattern.match(requirement) - if matches: - return ( - canonicalize_name(matches.group(1)), - {extra.strip() for extra in matches.group(2).split(",")}, - ) - - return None, None - - -def _extract_wheel( - wheel_file: str, - extras: Dict[str, Set[str]], - enable_pipstar: bool, - platforms: List[wheel.Platform], - installation_dir: Path = Path("."), -) -> None: - """Extracts wheel into given directory and creates py_library and filegroup targets. - - Args: - wheel_file: the filepath of the .whl - installation_dir: the destination directory for installation of the wheel. - extras: a list of extras to add as dependencies for the installed wheel - enable_pipstar: if true, turns off certain operations. - """ - - whl = wheel.Wheel(wheel_file) - whl.unzip(installation_dir) - - metadata = { - "entry_points": [ - { - "name": name, - "module": module, - "attribute": attribute, - } - for name, (module, attribute) in sorted(whl.entry_points().items()) - ], - } - if not enable_pipstar: - extras_requested = extras[whl.name] if whl.name in extras else set() - dependencies = whl.dependencies(extras_requested, platforms) - - metadata.update( - { - "name": whl.name, - "version": whl.version, - "deps": dependencies.deps, - "deps_by_platform": dependencies.deps_select, - } - ) - - with open(os.path.join(installation_dir, "metadata.json"), "w") as f: - json.dump(metadata, f) - - def main() -> None: args = arguments.parser(description=__doc__).parse_args() deserialized_args = dict(vars(args)) @@ -130,19 +58,6 @@ def main() -> None: _configure_reproducible_wheels() - if args.whl_file: - whl = Path(args.whl_file) - - name, extras_for_pkg = _parse_requirement_for_extra(args.requirement) - extras = {name: extras_for_pkg} if extras_for_pkg and name else dict() - _extract_wheel( - wheel_file=whl, - extras=extras, - enable_pipstar=args.enable_pipstar, - platforms=arguments.get_platforms(args), - ) - return - pip_args = ( [sys.executable, "-m", "pip"] + (["--isolated"] if args.isolated else []) diff --git a/python/private/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl index 5cc53d84c6..37cc36492e 100644 --- a/python/private/pypi/whl_library.bzl +++ b/python/private/pypi/whl_library.bzl @@ -14,23 +14,23 @@ "" -load("@rules_python_internal//:rules_python_config.bzl", rp_config = "config") load("//python/private:auth.bzl", "AUTH_ATTRS", "get_auth") load("//python/private:envsubst.bzl", "envsubst") load("//python/private:is_standalone_interpreter.bzl", "is_standalone_interpreter") +load("//python/private:normalize_name.bzl", "normalize_name") load("//python/private:repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "repo_utils") load(":attrs.bzl", "ATTRS", "use_isolated") load(":deps.bzl", "all_repo_names", "record_files") load(":generate_whl_library_build_bazel.bzl", "generate_whl_library_build_bazel") -load(":parse_whl_name.bzl", "parse_whl_name") load(":patch_whl.bzl", "patch_whl") +load(":pep508_requirement.bzl", "requirement") load(":pypi_repo_utils.bzl", "pypi_repo_utils") -load(":whl_metadata.bzl", "whl_metadata") -load(":whl_target_platforms.bzl", "whl_target_platforms") +load(":urllib.bzl", "urllib") +load(":whl_extract.bzl", "whl_extract") +load(":whl_metadata.bzl", "parse_entry_points", "whl_metadata") _CPPFLAGS = "CPPFLAGS" _COMMAND_LINE_TOOLS_PATH_SLUG = "commandlinetools" -_WHEEL_ENTRY_POINT_PREFIX = "rules_python_wheel_entry_point" def _get_xcode_location_cflags(rctx, logger = None): """Query the xcode sdk location to update cflags @@ -149,21 +149,13 @@ def _parse_optional_attrs(rctx, args, extra_pip_args = None): if use_isolated(rctx, rctx.attr): args.append("--isolated") - # Bazel version 7.1.0 and later (and rolling releases from version 8.0.0-pre.20240128.3) - # support rctx.getenv(name, default): When building incrementally, any change to the value of - # the variable named by name will cause this repository to be re-fetched. - if "getenv" in dir(rctx): - getenv = rctx.getenv - else: - getenv = rctx.os.environ.get - # Check for None so we use empty default types from our attrs. # Some args want to be list, and some want to be dict. if extra_pip_args != None: args += [ "--extra_pip_args", json.encode(struct(arg = [ - envsubst(pip_arg, rctx.attr.envsubst, getenv) + envsubst(pip_arg, rctx.attr.envsubst, rctx.getenv) for pip_arg in extra_pip_args ])), ] @@ -188,7 +180,11 @@ def _parse_optional_attrs(rctx, args, extra_pip_args = None): if rctx.attr.add_libdir_to_library_search_path: if "LDFLAGS" in env: fail("Can't set both environment LDFLAGS and add_libdir_to_library_search_path") - command = [pypi_repo_utils.resolve_python_interpreter(rctx), "-c", "import sys ; sys.stdout.write('{}/lib'.format(sys.exec_prefix))"] + command = [ + pypi_repo_utils.resolve_python_interpreter(rctx), + "-c", + "import sys ; sys.stdout.write('{}/lib'.format(sys.exec_prefix))", + ] result = rctx.execute(command) if result.return_code != 0: fail("Failed to get LDFLAGS path: command: {}, exit code: {}, stdout: {}, stderr: {}".format(command, result.return_code, result.stdout, result.stderr)) @@ -264,27 +260,61 @@ def _create_repository_execution_environment(rctx, python_interpreter, logger = env[_CPPFLAGS] = " ".join(cppflags) return env -def _whl_library_impl(rctx): - logger = repo_utils.logger(rctx) - python_interpreter = pypi_repo_utils.resolve_python_interpreter( - rctx, - python_interpreter = rctx.attr.python_interpreter, - python_interpreter_target = rctx.attr.python_interpreter_target, +def _get_entry_points(rctx, install_dir_path, metadata): + dist_info_dir = "{}-{}.dist-info".format( + metadata.name.replace("-", "_"), + metadata.version.replace("-", "_"), ) - args = [ - "-m", - "python.private.pypi.whl_installer.wheel_installer", - "--requirement", - rctx.attr.requirement, - ] - extra_pip_args = [] - extra_pip_args.extend(rctx.attr.extra_pip_args) + entry_points_txt = install_dir_path.get_child(dist_info_dir).get_child("entry_points.txt") + if entry_points_txt.exists: + return parse_entry_points(rctx.read(entry_points_txt)) + return {} + +def _move_scripts_needing_shebang_rewrite(rctx, entry_points): + bin_dir = rctx.path("bin") + if not bin_dir.exists: + return + + ep_names = {name.lower(): True for name in entry_points} + for script in bin_dir.readdir(): + if script.is_dir: + continue + if script.basename.lower() in ep_names: + rctx.delete(script) + continue + if script.basename.endswith(".exe") or script.basename.endswith(".dll"): + continue + content = rctx.read(script) + if content.startswith("#!python"): + rewrite_bin_dir = rctx.path("rewrite-bin") + repo_utils.mkdir(rctx, rewrite_bin_dir) + repo_utils.rename(rctx, script, rctx.path("rewrite-bin/" + script.basename)) + +def _to_purl(*, index, metadata, filename): + """ + Produce a PyPI PURL from the metadata. - # Manually construct the PYTHONPATH since we cannot use the toolchain here - environment = _create_repository_execution_environment(rctx, python_interpreter, logger = logger) + https://github.com/package-url/purl-spec/blob/main/types-doc/pypi-definition.md + """ + + # https://github.com/package-url/purl-spec/blob/main/types-doc/pypi-definition.md#name-definition + name = normalize_name(metadata.name).replace("_", "-") + + qualifiers = {} + if index: + qualifiers["repository_url"] = index + if filename: + qualifiers["file_name"] = filename + + return "pkg:pypi/{}@{}?{}".format(name, metadata.version, "&".join(["{}={}".format(key, val) for key, val in qualifiers.items()])) + +def _whl_library_impl(rctx): + logger = repo_utils.logger(rctx) whl_path = None sdist_filename = None + extra_pip_args = [] + extra_pip_args.extend(rctx.attr.extra_pip_args) if rctx.attr.whl_file: rctx.watch(rctx.attr.whl_file) whl_path = rctx.path(rctx.attr.whl_file) @@ -295,6 +325,13 @@ def _whl_library_impl(rctx): elif rctx.attr.urls and rctx.attr.filename: filename = rctx.attr.filename urls = rctx.attr.urls + urls = [ + urllib.absolute_url( + envsubst(rctx.attr.index_url, rctx.attr.envsubst, rctx.getenv), + url, + ) + for url in urls + ] result = rctx.download( url = urls, output = filename, @@ -322,7 +359,29 @@ def _whl_library_impl(rctx): # build deps from PyPI (e.g. `flit_core`) if they are missing. extra_pip_args.extend(["--find-links", "."]) - args = _parse_optional_attrs(rctx, args, extra_pip_args) + # When we already have a wheel, Python isn't used, + # so there's no need to setup env vars to run Python, unless we need to + # build an sdist or resolve a requirement. + if whl_path: + environment = {} + args = [] + python_interpreter = None + else: + python_interpreter = pypi_repo_utils.resolve_python_interpreter( + rctx, + python_interpreter = rctx.attr.python_interpreter, + python_interpreter_target = rctx.attr.python_interpreter_target, + ) + args = [ + "-m", + "python.private.pypi.whl_installer.wheel_installer", + "--requirement", + rctx.attr.requirement, + ] + args = _parse_optional_attrs(rctx, args, extra_pip_args) + + # Manually construct the PYTHONPATH since we cannot use the toolchain here + environment = _create_repository_execution_environment(rctx, python_interpreter, logger = logger) if not whl_path: if rctx.attr.urls: @@ -361,208 +420,84 @@ def _whl_library_impl(rctx): if patches: whl_path = patch_whl( rctx, - op = "whl_library.PatchWhl({}, {})".format(rctx.attr.name, rctx.attr.requirement), - python_interpreter = python_interpreter, whl_path = whl_path, patches = patches, - quiet = rctx.attr.quiet, - timeout = rctx.attr.timeout, - ) - - if rp_config.enable_pipstar: - pypi_repo_utils.execute_checked( - rctx, - op = "whl_library.ExtractWheel({}, {})".format(rctx.attr.name, whl_path), - python = python_interpreter, - arguments = args + [ - "--whl-file", - whl_path, - "--enable-pipstar", - ], - srcs = rctx.attr._python_srcs, - environment = environment, - quiet = rctx.attr.quiet, - timeout = rctx.attr.timeout, - logger = logger, - ) - - metadata = json.decode(rctx.read("metadata.json")) - rctx.delete("metadata.json") - - # NOTE @aignas 2024-06-22: this has to live on until we stop supporting - # passing `twine` as a `:pkg` library via the `WORKSPACE` builds. - # - # See ../../packaging.bzl line 190 - entry_points = {} - for item in metadata["entry_points"]: - name = item["name"] - module = item["module"] - attribute = item["attribute"] - - # There is an extreme edge-case with entry_points that end with `.py` - # See: https://github.com/bazelbuild/bazel/blob/09c621e4cf5b968f4c6cdf905ab142d5961f9ddc/src/test/java/com/google/devtools/build/lib/rules/python/PyBinaryConfiguredTargetTest.java#L174 - entry_point_without_py = name[:-3] + "_py" if name.endswith(".py") else name - entry_point_target_name = ( - _WHEEL_ENTRY_POINT_PREFIX + "_" + entry_point_without_py - ) - entry_point_script_name = entry_point_target_name + ".py" - - rctx.file( - entry_point_script_name, - _generate_entry_point_contents(module, attribute), ) - entry_points[entry_point_without_py] = entry_point_script_name - metadata = whl_metadata( - install_dir = whl_path.dirname.get_child("site-packages"), - read_fn = rctx.read, - logger = logger, - ) - - build_file_contents = generate_whl_library_build_bazel( - name = whl_path.basename, - sdist_filename = sdist_filename, - dep_template = rctx.attr.dep_template or "@{}{{name}}//:{{target}}".format(rctx.attr.repo_prefix), - entry_points = entry_points, - metadata_name = metadata.name, - metadata_version = metadata.version, - requires_dist = metadata.requires_dist, - # TODO @aignas 2025-05-17: maybe have a build flag for this instead - enable_implicit_namespace_pkgs = rctx.attr.enable_implicit_namespace_pkgs, - # TODO @aignas 2025-04-14: load through the hub: - annotation = None if not rctx.attr.annotation else struct(**json.decode(rctx.read(rctx.attr.annotation))), - data_exclude = rctx.attr.pip_data_exclude, - group_deps = rctx.attr.group_deps, - group_name = rctx.attr.group_name, - ) - else: - target_platforms = rctx.attr.experimental_target_platforms or [] - if target_platforms: - parsed_whl = parse_whl_name(whl_path.basename) - - # NOTE @aignas 2023-12-04: if the wheel is a platform specific wheel, we - # only include deps for that target platform - if parsed_whl.platform_tag != "any": - target_platforms = [ - p.target_platform - for p in whl_target_platforms( - platform_tag = parsed_whl.platform_tag, - abi_tag = parsed_whl.abi_tag.strip("tm"), - ) - ] - - pypi_repo_utils.execute_checked( - rctx, - op = "whl_library.ExtractWheel({}, {})".format(rctx.attr.name, whl_path), - python = python_interpreter, - arguments = args + [ - "--whl-file", - whl_path, - ] + ["--platform={}".format(p) for p in target_platforms], - srcs = rctx.attr._python_srcs, - environment = environment, - quiet = rctx.attr.quiet, - timeout = rctx.attr.timeout, - logger = logger, - ) + whl_extract(rctx, whl_path = whl_path, logger = logger) - metadata = json.decode(rctx.read("metadata.json")) - rctx.delete("metadata.json") + install_dir_path = whl_path.dirname.get_child("site-packages") + metadata = whl_metadata( + install_dir = install_dir_path, + read_fn = rctx.read, + logger = logger, + ) + namespace_package_files = pypi_repo_utils.find_namespace_package_files(rctx, install_dir_path) - # NOTE @aignas 2024-06-22: this has to live on until we stop supporting - # passing `twine` as a `:pkg` library via the `WORKSPACE` builds. - # - # See ../../packaging.bzl line 190 - entry_points = {} - for item in metadata["entry_points"]: - name = item["name"] - module = item["module"] - attribute = item["attribute"] - - # There is an extreme edge-case with entry_points that end with `.py` - # See: https://github.com/bazelbuild/bazel/blob/09c621e4cf5b968f4c6cdf905ab142d5961f9ddc/src/test/java/com/google/devtools/build/lib/rules/python/PyBinaryConfiguredTargetTest.java#L174 - entry_point_without_py = name[:-3] + "_py" if name.endswith(".py") else name - entry_point_target_name = ( - _WHEEL_ENTRY_POINT_PREFIX + "_" + entry_point_without_py - ) - entry_point_script_name = entry_point_target_name + ".py" + entry_points = _get_entry_points(rctx, install_dir_path, metadata) + _move_scripts_needing_shebang_rewrite(rctx, entry_points) - rctx.file( - entry_point_script_name, - _generate_entry_point_contents(module, attribute), - ) - entry_points[entry_point_without_py] = entry_point_script_name - - build_file_contents = generate_whl_library_build_bazel( - name = whl_path.basename, - sdist_filename = sdist_filename, - dep_template = rctx.attr.dep_template or "@{}{{name}}//:{{target}}".format(rctx.attr.repo_prefix), - entry_points = entry_points, - # TODO @aignas 2025-05-17: maybe have a build flag for this instead - enable_implicit_namespace_pkgs = rctx.attr.enable_implicit_namespace_pkgs, - # TODO @aignas 2025-04-14: load through the hub: - dependencies = metadata["deps"], - dependencies_by_platform = metadata["deps_by_platform"], - annotation = None if not rctx.attr.annotation else struct(**json.decode(rctx.read(rctx.attr.annotation))), - data_exclude = rctx.attr.pip_data_exclude, - group_deps = rctx.attr.group_deps, - group_name = rctx.attr.group_name, - tags = [ - "pypi_name={}".format(metadata["name"]), - "pypi_version={}".format(metadata["version"]), - ], - ) + build_file_contents = generate_whl_library_build_bazel( + name = whl_path.basename, + sdist_filename = sdist_filename, + dep_template = rctx.attr.dep_template or "@{}{{name}}//:{{target}}".format( + rctx.attr.repo_prefix, + ), + config_load = rctx.attr.config_load, + metadata_name = metadata.name, + metadata_version = metadata.version, + requires_dist = metadata.requires_dist, + # TODO @aignas 2025-05-17: maybe have a build flag for this instead + enable_implicit_namespace_pkgs = rctx.attr.enable_implicit_namespace_pkgs, + # TODO @aignas 2025-04-14: load through the hub: + annotation = None if not rctx.attr.annotation else struct(**json.decode(rctx.read(rctx.attr.annotation))), + data_exclude = rctx.attr.pip_data_exclude, + group_deps = rctx.attr.group_deps, + group_name = rctx.attr.group_name, + namespace_package_files = namespace_package_files, + extras = requirement(rctx.attr.requirement).extras, + entry_points = entry_points, + purl = _to_purl( + index = rctx.attr.index_url, + metadata = metadata, + filename = sdist_filename or whl_path.basename, + ), + ) # Delete these in case the wheel had them. They generally don't cause # a problem, but let's avoid the chance of that happening. rctx.file("WORKSPACE") rctx.file("WORKSPACE.bazel") rctx.file("MODULE.bazel") - rctx.file("REPO.bazel") + rctx.file("REPO.bazel", """\ +repo( + default_package_metadata = [ + "//:package_metadata", + ], +) +""") + # BUILD files interfere with globbing and Bazel package boundaries. + _remove_files(rctx, "BUILD", "BUILD.bazel") + rctx.file("BUILD.bazel", build_file_contents) + + if hasattr(rctx, "repo_metadata"): + return rctx.repo_metadata(reproducible = True) + + return None + +def _remove_files(rctx, *basenames): paths = list(rctx.path(".").readdir()) for _ in range(10000000): if not paths: break path = paths.pop() - # BUILD files interfere with globbing and Bazel package boundaries. - if path.basename in ("BUILD", "BUILD.bazel"): + if path.basename in basenames: rctx.delete(path) elif path.is_dir: paths.extend(path.readdir()) - rctx.file("BUILD.bazel", build_file_contents) - return - -def _generate_entry_point_contents( - module, - attribute, - shebang = "#!/usr/bin/env python3"): - """Generate the contents of an entry point script. - - Args: - module (str): The name of the module to use. - attribute (str): The name of the attribute to call. - shebang (str, optional): The shebang to use for the entry point python - file. - - Returns: - str: A string of python code. - """ - contents = """\ -{shebang} -import sys -from {module} import {attribute} -if __name__ == "__main__": - sys.exit({attribute}()) -""".format( - shebang = shebang, - module = module, - attribute = attribute, - ) - return contents - # NOTE @aignas 2024-03-21: The usage of dict({}, **common) ensures that all args to `dict` are unique whl_library_attrs = dict({ "annotation": attr.label( @@ -572,6 +507,9 @@ whl_library_attrs = dict({ ), allow_files = True, ), + "config_load": attr.string( + doc = "The load location for configuration for pipstar.", + ), "dep_template": attr.string( doc = """ The dep template to use for referencing the dependencies. It should have `{name}` @@ -592,6 +530,9 @@ For example if your whl depends on `numpy` and your Python package repo is named "group_name": attr.string( doc = "Name of the group, if any.", ), + "index_url": attr.string( + doc = "The index_url that the package will be downloaded from.", + ), "repo": attr.string( doc = "Pointer to parent repo name. Used to make these rules rerun if the parent repo changes.", ), @@ -658,8 +599,6 @@ way to define whl_library and move whl patching to a separate place. INTERNAL US "_python_srcs": attr.label_list( # Used as a default value in a rule to ensure we fetch the dependencies. default = [ - Label("//python/private/pypi/whl_installer:platform.py"), - Label("//python/private/pypi/whl_installer:wheel.py"), Label("//python/private/pypi/whl_installer:wheel_installer.py"), Label("//python/private/pypi/whl_installer:arguments.py"), ] + record_files.values(), @@ -672,7 +611,13 @@ whl_library = repository_rule( attrs = whl_library_attrs, doc = """ Download and extracts a single wheel based into a bazel repo based on the requirement string passed in. -Instantiated from pip_repository and inherits config options from there.""", +Instantiated from pip_repository and inherits config options from there. + +:::{versionchanged} 1.9.0 +The `whl_library` is marked as reproducible if using starlark to extract and parse the +wheel contents without building an `sdist` first. +::: +""", implementation = _whl_library_impl, environ = [ "RULES_PYTHON_PIP_ISOLATED", diff --git a/python/private/pypi/whl_library_targets.bzl b/python/private/pypi/whl_library_targets.bzl index aed5bc74f5..933b529053 100644 --- a/python/private/pypi/whl_library_targets.bzl +++ b/python/private/pypi/whl_library_targets.bzl @@ -17,7 +17,6 @@ load("@bazel_skylib//rules:copy_file.bzl", "copy_file") load("//python:py_binary.bzl", "py_binary") load("//python:py_library.bzl", "py_library") -load("//python/private:glob_excludes.bzl", "glob_excludes") load("//python/private:normalize_name.bzl", "normalize_name") load(":env_marker_setting.bzl", "env_marker_setting") load( @@ -27,12 +26,13 @@ load( "EXTRACTED_WHEEL_FILES", "PY_LIBRARY_IMPL_LABEL", "PY_LIBRARY_PUBLIC_LABEL", - "WHEEL_ENTRY_POINT_PREFIX", "WHEEL_FILE_IMPL_LABEL", "WHEEL_FILE_PUBLIC_LABEL", ) load(":namespace_pkgs.bzl", _create_inits = "create_inits") load(":pep508_deps.bzl", "deps") +load(":venv_entry_point.bzl", "venv_entry_point") +load(":venv_rewrite_shebang.bzl", "venv_rewrite_shebang") # Files that are special to the Bazel processing of things. _BAZEL_REPO_FILE_GLOBS = [ @@ -40,10 +40,13 @@ _BAZEL_REPO_FILE_GLOBS = [ "BUILD.bazel", "REPO.bazel", "WORKSPACE", - "WORKSPACE", + "WORKSPACE.bzlmod", "WORKSPACE.bazel", ] +_IS_VENV_SITE_PACKAGES_YES = Label("//python/config_settings:_is_venvs_site_packages_yes") +_VENV_SITE_PACKAGES_FLAG = Label("//python/config_settings:venvs_site_packages") + def whl_library_targets_from_requires( *, name, @@ -51,6 +54,7 @@ def whl_library_targets_from_requires( metadata_version = "", requires_dist = [], extras = [], + entry_points = {}, include = [], group_deps = [], **kwargs): @@ -67,6 +71,7 @@ def whl_library_targets_from_requires( requires_dist: {type}`list[str]` The list of `Requires-Dist` values from the whl `METADATA`. extras: {type}`list[str]` The list of requested extras. This essentially includes extra transitive dependencies in the final targets depending on the wheel `METADATA`. + entry_points: {type}`list[dict]` A list of parsed entry point definitions. include: {type}`list[str]` The list of packages to include. **kwargs: Extra args passed to the {obj}`whl_library_targets` """ @@ -82,6 +87,7 @@ def whl_library_targets_from_requires( name = name, dependencies = package_deps.deps, dependencies_with_markers = package_deps.deps_select, + entry_points = entry_points, tags = [ "pypi_name={}".format(metadata_name), "pypi_version={}".format(metadata_version), @@ -114,20 +120,21 @@ def whl_library_targets( tags = [], dependencies = [], filegroups = None, - dependencies_by_platform = {}, dependencies_with_markers = {}, - group_deps = [], + entry_points = {}, group_name = "", data = [], copy_files = {}, copy_executables = {}, - entry_points = {}, native = native, enable_implicit_namespace_pkgs = False, + namespace_package_files = [], rules = struct( copy_file = copy_file, py_binary = py_binary, py_library = py_library, + venv_entry_point = venv_entry_point, + venv_rewrite_shebang = venv_rewrite_shebang, env_marker_setting = env_marker_setting, create_inits = _create_inits, )): @@ -142,20 +149,15 @@ def whl_library_targets( the filename of the sdist. tags: {type}`list[str]` The tags set on the `py_library`. dependencies: {type}`list[str]` A list of dependencies. - dependencies_by_platform: {type}`dict[str, list[str]]` A list of - dependencies by platform key. dependencies_with_markers: {type}`dict[str, str]` A marker to evaluate in order for the dep to be included. + entry_points: {type}`list[dict]` A list of parsed entry point definitions. filegroups: {type}`dict[str, list[str]] | None` A dictionary of the target names and the glob matches. If `None`, defaults will be used. group_name: {type}`str` name of the dependency group (if any) which contains this library. If set, this library will behave as a shim to group implementation rules which will provide simultaneously installed dependencies which would otherwise form a cycle. - group_deps: {type}`list[str]` names of fellow members of the group (if - any). These will be excluded from generated deps lists so as to avoid - direct cycles. These dependencies will be provided at runtime by the - group rules which wrap this library and its fellows together. copy_executables: {type}`dict[str, str]` The mapping between src and dest locations for the targets. copy_files: {type}`dict[str, str]` The mapping between src and @@ -165,21 +167,47 @@ def whl_library_targets( srcs_exclude: {type}`list[str]` The globs for srcs attribute exclusion in `py_library`. data: {type}`list[str]` A list of labels to include as part of the `data` attribute in `py_library`. - entry_points: {type}`dict[str, str]` The mapping between the script - name and the python file to use. DEPRECATED. enable_implicit_namespace_pkgs: {type}`boolean` generate __init__.py files for namespace pkgs. native: {type}`native` The native struct for overriding in tests. + namespace_package_files: {type}`list[str]` A list of labels of files whose + directories are namespace packages. rules: {type}`struct` A struct with references to rules for creating targets. """ dependencies = sorted([normalize_name(d) for d in dependencies]) - dependencies_by_platform = { - platform: sorted([normalize_name(d) for d in deps]) - for platform, deps in dependencies_by_platform.items() - } tags = sorted(tags) data = [] + data + bins_for_data_label = [] + + for ep_dict in entry_points.values(): + kwargs = dict(ep_dict) + ep_name = kwargs.pop("name") + ep_target_name = "bin/{}".format(ep_name) + rules.venv_entry_point( + name = ep_target_name, + **kwargs + ) + bins_for_data_label.append(ep_target_name) + data.append(ep_target_name) + + existing_bin_names = {ep["name"].lower(): None for ep in entry_points.values()} + for p in native.glob(["bin/*"], allow_empty = True): + existing_bin_names[p[len("bin/"):].lower()] = None + + for src_path in native.glob(["rewrite-bin/*"], allow_empty = True): + script_name = src_path[len("rewrite-bin/"):] + if script_name.lower() in existing_bin_names: + continue + rewrite_target_name = "bin/{}".format(script_name) + rules.venv_rewrite_shebang( + name = rewrite_target_name, + src = src_path, + package = name, + ) + bins_for_data_label.append(rewrite_target_name) + data.append(rewrite_target_name) + if filegroups == None: filegroups = { EXTRACTED_WHEEL_FILES: dict( @@ -193,15 +221,18 @@ def whl_library_targets( include = ["site-packages/*.dist-info/**"], ), DATA_LABEL: dict( - include = ["data/**"], + include = ["data/**", "bin/**", "include/**"], ), } for filegroup_name, glob_kwargs in filegroups.items(): glob_kwargs = {"allow_empty": True} | glob_kwargs + srcs = native.glob(**glob_kwargs) + if filegroup_name == DATA_LABEL: + srcs = srcs + bins_for_data_label native.filegroup( name = filegroup_name, - srcs = native.glob(**glob_kwargs), + srcs = srcs, visibility = ["//visibility:public"], ) @@ -224,9 +255,7 @@ def whl_library_targets( data.append(dest) _config_settings( - dependencies_by_platform = dependencies_by_platform.keys(), dependencies_with_markers = dependencies_with_markers, - native = native, rules = rules, visibility = ["//visibility:private"], ) @@ -235,52 +264,29 @@ def whl_library_targets( for d in dependencies_with_markers } - # TODO @aignas 2024-10-25: remove the entry_point generation once - # `py_console_script_binary` is the only way to use entry points. - for entry_point, entry_point_script_name in entry_points.items(): - rules.py_binary( - name = "{}_{}".format(WHEEL_ENTRY_POINT_PREFIX, entry_point), - # Ensure that this works on Windows as well - script may have Windows path separators. - srcs = [entry_point_script_name.replace("\\", "/")], - # This makes this directory a top-level in the python import - # search path for anything that depends on this. - imports = ["."], - deps = [":" + PY_LIBRARY_PUBLIC_LABEL], - visibility = ["//visibility:public"], - ) - - # Ensure this list is normalized - # Note: mapping used as set - group_deps = { - normalize_name(d): True - for d in group_deps - } - - dependencies = [ - d - for d in dependencies - if d not in group_deps - ] - dependencies_by_platform = { - p: deps - for p, deps in dependencies_by_platform.items() - for deps in [[d for d in deps if d not in group_deps]] - if deps - } - # If this library is a member of a group, its public label aliases need to # point to the group implementation rule not the implementation rules. We # also need to mark the implementation rules as visible to the group # implementation. if group_name and "//:" in dep_template: # This is the legacy behaviour where the group library is outside the hub repo + # + # It is expected to disappear when we drop WORKSPACE or drop the vendoring of + # pip_parse `requirements.bzl` in WORKSPACE. The alternative would be to add + # another argument to the macro, but it is already full of arguments. label_tmpl = dep_template.format( - name = "_groups", + name = "_config", target = normalize_name(group_name) + "_{}", + ).replace( + "//:", + "//_groups:", ) impl_vis = [dep_template.format( - name = "_groups", + name = "_config", target = "__pkg__", + ).replace( + "//:", + "//_groups:", )] native.alias( @@ -312,16 +318,8 @@ def whl_library_targets( srcs = [name], data = _deps( deps = dependencies, - deps_by_platform = dependencies_by_platform, deps_conditional = deps_conditional, tmpl = dep_template.format(name = "{}", target = WHEEL_FILE_PUBLIC_LABEL), - # NOTE @aignas 2024-10-28: Actually, `select` is not part of - # `native`, but in order to support bazel 6.4 in unit tests, I - # have to somehow pass the `select` implementation in the unit - # tests and I chose this to be routed through the `native` - # struct. So, tests` will be successful in `getattr` and the - # real code will use the fallback provided here. - select = getattr(native, "select", select), ), visibility = impl_vis, ) @@ -342,11 +340,9 @@ def whl_library_targets( "**/*.py", "**/*.pyc", "**/*.pyc.*", # During pyc creation, temp files named *.pyc.NNNN are created - # RECORD is known to contain sha256 checksums of files which might include the checksums - # of generated files produced when wheels are installed. The file is ignored to avoid - # Bazel caching issues. - "**/*.dist-info/RECORD", - ] + glob_excludes.version_dependent_exclusions() + ] + if sdist_filename: + _data_exclude.append("**/*.dist-info/RECORD") for item in data_exclude: if item not in _data_exclude: _data_exclude.append(item) @@ -354,6 +350,7 @@ def whl_library_targets( data = data + native.glob( ["site-packages/**/*"], exclude = _data_exclude, + allow_empty = True, ) pyi_srcs = native.glob( @@ -362,14 +359,20 @@ def whl_library_targets( ) if not enable_implicit_namespace_pkgs: - srcs = srcs + getattr(native, "select", select)({ - Label("//python/config_settings:is_venvs_site_packages"): [], + generated_namespace_package_files = select({ + _IS_VENV_SITE_PACKAGES_YES: [], "//conditions:default": rules.create_inits( srcs = srcs + data + pyi_srcs, ignored_dirnames = [], # If you need to ignore certain folders, you can patch rules_python here to do so. root = "site-packages", ), }) + namespace_package_files += generated_namespace_package_files + srcs = srcs + generated_namespace_package_files + + # This is done after create_inits() is called so that the data scheme + # files don't have such files created in their directories. + data = data + [DATA_LABEL] rules.py_library( name = py_library_label, @@ -381,32 +384,22 @@ def whl_library_targets( imports = ["site-packages"], deps = _deps( deps = dependencies, - deps_by_platform = dependencies_by_platform, deps_conditional = deps_conditional, tmpl = dep_template.format(name = "{}", target = PY_LIBRARY_PUBLIC_LABEL), - select = getattr(native, "select", select), ), tags = tags, visibility = impl_vis, - experimental_venvs_site_packages = Label("@rules_python//python/config_settings:venvs_site_packages"), + experimental_venvs_site_packages = _VENV_SITE_PACKAGES_FLAG, + namespace_package_files = namespace_package_files, ) -def _config_settings(dependencies_by_platform, dependencies_with_markers, rules, native = native, **kwargs): +def _config_settings(dependencies_with_markers, rules, **kwargs): """Generate config settings for the targets. Args: - dependencies_by_platform: {type}`list[str]` platform keys, can be - one of the following formats: - * `//conditions:default` - * `@platforms//os:{value}` - * `@platforms//cpu:{value}` - * `@//python/config_settings:is_python_3.{minor_version}` - * `{os}_{cpu}` - * `cp3{minor_version}_{os}_{cpu}` dependencies_with_markers: {type}`dict[str, str]` The markers to evaluate by each dep. rules: used for testing - native: {type}`native` The native struct for overriding in tests. **kwargs: Extra kwargs to pass to the rule. """ for dep, expression in dependencies_with_markers.items(): @@ -416,46 +409,7 @@ def _config_settings(dependencies_by_platform, dependencies_with_markers, rules, **kwargs ) - for p in dependencies_by_platform: - if p.startswith("@") or p.endswith("default"): - continue - - # TODO @aignas 2025-04-20: add tests here - abi, _, tail = p.partition("_") - if not abi.startswith("cp"): - tail = p - abi = "" - os, _, arch = tail.partition("_") - - _kwargs = dict(kwargs) - _kwargs["constraint_values"] = [ - "@platforms//cpu:{}".format(arch), - "@platforms//os:{}".format(os), - ] - - if abi: - _kwargs["flag_values"] = { - Label("//python/config_settings:python_version"): "3.{}".format(abi[len("cp3"):]), - } - - native.config_setting( - name = "is_{name}".format( - name = p.replace("cp3", "python_3."), - ), - **_kwargs - ) - -def _plat_label(plat): - if plat.endswith("default"): - return plat - elif plat.startswith("@//"): - return Label(plat.strip("@")) - elif plat.startswith("@"): - return plat - else: - return ":is_" + plat.replace("cp3", "python_3.") - -def _deps(deps, deps_by_platform, deps_conditional, tmpl, select = select): +def _deps(deps, deps_conditional, tmpl): deps = [tmpl.format(d) for d in sorted(deps)] for dep, setting in deps_conditional.items(): @@ -464,22 +418,4 @@ def _deps(deps, deps_by_platform, deps_conditional, tmpl, select = select): "//conditions:default": [], }) - if not deps_by_platform: - return deps - - deps_by_platform = { - _plat_label(p): [ - tmpl.format(d) - for d in sorted(deps) - ] - for p, deps in sorted(deps_by_platform.items()) - } - - # Add the default, which means that we will be just using the dependencies in - # `deps` for platforms that are not handled in a special way by the packages - deps_by_platform.setdefault("//conditions:default", []) - - if not deps: - return select(deps_by_platform) - else: - return deps + select(deps_by_platform) + return deps diff --git a/python/private/pypi/whl_metadata.bzl b/python/private/pypi/whl_metadata.bzl index cf2d51afda..2981a5d92f 100644 --- a/python/private/pypi/whl_metadata.bzl +++ b/python/private/pypi/whl_metadata.bzl @@ -23,10 +23,15 @@ def whl_metadata(*, install_dir, read_fn, logger): """ metadata_file = find_whl_metadata(install_dir = install_dir, logger = logger) contents = read_fn(metadata_file) + result = parse_whl_metadata(contents) if not (result.name and result.version): - logger.fail("Failed to parsed the wheel METADATA file:\n{}".format(contents)) + logger.fail("Failed to parse the wheel METADATA file:\n{}\n{}\n{}".format( + 80 * "=", + contents.rstrip("\n"), + 80 * "=", + )) return None return result @@ -106,3 +111,58 @@ def find_whl_metadata(*, install_dir, logger): else: logger.fail("The '*.dist-info' directory could not be found in '{}'".format(install_dir.basename)) return None + +def parse_entry_points(contents): + """Parses entry_points.txt contents and returns console_scripts and gui_scripts entries. + + Args: + contents: {type}`str` The contents of the entry_points.txt file. + + Returns: + {type}`dict[str, dict]` A dict keyed by the original entry point name. + """ + entries = {} + seen_lower_names = {} + current_group = None + current_group_lower = None + for line in contents.splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + if line.startswith("[") and line.endswith("]"): + current_group = line[1:-1].strip() + current_group_lower = current_group.lower() + continue + + if current_group_lower in ("console_scripts", "gui_scripts"): + name, _, ref = line.partition("=") + name = name.strip() + + # Names are case-insensitive. + # See https://packaging.python.org/en/latest/specifications/entry-points/#data-model + # Entry points must be unique for a given name because they turn + # into files and may be on a case-insensitive file system. + lower_name = name.lower() + if lower_name in seen_lower_names: + continue + seen_lower_names[lower_name] = True + + # remove inline comments + ref, _, _ = ref.partition("#") + ref = ref.strip() + + extras = "" + if "[" in ref and ref.endswith("]"): + ref, _, extras_part = ref.partition("[") + extras = extras_part[:-1].strip() + ref = ref.strip() + + module, _, attribute = ref.partition(":") + entries[name] = { + "attribute": attribute.strip(), + "extras": extras, + "group": current_group, + "module": module.strip(), + "name": name, + } + return entries diff --git a/python/private/pypi/whl_repo_name.bzl b/python/private/pypi/whl_repo_name.bzl index 2b3b5418aa..29d774c361 100644 --- a/python/private/pypi/whl_repo_name.bzl +++ b/python/private/pypi/whl_repo_name.bzl @@ -18,12 +18,14 @@ load("//python/private:normalize_name.bzl", "normalize_name") load(":parse_whl_name.bzl", "parse_whl_name") -def whl_repo_name(filename, sha256): +def whl_repo_name(filename, sha256, *target_platforms): """Return a valid whl_library repo name given a distribution filename. Args: filename: {type}`str` the filename of the distribution. sha256: {type}`str` the sha256 of the distribution. + *target_platforms: {type}`list[str]` the extra suffixes to append. + Only used when we need to support different extras per version. Returns: a string that can be used in {obj}`whl_library`. @@ -59,6 +61,8 @@ def whl_repo_name(filename, sha256): elif version: parts.insert(1, version) + parts.extend([p.partition("_")[-1] for p in target_platforms]) + return "_".join(parts) def pypi_repo_name(whl_name, *target_platforms): diff --git a/python/private/pypi/whl_target_platforms.bzl b/python/private/pypi/whl_target_platforms.bzl index 6c3dd5da83..28547c679c 100644 --- a/python/private/pypi/whl_target_platforms.bzl +++ b/python/private/pypi/whl_target_platforms.bzl @@ -30,6 +30,7 @@ _CPU_ALIASES = { "ppc": "ppc", "ppc64": "ppc", "ppc64le": "ppc64le", + "riscv64": "riscv64", "s390x": "s390x", "arm": "arm", "armv6l": "arm", diff --git a/python/private/pyproject_utils.bzl b/python/private/pyproject_utils.bzl new file mode 100644 index 0000000000..e96ffd2023 --- /dev/null +++ b/python/private/pyproject_utils.bzl @@ -0,0 +1,51 @@ +"""Utilities for reading values from pyproject.toml.""" + +load("@toml.bzl", "toml") +load(":version.bzl", "version") + +def read_pyproject(module_ctx, pyproject): + """Read a pyproject.toml file and return the relevant fields. + + The file is parsed with a pure-Starlark TOML decoder; no Python + interpreter is required. The raw `requires-python` value is returned + as-is so that callers can decide how to interpret it. + + Args: + module_ctx: the module extension context (needs `path` and `read`). + pyproject: {type}`Label` pointing at the pyproject.toml file. + + Returns: + {type}`struct` with the attributes: + * `requires_python`: {type}`str | None` the raw `requires-python` + value (e.g. `"==3.13.9"`), or `None` if it is not set. + """ + data = toml.decode(module_ctx.read(module_ctx.path(pyproject), watch = "yes")) + return struct( + requires_python = data.get("project", {}).get("requires-python"), + ) + +def version_from_requires_python(requires_python): + """Derive a concrete Python version from a `requires-python` value. + + Currently only an exact `==X.Y.Z` specifier is supported. The value is + validated and normalized via {obj}`//python/private:version.bzl` so that + malformed input fails in a consistent way. Broader specifier support + (e.g. `>=`, `X.Y`) can be layered on here in the future. + + Args: + requires_python: {type}`str` the raw `requires-python` value. + + Returns: + {type}`str` the normalized version string (e.g. `"3.13.9"`). + """ + if not requires_python: + fail("`requires-python` must be specified") + + if not requires_python.startswith("=="): + fail("`requires-python` must pin an exact version with `==`, got: {}".format(requires_python)) + + bare_version = requires_python[len("=="):].strip() + + # Parse strictly so malformed versions fail cleanly, then normalize. + version.parse(bare_version, strict = True) + return version.normalize(bare_version) diff --git a/python/private/python.bzl b/python/private/python.bzl index 6eb8a3742e..f098a626fb 100644 --- a/python/private/python.bzl +++ b/python/private/python.bzl @@ -15,10 +15,12 @@ "Python toolchain module extensions for use with bzlmod." load("@bazel_features//:features.bzl", "bazel_features") -load("//python:versions.bzl", "DEFAULT_RELEASE_BASE_URL", "PLATFORMS", "TOOL_VERSIONS") +load("//python:versions.bzl", "DEFAULT_RELEASE_BASE_URLS", "PLATFORMS") load(":auth.bzl", "AUTH_ATTRS") load(":full_version.bzl", "full_version") +load(":pbs_manifest.bzl", "parse_runtime_manifest") load(":platform_info.bzl", "platform_info") +load(":pyproject_utils.bzl", "read_pyproject", "version_from_requires_python") load(":python_register_toolchains.bzl", "python_register_toolchains") load(":pythons_hub.bzl", "hub_repo") load(":repo_utils.bzl", "repo_utils") @@ -29,10 +31,9 @@ load( "sorted_host_platform_names", "sorted_host_platforms", ) -load(":util.bzl", "IS_BAZEL_6_4_OR_HIGHER") load(":version.bzl", "version") -def parse_modules(*, module_ctx, logger, _fail = fail): +def parse_modules(*, module_ctx, logger = None, _fail = fail): """Parse the modules and return a struct for registrations. Args: @@ -57,7 +58,7 @@ def parse_modules(*, module_ctx, logger, _fail = fail): platform suffix. * register_coverage_tool: bool """ - if module_ctx.os.environ.get("RULES_PYTHON_BZLMOD_DEBUG", "0") == "1": + if module_ctx.getenv("RULES_PYTHON_BZLMOD_DEBUG", "0") == "1": debug_info = { "toolchains_registered": [], } @@ -77,55 +78,9 @@ def parse_modules(*, module_ctx, logger, _fail = fail): # Map of string Major.Minor or Major.Minor.Patch to the toolchain_info struct global_toolchain_versions = {} - ignore_root_user_error = None + config = _get_toolchain_config(mctx = module_ctx, modules = module_ctx.modules, _fail = _fail) - # if the root module does not register any toolchain then the - # ignore_root_user_error takes its default value: True - if not module_ctx.modules[0].tags.toolchain: - ignore_root_user_error = True - - config = _get_toolchain_config(modules = module_ctx.modules, _fail = _fail) - - default_python_version = None - for mod in module_ctx.modules: - defaults_attr_structs = _create_defaults_attr_structs(mod = mod) - default_python_version_env = None - default_python_version_file = None - - # Only the root module and rules_python are allowed to specify the default - # toolchain for a couple reasons: - # * It prevents submodules from specifying different defaults and only - # one of them winning. - # * rules_python needs to set a soft default in case the root module doesn't, - # e.g. if the root module doesn't use Python itself. - # * The root module is allowed to override the rules_python default. - if mod.is_root or (mod.name == "rules_python" and not default_python_version): - for defaults_attr in defaults_attr_structs: - default_python_version = _one_or_the_same( - default_python_version, - defaults_attr.python_version, - onerror = _fail_multiple_defaults_python_version, - ) - default_python_version_env = _one_or_the_same( - default_python_version_env, - defaults_attr.python_version_env, - onerror = _fail_multiple_defaults_python_version_env, - ) - default_python_version_file = _one_or_the_same( - default_python_version_file, - defaults_attr.python_version_file, - onerror = _fail_multiple_defaults_python_version_file, - ) - if default_python_version_file: - default_python_version = _one_or_the_same( - default_python_version, - module_ctx.read(default_python_version_file, watch = "yes").strip(), - ) - if default_python_version_env: - default_python_version = module_ctx.getenv( - default_python_version_env, - default_python_version, - ) + default_python_version = _compute_default_python_version(module_ctx) seen_versions = {} for mod in module_ctx.modules: @@ -134,6 +89,7 @@ def parse_modules(*, module_ctx, logger, _fail = fail): mod = mod, seen_versions = seen_versions, config = config, + default_python_version = default_python_version, ) for toolchain_attr in toolchain_attr_structs: @@ -153,32 +109,17 @@ def parse_modules(*, module_ctx, logger, _fail = fail): # * rules_python needs to set a soft default in case the root module doesn't, # e.g. if the root module doesn't use Python itself. # * The root module is allowed to override the rules_python default. - if default_python_version: - is_default = default_python_version == toolchain_version - if toolchain_attr.is_default and not is_default: - fail("The 'is_default' attribute doesn't work if you set " + - "the default Python version with the `defaults` tag.") - else: - is_default = toolchain_attr.is_default - - # Also only the root module should be able to decide ignore_root_user_error. - # Modules being depended upon don't know the final environment, so they aren't - # in the right position to know or decide what the correct setting is. + is_default = default_python_version == toolchain_version - # If an inconsistency in the ignore_root_user_error among multiple toolchains is detected, fail. - if ignore_root_user_error != None and toolchain_attr.ignore_root_user_error != ignore_root_user_error: - fail("Toolchains in the root module must have consistent 'ignore_root_user_error' attributes") - - ignore_root_user_error = toolchain_attr.ignore_root_user_error - elif mod.name == "rules_python" and not default_toolchain and not default_python_version: - # We don't do the len() check because we want the default that rules_python - # sets to be clearly visible. - is_default = toolchain_attr.is_default + elif mod.name == "rules_python" and not default_toolchain: + # This branch handles when the root module doesn't declare a + # Python toolchain + is_default = default_python_version == toolchain_version else: is_default = False if is_default and default_toolchain != None: - _fail_multiple_default_toolchains( + _fail_multiple_default_toolchains_chosen( first = default_toolchain.name, second = toolchain_name, ) @@ -199,7 +140,7 @@ def parse_modules(*, module_ctx, logger, _fail = fail): first = first, second_toolchain_name = toolchain_name, second_module_name = mod.name, - logger = logger, + logger = logger or repo_utils.logger(module_ctx, "python", mod = mod), ) toolchain_info = None else: @@ -212,7 +153,6 @@ def parse_modules(*, module_ctx, logger, _fail = fail): global_toolchain_versions[toolchain_version] = toolchain_info if debug_info: debug_info["toolchains_registered"].append({ - "ignore_root_user_error": ignore_root_user_error, "module": {"is_root": mod.is_root, "name": mod.name}, "name": toolchain_name, }) @@ -231,8 +171,6 @@ def parse_modules(*, module_ctx, logger, _fail = fail): elif toolchain_info: toolchains.append(toolchain_info) - config.default.setdefault("ignore_root_user_error", ignore_root_user_error) - # A default toolchain is required so that the non-version-specific rules # are able to match a toolchain. if default_toolchain == None: @@ -278,8 +216,10 @@ def parse_modules(*, module_ctx, logger, _fail = fail): ) def _python_impl(module_ctx): - logger = repo_utils.logger(module_ctx, "python") - py = parse_modules(module_ctx = module_ctx, logger = logger) + py = parse_modules(module_ctx = module_ctx) + + # For all other processing (after parsing the modules) let's use a single logger. + logger = repo_utils.logger(module_ctx, "python", mod = module_ctx.modules[0]) # Host compatible runtime repos # dict[str version, struct] where struct has: @@ -314,7 +254,18 @@ def _python_impl(module_ctx): full_python_version = full_version( version = toolchain_info.python_version, minor_mapping = py.config.minor_mapping, + fail_on_err = False, ) + if not full_python_version: + logger.info(lambda: ( + "The actual toolchain for python_version '{version}' " + + "has not been registered, but was requested, please configure a toolchain " + + "to be actually downloaded and setup" + ).format( + version = toolchain_info.python_version, + )) + continue + kwargs = { "python_version": full_python_version, "register_coverage_tool": toolchain_info.register_coverage_tool, @@ -327,6 +278,7 @@ def _python_impl(module_ctx): register_result = python_register_toolchains( name = toolchain_info.name, _internal_bzlmod_toolchain_call = True, + _internal_module_ctx = module_ctx, **kwargs ) if not register_result.impl_repos: @@ -457,6 +409,10 @@ def _python_impl(module_ctx): # the PLATFORMS global for this toolchain toolchain_platform_keys = {} + # Extra target_settings to add to every registered toolchain, e.g. for + # gating the default toolchains behind a custom config_setting. + global_add_target_settings = py.config.add_target_settings + # Split the toolchain info into separate objects so they can be passed onto # the repository rule. for entry in toolchain_impls: @@ -468,7 +424,7 @@ def _python_impl(module_ctx): # The target_settings attribute may not be present for users # patching python/versions.bzl. - toolchain_ts_map[key] = getattr(entry.platform, "target_settings", []) + toolchain_ts_map[key] = getattr(entry.platform, "target_settings", []) + global_add_target_settings toolchain_platform_keys[key] = entry.platform_name toolchain_python_versions[key] = entry.full_python_version @@ -578,14 +534,24 @@ def _fail_multiple_defaults_python_version_env(first, second): second = second, )) -def _fail_multiple_default_toolchains(first, second): +def _fail_multiple_default_toolchains_chosen(first, second): fail(("Multiple default toolchains: only one toolchain " + - "can have is_default=True. First default " + + "can be chosen as a default. First default " + "was toolchain '{first}'. Second was '{second}'").format( first = first, second = second, )) +def _fail_multiple_default_toolchains_in_module(mod, toolchain_attrs): + fail(("Multiple default toolchains: only one toolchain " + + "can have is_default=True.\n" + + "Module '{module}' contains {count} toolchains with " + + "is_default=True: {versions}").format( + module = mod.name, + count = len(toolchain_attrs), + versions = ", ".join(sorted([v.python_version for v in toolchain_attrs])), + )) + def _validate_version(version_str, *, _fail = fail): v = version.parse(version_str, strict = True, _fail = _fail) if v == None: @@ -746,9 +712,11 @@ def _process_global_overrides(*, tag, default, _fail = fail): default["minor_mapping"] = tag.minor_mapping + if tag.add_target_settings: + default["add_target_settings"] = list(tag.add_target_settings) + forwarded_attrs = sorted(AUTH_ATTRS) + [ - "ignore_root_user_error", - "base_url", + "base_urls", "register_all_versions", ] for key in forwarded_attrs: @@ -776,10 +744,100 @@ def _override_defaults(*overrides, modules, _fail = fail, default): override.fn(tag = tag, _fail = _fail, default = default) -def _get_toolchain_config(*, modules, _fail = fail): +def _manifest_entry_sort_key(entry): + flavor_rank = {"full": 3, "install_only": 1, "install_only_stripped": 2}.get(entry.archive_flavor, 4) + microarch = entry.microarch + if not microarch: + microarch_rank = 0 + elif microarch.startswith("v") and microarch[1:].isdigit(): + microarch_rank = int(microarch[1:]) + else: + microarch_rank = 999 + return (flavor_rank, microarch_rank) + +def _populate_from_pbs_manifest( + *, + mctx, + add_runtime_manifest_urls = [], + add_runtime_manifest_files = [], + runtime_manifest_sha = "", + base_urls = [], + available_versions, + _fail): + manifest_contents = [] + + if add_runtime_manifest_urls: + manifest_path = mctx.path("runtime_manifest") + result = mctx.download( + url = add_runtime_manifest_urls, + output = manifest_path, + sha256 = runtime_manifest_sha, + ) + if not result.success: + _fail("Failed to download manifest from {}: {}".format(add_runtime_manifest_urls, result)) + return + manifest_contents.append(mctx.read(manifest_path)) + + for manifest_file in add_runtime_manifest_files: + manifest_contents.append(mctx.read(manifest_file, watch = "yes")) + + if not manifest_contents: + return + + base_download_urls = [url.rpartition("/")[0] for url in add_runtime_manifest_urls] + if not base_download_urls and base_urls: + base_download_urls = list(base_urls) + + entries = [] + for content in manifest_contents: + entries.extend(parse_runtime_manifest(content)) + + # We don't model archive_flavor via flags yet, so have to pick one. + # Preference is given to install_only because its smaller + entries = sorted( + entries, + key = _manifest_entry_sort_key, + ) + + for entry in entries: + location = entry.location + sha256 = entry.sha256 + py_version = entry.python_version + + # Fallback to matching against PLATFORMS keys as before to ensure compatibility + # with rules_python expected platform keys. + matched_platform = "{}-{}-{}".format(entry.arch, entry.vendor, entry.os) + if entry.libc: + matched_platform += "-" + entry.libc + if entry.freethreaded: + matched_platform += "-freethreaded" + + if matched_platform not in PLATFORMS: + continue + + if entry.archive_flavor not in ["install_only", "install_only_stripped", "full"]: + continue + + v_dict = available_versions.setdefault(py_version, {}) + if matched_platform in v_dict.get("sha256", {}): + continue + + if "://" in location: + urls = [location] + else: + urls = ["{}/{}".format(b_url, location) for b_url in base_download_urls] + + strip_prefix = "python/install" if entry.archive_flavor == "full" else "python" + + v_dict.setdefault("sha256", {})[matched_platform] = sha256 + v_dict.setdefault("url", {})[matched_platform] = urls + v_dict.setdefault("strip_prefix", {})[matched_platform] = strip_prefix + +def _get_toolchain_config(*, mctx, modules, _fail = fail): """Computes the configs for toolchains. Args: + mctx: The module context. modules: The modules from module_ctx _fail: Function to call for failing; only used for testing. @@ -799,30 +857,46 @@ def _get_toolchain_config(*, modules, _fail = fail): # Items that can be overridden available_versions = {} - for py_version, item in TOOL_VERSIONS.items(): - available_versions[py_version] = {} - available_versions[py_version]["sha256"] = dict(item["sha256"]) - platforms = item["sha256"].keys() - - strip_prefix = item["strip_prefix"] - if type(strip_prefix) == type(""): - available_versions[py_version]["strip_prefix"] = { - platform: strip_prefix - for platform in platforms - } - else: - available_versions[py_version]["strip_prefix"] = dict(strip_prefix) - url = item["url"] - if type(url) == type(""): - available_versions[py_version]["url"] = { - platform: url - for platform in platforms - } - else: - available_versions[py_version]["url"] = dict(url) + _populate_from_pbs_manifest( + mctx = mctx, + add_runtime_manifest_files = [Label("//python/private:runtimes_manifest.txt")], + base_urls = DEFAULT_RELEASE_BASE_URLS, + available_versions = available_versions, + _fail = _fail, + ) + + # Check for add_runtime_manifest_urls or add_runtime_manifest_files in override tags in root module + root_module = modules[0] if modules else None + if root_module and root_module.is_root: + for tag in root_module.tags.override: + if tag.add_runtime_manifest_urls or tag.add_runtime_manifest_files: + _populate_from_pbs_manifest( + mctx = mctx, + add_runtime_manifest_urls = tag.add_runtime_manifest_urls, + add_runtime_manifest_files = tag.add_runtime_manifest_files, + runtime_manifest_sha = tag.runtime_manifest_sha, + base_urls = tag.base_urls, + available_versions = available_versions, + _fail = _fail, + ) + + # Check for add_runtime_manifest_urls or add_runtime_manifest_files in override tags in root module + root_module = modules[0] if modules else None + if root_module and root_module.is_root: + for tag in root_module.tags.override: + if tag.add_runtime_manifest_urls or tag.add_runtime_manifest_files: + _populate_from_pbs_manifest( + mctx = mctx, + add_runtime_manifest_urls = tag.add_runtime_manifest_urls, + add_runtime_manifest_files = tag.add_runtime_manifest_files, + runtime_manifest_sha = tag.runtime_manifest_sha, + base_urls = tag.base_urls, + available_versions = available_versions, + _fail = _fail, + ) default = { - "base_url": DEFAULT_RELEASE_BASE_URL, + "base_urls": DEFAULT_RELEASE_BASE_URLS, "platforms": dict(PLATFORMS), # Copy so it's mutable. "tool_versions": available_versions, } @@ -854,6 +928,7 @@ def _get_toolchain_config(*, modules, _fail = fail): ) register_all_versions = default.pop("register_all_versions", False) + add_target_settings = default.pop("add_target_settings", []) kwargs = default.pop("kwargs", {}) versions = {} @@ -879,8 +954,88 @@ def _get_toolchain_config(*, modules, _fail = fail): minor_mapping = minor_mapping, default = default, register_all_versions = register_all_versions, + add_target_settings = add_target_settings, ) +def _compute_default_python_version(mctx): + default_python_version = None + for mod in mctx.modules: + # Only the root module and rules_python are allowed to specify the default + # toolchain for a couple reasons: + # * It prevents submodules from specifying different defaults and only + # one of them winning. + # * rules_python needs to set a soft default in case the root module doesn't, + # e.g. if the root module doesn't use Python itself. + # * The root module is allowed to override the rules_python default. + if not (mod.is_root or mod.name == "rules_python"): + continue + + defaults_attr_structs = _create_defaults_attr_structs(mod = mod) + default_python_version_env = None + default_python_version_file = None + pyproject_toml_label = None + + for defaults_attr in defaults_attr_structs: + pyproject_toml_label = _one_or_the_same( + pyproject_toml_label, + defaults_attr.pyproject_toml, + onerror = lambda: fail("Multiple pyproject.toml files specified in defaults"), + ) + + default_python_version = _one_or_the_same( + default_python_version, + defaults_attr.python_version, + onerror = _fail_multiple_defaults_python_version, + ) + default_python_version_env = _one_or_the_same( + default_python_version_env, + defaults_attr.python_version_env, + onerror = _fail_multiple_defaults_python_version_env, + ) + default_python_version_file = _one_or_the_same( + default_python_version_file, + defaults_attr.python_version_file, + onerror = _fail_multiple_defaults_python_version_file, + ) + + # Priority order: ENV > pyproject_toml > python_version_file > python_version + if default_python_version_file: + default_python_version = _one_or_the_same( + default_python_version, + mctx.read(default_python_version_file, watch = "yes").strip(), + ) + if pyproject_toml_label: + pyproject = read_pyproject(mctx, pyproject_toml_label) + if pyproject.requires_python: + default_python_version = version_from_requires_python(pyproject.requires_python) + if default_python_version_env: + default_python_version = mctx.getenv( + default_python_version_env, + default_python_version, + ) + + if default_python_version: + break + + # Otherwise, look at legacy python.toolchain() calls for a default + toolchain_attrs = mod.tags.toolchain + + # Convenience: if one python.toolchain() call exists, treat it as + # the default. + if len(toolchain_attrs) == 1: + default_python_version = toolchain_attrs[0].python_version + else: + sets_default = [v for v in toolchain_attrs if v.is_default] + if len(sets_default) == 1: + default_python_version = sets_default[0].python_version + elif len(sets_default) > 1: + _fail_multiple_default_toolchains_in_module(mod, toolchain_attrs) + + if default_python_version: + break + + return default_python_version + def _create_defaults_attr_structs(*, mod): arg_structs = [] @@ -894,11 +1049,29 @@ def _create_defaults_attr_struct(*, tag): python_version = getattr(tag, "python_version", None), python_version_env = getattr(tag, "python_version_env", None), python_version_file = getattr(tag, "python_version_file", None), + pyproject_toml = getattr(tag, "pyproject_toml", None), ) -def _create_toolchain_attr_structs(*, mod, config, seen_versions): +def _create_toolchain_attr_structs(*, mod, config, seen_versions, default_python_version): arg_structs = [] + # Auto-register a toolchain for the default version if not already + # registered via an explicit python.toolchain() call. + # This works for any default source: pyproject_toml, python_version_file, + # python_version_env, or python_version. + has_explicit_toolchain = default_python_version and any([ + tag.python_version == default_python_version + for tag in mod.tags.toolchain + ]) + if (default_python_version and + default_python_version not in seen_versions and + mod.is_root and not has_explicit_toolchain): + arg_structs.append(_create_toolchain_attrs_struct( + python_version = default_python_version, + toolchain_tag_count = 1, + )) + seen_versions[default_python_version] = True + for tag in mod.tags.toolchain: arg_structs.append(_create_toolchain_attrs_struct( tag = tag, @@ -916,7 +1089,11 @@ def _create_toolchain_attr_structs(*, mod, config, seen_versions): return arg_structs -def _create_toolchain_attrs_struct(*, tag = None, python_version = None, toolchain_tag_count = None): +def _create_toolchain_attrs_struct( + *, + tag = None, + python_version = None, + toolchain_tag_count = None): if tag and python_version: fail("Only one of tag and python version can be specified") if tag: @@ -929,20 +1106,22 @@ def _create_toolchain_attrs_struct(*, tag = None, python_version = None, toolcha is_default = is_default, python_version = python_version if python_version else tag.python_version, configure_coverage_tool = getattr(tag, "configure_coverage_tool", False), - ignore_root_user_error = getattr(tag, "ignore_root_user_error", True), ) -def _get_bazel_version_specific_kwargs(): - kwargs = {} - - if IS_BAZEL_6_4_OR_HIGHER: - kwargs["environ"] = ["RULES_PYTHON_BZLMOD_DEBUG"] - - return kwargs - _defaults = tag_class( doc = """Tag class to specify the default Python version.""", attrs = { + "pyproject_toml": attr.label( + mandatory = False, + doc = """\ +Label pointing to pyproject.toml file to read the default Python version from. +When specified, reads the `requires-python` field from pyproject.toml. +The version must be specified as `==X.Y.Z` (exact version with full semver). + +:::{versionadded} VERSION_NEXT_FEATURE +::: +""", + ), "python_version": attr.string( mandatory = False, doc = """\ @@ -1049,16 +1228,9 @@ Then the python interpreter will be available as `my_python_name`. "ignore_root_user_error": attr.bool( default = True, doc = """\ -The Python runtime installation is made read only. This improves the ability for -Bazel to cache it by preventing the interpreter from creating `.pyc` files for -the standard library dynamically at runtime as they are loaded (this often leads -to spurious cache misses or build failures). - -However, if the user is running Bazel as root, this read-onlyness is not -respected. Bazel will print a warning message when it detects that the runtime -installation is writable despite being made read only (i.e. it's running with -root access) while this attribute is set `False`, however this messaging can be ignored by setting -this to `False`. +:::{versionchanged} 1.8.0 +Noop, will be removed in the next major release. +::: """, mandatory = False, ), @@ -1090,6 +1262,66 @@ _override = tag_class( ::: """, attrs = { + "add_runtime_manifest_files": attr.label_list( + mandatory = False, + allow_files = True, + doc = """ +Labels pointing to local python-build-standalone manifest files (e.g., `SHA256SUMS`). + +Example: +`//my/custom/manifest:SHA256SUMS` + +:::{seealso} +[Manifest file format documentation](https://rules-python.readthedocs.io/en/latest/toolchains.html#manifest-file-format) +::: + +:::{versionadded} 2.1.0 +::: +""", + ), + "add_runtime_manifest_urls": attr.string_list( + mandatory = False, + doc = """ +URLs pointing to python-build-standalone manifest files (e.g., SHA256SUMS). + +Example: +`https://github.com/astral-sh/python-build-standalone/releases/download/20260414/SHA256SUMS` + +Note that `/latest/` can be used in place of a specific release date (e.g., `20260414`) to automatically use the latest release: +`https://github.com/astral-sh/python-build-standalone/releases/latest/download/SHA256SUMS` + +:::{seealso} +[Manifest file format documentation](https://rules-python.readthedocs.io/en/latest/toolchains.html#manifest-file-format) +::: + +:::{versionadded} 2.1.0 +::: +""", + ), + "add_target_settings": attr.string_list( + mandatory = False, + doc = """\ +A list of `config_setting` labels to add to the `target_settings` of every +toolchain registered by this module extension. This is useful for creating +separate "families" of toolchains gated behind custom build settings. + +For example, to ensure the default prebuilt toolchains are only resolved when +a `prebuilt` config setting is active: + +```starlark +python.override( + add_target_settings = ["@@//:python_toolchain_family_prebuilt"], +) +``` + +These settings are appended to the `target_settings` of all toolchains +registered by the extension, including any that already have settings +from `python.single_version_platform_override`. + +:::{versionadded} 2.1.0 +::: +""", + ), "available_python_versions": attr.string_list( mandatory = False, doc = """\ @@ -1104,10 +1336,10 @@ This attribute is usually used in order to ensure that no unexpected transitive dependencies are introduced. """, ), - "base_url": attr.string( + "base_urls": attr.string_list( mandatory = False, - doc = "The base URL to be used when downloading toolchains.", - default = DEFAULT_RELEASE_BASE_URL, + doc = "The base URLs to be used when downloading toolchains.", + default = DEFAULT_RELEASE_BASE_URLS, ), "ignore_root_user_error": attr.bool( default = True, @@ -1135,6 +1367,15 @@ The values in this mapping override the default values and do not replace them. default = {}, ), "register_all_versions": attr.bool(default = False, doc = "Add all versions"), + "runtime_manifest_sha": attr.string( + mandatory = False, + doc = """ +SHA256 hash for the add_runtime_manifest_urls. + +:::{versionadded} 2.1.0 +::: +""", + ), } | AUTH_ATTRS, ) @@ -1240,7 +1481,7 @@ The values should be one of the values in `@platforms//cpu` Docs for [Registering custom runtimes] ::: -:::{{versionadded}} 1.5.0 +:::{versionadded} 1.5.0 ::: """, ), @@ -1265,7 +1506,7 @@ The values should be one of the values in `@platforms//os` Docs for [Registering custom runtimes] ::: -:::{{versionadded}} 1.5.0 +:::{versionadded} 1.5.0 ::: """, ), @@ -1320,7 +1561,7 @@ If set, `target_settings`, `os_name`, and `arch` should also be set. Docs for [Registering custom runtimes] ::: -:::{{versionadded}} 1.5.0 +:::{versionadded} 1.5.0 ::: """, ), @@ -1334,7 +1575,7 @@ If set, `target_compatible_with`, `os_name`, and `arch` should also be set. Docs for [Registering custom runtimes] ::: -:::{{versionadded}} 1.5.0 +:::{versionadded} 1.5.0 ::: """, ), @@ -1356,7 +1597,7 @@ python = module_extension( "single_version_platform_override": _single_version_platform_override, "toolchain": _toolchain, }, - **_get_bazel_version_specific_kwargs() + environ = ["RULES_PYTHON_BZLMOD_DEBUG"], ) _DEBUG_BUILD_CONTENT = """ diff --git a/python/private/python_bootstrap_template.txt b/python/private/python_bootstrap_template.txt index 9717756036..482918c038 100644 --- a/python/private/python_bootstrap_template.txt +++ b/python/private/python_bootstrap_template.txt @@ -7,20 +7,62 @@ from __future__ import print_function import sys +# By default, Python prepends the directory containing this script to +# sys.path. The stage 1 bootstrap only needs stdlib modules before it +# re-execs into the configured runtime, so avoid resolving imports from the +# target's output or runfiles package directory. This matters when that +# directory contains files with stdlib names, such as shutil.py or types.py. +# +# Python 3.11 introduced PYTHONSAFEPATH (-P), which disables the unsafe prepend. +# Isolated mode (-I) also disables it, including on older interpreters without +# safe_path. In either case, sys.path[0] is not the script directory and should +# be preserved. +if ( + not getattr(sys.flags, "safe_path", False) and + not getattr(sys.flags, "isolated", False) and + sys.path +): + del sys.path[0] + +# Generated file from @rules_python//python/private:python_bootstrap_template.txt + +from os.path import abspath, dirname, join, basename, normpath import os +import shutil import subprocess -import uuid -# runfiles-relative path +# NOTE: The sentinel strings are split (e.g., "%stage2" + "_bootstrap%") so that +# the substitution logic won't replace them. This allows runtime detection of +# unsubstituted placeholders, which occurs when native py_binary is used in +# external repositories. In that case, we fall back to %main% which Bazel's +# native rule does substitute. +_STAGE2_BOOTSTRAP_SENTINEL = "%stage2" + "_bootstrap%" +# runfiles-root-relative path STAGE2_BOOTSTRAP="%stage2_bootstrap%" -# runfiles-relative path to venv's python interpreter +# NOTE: The fallback logic from stage2_bootstrap to main is only present +# as a courtesy for an older, unsupported, configuration. It can be removed +# when that case is unlikely to be a concern anymore. +# See https://github.com/bazel-contrib/rules_python/pull/3495 +if STAGE2_BOOTSTRAP == _STAGE2_BOOTSTRAP_SENTINEL: + _MAIN_SENTINEL = "%main" + "%" + _main = "%main%" + if _main != _MAIN_SENTINEL and _main: + STAGE2_BOOTSTRAP = _main + else: + STAGE2_BOOTSTRAP = "" + +if not STAGE2_BOOTSTRAP: + print("ERROR: %stage2_bootstrap% (or %main%) was not substituted.", file=sys.stderr) + sys.exit(1) + +# runfiles-root-relative path to venv's python interpreter # Empty string if a venv is not setup. PYTHON_BINARY = '%python_binary%' # The path to the actual interpreter that is used. # Typically PYTHON_BINARY is a symlink pointing to this. -# runfiles-relative path, absolute path, or single word. +# runfiles-root-relative path, absolute path, or single word. # Used to create a venv at runtime, or when a venv isn't setup. PYTHON_BINARY_ACTUAL = "%python_binary_actual%" @@ -30,32 +72,64 @@ IS_ZIPFILE = "%is_zipfile%" == "1" # 0 or 1. # If 1, then a venv will be created at runtime that replicates what would have # been the build-time structure. -RECREATE_VENV_AT_RUNTIME="%recreate_venv_at_runtime%" +RECREATE_VENV_AT_RUNTIME = "%recreate_venv_at_runtime%" == "1" +# 0 or 1 +# If 1, then the path to python will be resolved by running +# PYTHON_BINARY_ACTUAL to determine the actual underlying interpreter. +RESOLVE_PYTHON_BINARY_AT_RUNTIME = "%resolve_python_binary_at_runtime%" == "1" +# venv-relative path to the site-packages +# e.g. lib/python3.12t/site-packages +VENV_REL_SITE_PACKAGES = "%venv_rel_site_packages%" WORKSPACE_NAME = "%workspace_name%" # Target-specific interpreter args. -INTERPRETER_ARGS = [ -%interpreter_args% -] +# Sentinel split to detect unsubstituted placeholder (see STAGE2_BOOTSTRAP above). +_INTERPRETER_ARGS_SENTINEL = "%interpreter" + "_args%" +_INTERPRETER_ARGS_RAW = "%interpreter_args%" +if _INTERPRETER_ARGS_RAW == _INTERPRETER_ARGS_SENTINEL: + INTERPRETER_ARGS = [] +else: + INTERPRETER_ARGS = [arg for arg in _INTERPRETER_ARGS_RAW.split("\n") if arg] -ADDITIONAL_INTERPRETER_ARGS = os.environ.get("RULES_PYTHON_ADDITIONAL_INTERPRETER_ARGS", "") +# Symlinks to create at runtime in the venv. +# Line delimited entries +# Each entry is "venv_relative_path|path_to_symlink_to" +RUNTIME_VENV_SYMLINKS = """ +%runtime_venv_symlinks% +""".strip().split("\n") +RUNTIME_VENV_SYMLINKS = dict(line.split("|") for line in RUNTIME_VENV_SYMLINKS if line) -def IsRunningFromZip(): - return IS_ZIPFILE +ADDITIONAL_INTERPRETER_ARGS = os.environ.get("RULES_PYTHON_ADDITIONAL_INTERPRETER_ARGS", "") +EXTRACT_ROOT = os.environ.get("RULES_PYTHON_EXTRACT_ROOT") -if IsRunningFromZip(): +if IS_ZIPFILE: import shutil import tempfile import zipfile else: import re -# Return True if running on Windows -def IsWindows(): - return os.name == 'nt' +IS_WINDOWS = os.name == "nt" + +BIN_DIR_NAME = "bin" if not IS_WINDOWS else "Scripts" +LIB_DIR_NAME = "lib" if not IS_WINDOWS else "Lib" + +# Windows APIs can be picky about slashes depending on the context, +# so convert to backslashes to avoid any issues. +if IS_WINDOWS: + def norm_slashes(s): + return s.replace("/", "\\") + + STAGE2_BOOTSTRAP = norm_slashes(STAGE2_BOOTSTRAP) + PYTHON_BINARY = norm_slashes(PYTHON_BINARY) + PYTHON_BINARY_ACTUAL = norm_slashes(PYTHON_BINARY_ACTUAL) + RUNTIME_VENV_SYMLINKS = { + norm_slashes(k): norm_slashes(v) + for k, v in RUNTIME_VENV_SYMLINKS.items() + } -def GetWindowsPathWithUNCPrefix(path): +def get_windows_path_with_unc_prefix(path): """Adds UNC prefix after getting a normalized absolute Windows path. No-op for non-Windows platforms or if running under python2. @@ -64,7 +138,7 @@ def GetWindowsPathWithUNCPrefix(path): # No need to add prefix for non-Windows platforms. # And \\?\ doesn't work in python 2 or on mingw - if not IsWindows() or sys.version_info[0] < 3: + if not IS_WINDOWS or sys.version_info[0] < 3: return path # Starting in Windows 10, version 1607(OS build 14393), MAX_PATH limitations have been @@ -92,55 +166,51 @@ def GetWindowsPathWithUNCPrefix(path): if path.startswith(unicode_prefix): return path - # os.path.abspath returns a normalized absolute path - return unicode_prefix + os.path.abspath(path) + # abspath returns a normalized absolute path + return unicode_prefix + abspath(path) -def HasWindowsExecutableExtension(path): - return path.endswith('.exe') or path.endswith('.com') or path.endswith('.bat') - -if PYTHON_BINARY and IsWindows() and not HasWindowsExecutableExtension(PYTHON_BINARY): - PYTHON_BINARY = PYTHON_BINARY + '.exe' - -def SearchPath(name): +def search_path(name): """Finds a file in a given search path.""" search_path = os.getenv('PATH', os.defpath).split(os.pathsep) for directory in search_path: if directory: - path = os.path.join(directory, name) + path = join(directory, name) if os.path.isfile(path) and os.access(path, os.X_OK): return path return None -def FindPythonBinary(module_space): +def find_python_binary(runfiles_root): """Finds the real Python binary if it's not a normal absolute path.""" if PYTHON_BINARY: - return FindBinary(module_space, PYTHON_BINARY) + return find_binary(runfiles_root, PYTHON_BINARY) else: - return FindBinary(module_space, PYTHON_BINARY_ACTUAL) + return find_binary(runfiles_root, PYTHON_BINARY_ACTUAL) def print_verbose(*args, mapping=None, values=None): - if os.environ.get("RULES_PYTHON_BOOTSTRAP_VERBOSE"): - if mapping is not None: - for key, value in sorted((mapping or {}).items()): - print( - "bootstrap: stage 1: ", - *(list(args) + ["{}={}".format(key, repr(value))]), - file=sys.stderr, - flush=True - ) - elif values is not None: - for i, v in enumerate(values): - print( - "bootstrap: stage 1:", - *(list(args) + ["[{}] {}".format(i, repr(v))]), - file=sys.stderr, - flush=True - ) - else: - print("bootstrap: stage 1:", *args, file=sys.stderr, flush=True) - -def FindBinary(module_space, bin_name): + if not os.environ.get("RULES_PYTHON_BOOTSTRAP_VERBOSE"): + return + + if mapping is not None: + for key, value in sorted((mapping or {}).items()): + print( + "bootstrap: stage 1:", + *(list(args) + ["{}={}".format(key, repr(value))]), + file=sys.stderr, + flush=True + ) + elif values is not None: + for i, v in enumerate(values): + print( + "bootstrap: stage 1:", + *(list(args) + ["[{}] {}".format(i, repr(v))]), + file=sys.stderr, + flush=True + ) + else: + print("bootstrap: stage 1:", *args, file=sys.stderr, flush=True) + +def find_binary(runfiles_root, bin_name): """Finds the real binary if it's not a normal absolute path.""" if not bin_name: return None @@ -155,12 +225,12 @@ def FindBinary(module_space, bin_name): # Use normpath() to convert slashes to os.sep on Windows. elif os.sep in os.path.normpath(bin_name): # Case 3: Path is relative to the repo root. - return os.path.join(module_space, bin_name) + return join(runfiles_root, bin_name) else: # Case 4: Path has to be looked up in the search path. - return SearchPath(bin_name) + return search_path(bin_name) -def FindModuleSpace(main_rel_path): +def find_runfiles_root(main_rel_path): """Finds the runfiles tree.""" # When the calling process used the runfiles manifest to resolve the # location of this stub script, the path may be expanded. This means @@ -175,25 +245,32 @@ def FindModuleSpace(main_rel_path): runfiles_dir = runfiles_manifest_file[:-9] # Be defensive: the runfiles dir should contain our main entry point. If # it doesn't, then it must not be our runfiles directory. - if runfiles_dir and os.path.exists(os.path.join(runfiles_dir, main_rel_path)): + if runfiles_dir and os.path.exists(join(runfiles_dir, main_rel_path)): return runfiles_dir + # Clear RUNFILES_DIR & RUNFILES_MANIFEST_FILE since the runfiles dir was + # not found. These can be correctly set for a parent Python process, but + # inherited by the child, and not correct for it. Later bootstrap code + # assumes they're are correct if set. + os.environ.pop('RUNFILES_DIR', None) + os.environ.pop('RUNFILES_MANIFEST_FILE', None) + stub_filename = sys.argv[0] # On Windows, the path may contain both forward and backslashes. # Normalize to the OS separator because the regex used later assumes # the OS-specific separator. - if IsWindows: + if IS_WINDOWS: stub_filename = stub_filename.replace("/", os.sep) if not os.path.isabs(stub_filename): - stub_filename = os.path.join(os.getcwd(), stub_filename) + stub_filename = join(os.getcwd(), stub_filename) while True: - module_space = stub_filename + ('.exe' if IsWindows() else '') + '.runfiles' - if os.path.isdir(module_space): - return module_space + runfiles_root = stub_filename + ('.exe' if IS_WINDOWS else '') + '.runfiles' + if os.path.isdir(runfiles_root): + return runfiles_root - runfiles_pattern = r'(.*\.runfiles)' + (r'\\' if IsWindows() else '/') + '.*' + runfiles_pattern = r'(.*\.runfiles)' + (r'\\' if IS_WINDOWS else '/') + '.*' matchobj = re.match(runfiles_pattern, stub_filename) if matchobj: return matchobj.group(1) @@ -204,11 +281,11 @@ def FindModuleSpace(main_rel_path): if os.path.isabs(target): stub_filename = target else: - stub_filename = os.path.join(os.path.dirname(stub_filename), target) + stub_filename = join(os.path.dirname(stub_filename), target) raise AssertionError('Cannot find .runfiles directory for %s' % sys.argv[0]) -def ExtractZip(zip_path, dest_dir): +def extract_zip(zip_path, dest_dir): """Extracts the contents of a zip file, preserving the unix file mode bits. These include the permission bits, and in particular, the executable bit. @@ -220,30 +297,156 @@ def ExtractZip(zip_path, dest_dir): zip_path: The path to the zip file to extract dest_dir: The path to the destination directory """ - zip_path = GetWindowsPathWithUNCPrefix(zip_path) - dest_dir = GetWindowsPathWithUNCPrefix(dest_dir) + zip_path = get_windows_path_with_unc_prefix(zip_path) + dest_dir = get_windows_path_with_unc_prefix(dest_dir) with zipfile.ZipFile(zip_path) as zf: for info in zf.infolist(): zf.extract(info, dest_dir) # UNC-prefixed paths must be absolute/normalized. See # https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file#maximum-path-length-limitation - file_path = os.path.abspath(os.path.join(dest_dir, info.filename)) + file_path = abspath(join(dest_dir, info.filename)) # The Unix st_mode bits (see "man 7 inode") are stored in the upper 16 - # bits of external_attr. Of those, we set the lower 12 bits, which are the - # file mode bits (since the file type bits can't be set by chmod anyway). + # bits of external_attr. attrs = info.external_attr >> 16 - if attrs != 0: # Rumor has it these can be 0 for zips created on Windows. + # Symlink bit in st_mode is 0o120000. + if (attrs & 0o170000) == 0o120000: + with open(file_path, "r") as f: + target = f.read() + os.remove(file_path) + os.symlink(target, file_path) + # Of those, we set the lower 12 bits, which are the + # file mode bits (since the file type bits can't be set by chmod anyway). + elif attrs != 0: # Rumor has it these can be 0 for zips created on Windows. os.chmod(file_path, attrs & 0o7777) # Create the runfiles tree by extracting the zip file -def CreateModuleSpace(): +def create_runfiles_root(): temp_dir = tempfile.mkdtemp('', 'Bazel.runfiles_') - ExtractZip(os.path.dirname(__file__), temp_dir) - # IMPORTANT: Later code does `rm -fr` on dirname(module_space) -- it's + extract_zip(os.path.dirname(__file__), temp_dir) + # IMPORTANT: Later code does `rm -fr` on dirname(runfiles_root) -- it's # important that deletion code be in sync with this directory structure - return os.path.join(temp_dir, 'runfiles') + return join(temp_dir, 'runfiles') + +def _symlink_tree(link_from, link_to): + # Ensure the source is an absolute path to simplify symlinking + link_to_root = abspath(link_to) + link_from_root = abspath(link_from) + + # This is non-optimal because it recreates the entire + # venv site-packages tree (as opposed to finding a highest-common + # directory to symlink). But its easy and understandable. + for root, dirs, files in os.walk(link_to_root): + rel_path = os.path.relpath(root, link_to_root) + link_from_dir = join(link_from_root, rel_path) + + if not os.path.exists(link_from_dir): + os.makedirs(link_from_dir) + + for name in files: + link_to = join(root, name) + link_from = join(link_from_dir, name) + _symlink_exist_ok(from_=link_from, to=link_to) + +def _create_venv(runfiles_root, delete_dirs): + rel_runfiles_venv = dirname(dirname(PYTHON_BINARY)) + runfiles_venv = join(runfiles_root, rel_runfiles_venv) + print_verbose("create_venv: runfiles venv:", runfiles_venv) + if EXTRACT_ROOT: + venv = join(EXTRACT_ROOT, rel_runfiles_venv) + os.makedirs(venv, exist_ok=True) + else: + import tempfile + venv = tempfile.mkdtemp("", f"bazel.{basename(runfiles_venv)}.") + delete_dirs.append(venv) + + print_verbose("create_venv: created venv:", venv) + + python_exe_actual = find_binary(runfiles_root, PYTHON_BINARY_ACTUAL) + if python_exe_actual is None: + raise AssertionError('Could not find python binary: ' + repr(PYTHON_BINARY_ACTUAL)) + + # See stage1_bootstrap_template.sh for details on this code path. In short, + # this handles when the build-time python version doesn't match runtime + # and if the initially resolved python_exe_actual is a wrapper script. + if RESOLVE_PYTHON_BINARY_AT_RUNTIME: + venv_src = venv.replace("\\", "\\\\") # Escape backslashes + src = f""" +import sys, site +print(sys.executable) +print(sys.base_prefix) +print(site.getsitepackages(["{venv_src}"])[-1]) + """ + print_verbose("prog:", src) + output = subprocess.check_output([python_exe_actual, "-I"], shell=True, + encoding = "utf8", input=src) + output = output.strip().split("\n") + python_exe_actual = output[0] + python_home = output[1] if IS_WINDOWS else None + venv_lib = output[2] + os.makedirs(dirname(venv_lib), exist_ok=True) + runfiles_venv_lib = join(runfiles_venv, VENV_REL_SITE_PACKAGES) + else: + # On unixy, Python can find home based on the symlink. + python_home = dirname(python_exe_actual) if IS_WINDOWS else None + venv_lib = join(venv, LIB_DIR_NAME) + runfiles_venv_lib = join(runfiles_venv, LIB_DIR_NAME) -def RunfilesEnvvar(module_space): + venv_bin = join(venv, BIN_DIR_NAME) + try: + os.mkdir(venv_bin) + except FileExistsError as e: + pass + # Match the basename; some tools, e.g. pyvenv key off the executable name + venv_python_exe = join(venv_bin, os.path.basename(python_exe_actual)) + _symlink_exist_ok(from_=venv_python_exe, to=python_exe_actual) + + # Windows requires supporting .dll files in the venv bin dir, but + # they aren't known in advance when the interpreter is resolved at runtime. + if RESOLVE_PYTHON_BINARY_AT_RUNTIME and IS_WINDOWS: + files = os.listdir(python_home) + for f in files: + if not f.endswith((".dll", ".pdb")): continue + venv_path = join(venv, BIN_DIR_NAME, f) + target = join(python_home, f) + _symlink_exist_ok(from_=venv_path, to=target) + + runfiles_venv_bin = join(runfiles_venv, BIN_DIR_NAME) + # The runfiles bin directory may not exist if it's empty, e.g. + # supports_build_time_venv=False and the interpreter is resolved at + # runtime. + if os.path.exists(runfiles_venv_bin): + # Add any missing build-time entries under bin/. Do this before + # the manual symlink creation to better mimic what the + # regular runfiles directory looks like. + for f_basename in os.listdir(runfiles_venv_bin): + venv_path = join(venv, BIN_DIR_NAME, f_basename) + target = join(runfiles_venv_bin, f_basename) + _symlink_exist_ok(from_=venv_path, to=target) + + # Recreate correct relative symlinks + for venv_rel_path, link_to_rf_path in RUNTIME_VENV_SYMLINKS.items(): + venv_abs_path = join(venv, venv_rel_path) + link_to = normpath(join(runfiles_root, link_to_rf_path)) + os.makedirs(dirname(venv_abs_path), exist_ok=True) + _symlink_exist_ok(from_=venv_abs_path, to=link_to) + + # Do this last to handle non-relative symlinks artifacts + if os.path.exists(runfiles_venv_lib): + _symlink_tree(link_from=venv_lib, link_to=runfiles_venv_lib) + + if IS_WINDOWS: + print_verbose("create_venv: pyvenv.cfg home: ", python_home) + venv_pyvenv_cfg = join(venv, "pyvenv.cfg") + with open(venv_pyvenv_cfg, "w") as fp: + # Until Windows supports a build-time generated venv using symlinks + # to directories, we have to write the full, absolute, path to PYTHONHOME + # so that support directories (e.g. DLLs, libs) can be found. + fp.write("home = {}\n".format(python_home)) + else: + _symlink_exist_ok(from_=join(venv, "pyvenv.cfg"), to=join(runfiles_venv, "pyvenv.cfg")) + return venv_python_exe + +def runfiles_envvar(runfiles_root): """Finds the runfiles manifest or the runfiles directory. Returns: @@ -262,11 +465,11 @@ def RunfilesEnvvar(module_space): return ('RUNFILES_DIR', runfiles) # If running from a zip, there's no manifest file. - if IsRunningFromZip(): - return ('RUNFILES_DIR', module_space) + if IS_ZIPFILE: + return ('RUNFILES_DIR', runfiles_root) # Look for the runfiles "output" manifest, argv[0] + ".runfiles_manifest" - runfiles = module_space + '_manifest' + runfiles = runfiles_root + '_manifest' if os.path.exists(runfiles): return ('RUNFILES_MANIFEST_FILE', runfiles) @@ -274,19 +477,19 @@ def RunfilesEnvvar(module_space): # Normally .runfiles_manifest and MANIFEST are both present, but the # former will be missing for zip-based builds or if someone copies the # runfiles tree elsewhere. - runfiles = os.path.join(module_space, 'MANIFEST') + runfiles = join(runfiles_root, 'MANIFEST') if os.path.exists(runfiles): return ('RUNFILES_MANIFEST_FILE', runfiles) # If running in a sandbox and no environment variables are set, then # Look for the runfiles next to the binary. - if module_space.endswith('.runfiles') and os.path.isdir(module_space): - return ('RUNFILES_DIR', module_space) + if runfiles_root.endswith('.runfiles') and os.path.isdir(runfiles_root): + return ('RUNFILES_DIR', runfiles_root) return (None, None) -def ExecuteFile(python_program, main_filename, args, env, module_space, - workspace, delete_module_space): +def execute_file(python_program, main_filename, args, env, runfiles_root, + workspace, delete_dirs): # type: (str, str, list[str], dict[str, str], str, str|None, str|None) -> ... """Executes the given Python file using the various environment settings. @@ -298,11 +501,11 @@ def ExecuteFile(python_program, main_filename, args, env, module_space, main_filename: (str) The Python file to execute args: (list[str]) Additional args to pass to the Python file env: (dict[str, str]) A dict of environment variables to set for the execution - module_space: (str) Path to the module space/runfiles tree directory + runfiles_root: (str) Path to the runfiles root directory workspace: (str|None) Name of the workspace to execute in. This is expected to be a directory under the runfiles tree. - delete_module_space: (bool), True if the module space should be deleted - after a successful (exit code zero) program run, False if not. + delete_dirs: (list[str]) directories that should be deleted after the user + program has finished running. """ argv = [python_program] argv.extend(INTERPRETER_ARGS) @@ -326,36 +529,66 @@ def ExecuteFile(python_program, main_filename, args, env, module_space, # can't execv because we need control to return here. This only # happens for targets built in the host config. # - if not (IsWindows() or workspace or delete_module_space): - _RunExecv(python_program, argv, env) + if not (IS_WINDOWS or workspace or delete_dirs): + _run_execv(python_program, argv, env) + print_verbose("run: subproc: environ:", mapping=os.environ) + print_verbose("run: subproc: cwd:", workspace) + print_verbose("run: subproc: argv:", values=argv) ret_code = subprocess.call( - argv, - env=env, - cwd=workspace - ) + argv, env=env, cwd=workspace) + print_verbose("run: subproc: exit code:", ret_code) - if delete_module_space: - # NOTE: dirname() is called because CreateModuleSpace() creates a - # sub-directory within a temporary directory, and we want to remove the - # whole temporary directory. - shutil.rmtree(os.path.dirname(module_space), True) + if delete_dirs: + for delete_dir in delete_dirs: + print_verbose("cleanup: rmtree:", delete_dir) + shutil.rmtree(delete_dir, True) sys.exit(ret_code) -def _RunExecv(python_program, argv, env): +def _run_execv(python_program, argv, env): # type: (str, list[str], dict[str, str]) -> ... """Executes the given Python file using the various environment settings.""" os.environ.update(env) print_verbose("RunExecv: environ:", mapping=os.environ) print_verbose("RunExecv: python:", python_program) print_verbose("RunExecv: argv:", values=argv) - os.execv(python_program, argv) + try: + os.execv(python_program, argv) + except: + with open(python_program, 'rb') as f: + print_verbose("pyprog head:" + str(f.read(50))) + raise + +def _symlink_exist_ok(*, from_, to): + try: + # On Windows, symlinks have to be told whether they're + # pointing to a file or directory. Python is supposed to auto-detect + # this, but in practice, this doesn't reliably happen. + os.symlink(to, from_, target_is_directory=os.path.isdir(to)) + except FileExistsError: + pass + -def Main(): +def main(): + print_verbose("sys.version:", sys.version) + print_verbose("sys.executable:", sys.executable) print_verbose("initial argv:", values=sys.argv) print_verbose("initial cwd:", os.getcwd()) print_verbose("initial environ:", mapping=os.environ) print_verbose("initial sys.path:", values=sys.path) + print_verbose("IS_ZIPFILE:", IS_ZIPFILE) + print_verbose("PYTHON_BINARY:", PYTHON_BINARY) + print_verbose("PYTHON_BINARY_ACTUAL:", PYTHON_BINARY_ACTUAL) + print_verbose("RECREATE_VENV_AT_RUNTIME:", RECREATE_VENV_AT_RUNTIME) + print_verbose("RESOLVE_PYTHON_BINARY_AT_RUNTIME:", RESOLVE_PYTHON_BINARY_AT_RUNTIME) + print_verbose("RUNTIME_VENV_SYMLINKS size:", len(RUNTIME_VENV_SYMLINKS)) + print_verbose("STAGE2_BOOTSTRAP:", STAGE2_BOOTSTRAP) + print_verbose("VENV_REL_SITE_PACKAGES:", VENV_REL_SITE_PACKAGES) + print_verbose("WORKSPACE_NAME:", WORKSPACE_NAME ) + print_verbose("bootstrap sys.executable:", sys.executable) + print_verbose("bootstrap sys._base_executable:", getattr(sys, "_base_executable", "unknown")) + print_verbose("bootstrap sys.version:", sys.version) + args = sys.argv[1:] new_env = {} @@ -366,18 +599,25 @@ def Main(): # matters if `_main` doesn't exist (which can occur if a binary # is packaged and needs no artifacts from the main repo) main_rel_path = os.path.normpath(STAGE2_BOOTSTRAP) + print_verbose("main_rel_path:", main_rel_path) - if IsRunningFromZip(): - module_space = CreateModuleSpace() - delete_module_space = True + delete_dirs = [] + + if IS_ZIPFILE: + runfiles_root = create_runfiles_root() + # NOTE: dirname() is called because create_runfiles_root() creates a + # sub-directory within a temporary directory, and we want to remove the + # whole temporary directory. + delete_dirs.append(dirname(runfiles_root)) else: - module_space = FindModuleSpace(main_rel_path) - delete_module_space = False + runfiles_root = find_runfiles_root(main_rel_path) + + print_verbose("runfiles root:", runfiles_root) - if os.environ.get("RULES_PYTHON_TESTING_TELL_MODULE_SPACE"): - new_env["RULES_PYTHON_TESTING_MODULE_SPACE"] = module_space + if os.environ.get("RULES_PYTHON_TESTING_TELL_RUNFILES_ROOT"): + new_env["RULES_PYTHON_TESTING_RUNFILES_ROOT"] = runfiles_root - runfiles_envkey, runfiles_envvalue = RunfilesEnvvar(module_space) + runfiles_envkey, runfiles_envvalue = runfiles_envvar(runfiles_root) if runfiles_envkey: new_env[runfiles_envkey] = runfiles_envvalue @@ -385,16 +625,26 @@ def Main(): # See: https://docs.python.org/3.11/using/cmdline.html#envvar-PYTHONSAFEPATH new_env['PYTHONSAFEPATH'] = '1' - main_filename = os.path.join(module_space, main_rel_path) - main_filename = GetWindowsPathWithUNCPrefix(main_filename) + main_filename = join(runfiles_root, main_rel_path) + main_filename = get_windows_path_with_unc_prefix(main_filename) assert os.path.exists(main_filename), \ 'Cannot exec() %r: file not found.' % main_filename assert os.access(main_filename, os.R_OK), \ 'Cannot exec() %r: file not readable.' % main_filename - program = python_program = FindPythonBinary(module_space) - if python_program is None: - raise AssertionError('Could not find python binary: ' + repr(PYTHON_BINARY)) + if RECREATE_VENV_AT_RUNTIME: + # When the venv is created at runtime, python_program is PYTHON_BINARY_ACTUAL + # so we have to re-point it to the symlink in the venv + python_program = _create_venv(runfiles_root, delete_dirs) + else: + python_program = find_python_binary(runfiles_root) + if python_program is None: + raise AssertionError("Could not find python binary: {} or {}".format( + repr(PYTHON_BINARY), + repr(PYTHON_BINARY_ACTUAL) + )) + + python_program = get_windows_path_with_unc_prefix(python_program) # Some older Python versions on macOS (namely Python 3.7) may unintentionally # leave this environment variable set after starting the interpreter, which @@ -406,20 +656,20 @@ def Main(): new_env.update((key, val) for key, val in os.environ.items() if key not in new_env) workspace = None - if IsRunningFromZip(): + if IS_ZIPFILE: # If RUN_UNDER_RUNFILES equals 1, it means we need to # change directory to the right runfiles directory. # (So that the data files are accessible) if os.environ.get('RUN_UNDER_RUNFILES') == '1': - workspace = os.path.join(module_space, WORKSPACE_NAME) + workspace = join(runfiles_root, WORKSPACE_NAME) try: sys.stdout.flush() - # NOTE: ExecuteFile may call execve() and lines after this will never run. - ExecuteFile( - python_program, main_filename, args, new_env, module_space, + # NOTE: execute_file may call execve() and lines after this will never run. + execute_file( + python_program, main_filename, args, new_env, runfiles_root, workspace, - delete_module_space = delete_module_space, + delete_dirs = delete_dirs, ) except EnvironmentError: @@ -427,8 +677,8 @@ def Main(): e = sys.exc_info()[1] # This exception occurs when os.execv() fails for some reason. if not getattr(e, 'filename', None): - e.filename = program # Add info to error message + e.filename = python_program # Add info to error message raise if __name__ == '__main__': - Main() + main() diff --git a/python/private/python_register_toolchains.bzl b/python/private/python_register_toolchains.bzl index 2e0748deb0..5a2f96857b 100644 --- a/python/private/python_register_toolchains.bzl +++ b/python/private/python_register_toolchains.bzl @@ -17,7 +17,7 @@ load( "//python:versions.bzl", - "DEFAULT_RELEASE_BASE_URL", + "DEFAULT_RELEASE_BASE_URLS", "MINOR_MAPPING", "PLATFORMS", "TOOL_VERSIONS", @@ -26,6 +26,7 @@ load( load(":coverage_deps.bzl", "coverage_dep") load(":full_version.bzl", "full_version") load(":python_repository.bzl", "python_repository") +load(":repo_utils.bzl", "repo_utils") load( ":toolchains_repo.bzl", "host_compatible_python_repo", @@ -89,7 +90,20 @@ def python_register_toolchains( if bzlmod_toolchain_call: register_toolchains = False - base_url = kwargs.pop("base_url", DEFAULT_RELEASE_BASE_URL) + # When invoked from the bzlmod python extension, a module_ctx is plumbed in + # so the coverage_dep logger can attribute warnings to the right module and + # honor module-root filtering. In the WORKSPACE/macro path no module_ctx is + # available; a minimal stand-in struct gives the logger what it needs. + module_ctx = kwargs.pop("_internal_module_ctx", None) + if module_ctx != None: + coverage_logger = repo_utils.logger(module_ctx, name = "coverage_dep") + else: + coverage_logger = repo_utils.logger( + struct(getenv = lambda _: None), + name = "coverage_dep", + ) + + base_urls = kwargs.pop("base_urls", DEFAULT_RELEASE_BASE_URLS) tool_versions = tool_versions or TOOL_VERSIONS minor_mapping = minor_mapping or MINOR_MAPPING @@ -97,21 +111,6 @@ def python_register_toolchains( toolchain_repo_name = "{name}_toolchains".format(name = name) - # When using unreleased Bazel versions, the version is an empty string - if native.bazel_version: - bazel_major = int(native.bazel_version.split(".")[0]) - if bazel_major < 6: - if register_coverage_tool: - # buildifier: disable=print - print(( - "WARNING: ignoring register_coverage_tool=True when " + - "registering @{name}: Bazel 6+ required, got {version}" - ).format( - name = name, - version = native.bazel_version, - )) - register_coverage_tool = False - # list[str] of the platform names that were used loaded_platforms = [] @@ -123,7 +122,12 @@ def python_register_toolchains( continue loaded_platforms.append(platform) - (release_filename, urls, strip_prefix, patches, patch_strip) = get_release_info(platform, python_version, base_url, tool_versions) + (release_filename, urls, strip_prefix, patches, patch_strip) = get_release_info( + platform, + python_version, + base_urls = base_urls, + tool_versions = tool_versions, + ) # allow passing in a tool version coverage_tool = None @@ -136,6 +140,7 @@ def python_register_toolchains( ), python_version = python_version, platform = platform, + logger = coverage_logger, visibility = ["@{name}_{platform}//:__subpackages__".format( name = name, platform = platform, diff --git a/python/private/python_repository.bzl b/python/private/python_repository.bzl index cb0731e6eb..9c44971117 100644 --- a/python/private/python_repository.bzl +++ b/python/private/python_repository.bzl @@ -52,6 +52,98 @@ def is_standalone_interpreter(rctx, python_interpreter_path, *, logger = None): logger = logger, ).return_code == 0 +def _get_pycache_root(rctx): + """Calculates and creates the pycache root directory. + + Returns: + {type}`path | None` The path to the pycache root, or None if it couldn't + be created. + """ + os_name = repo_utils.get_platforms_os_name(rctx) + is_windows = os_name == "windows" + + # 1. RULES_PYTHON_PYCACHE_DIR + res = rctx.getenv("RULES_PYTHON_PYCACHE_DIR") + if res: + res = res + "/" + rctx.name + return repo_utils.mkdir(rctx, res) + + # Suffix for cases 2-4 + # The first level directory is static and documented so that it is easy to + # use with e.g. --sandbox_add_mount_pair=/tmp/rules_python_pycache + suffix = "rules_python_pycache/{}/{}".format(hash(str(rctx.workspace_root)), rctx.name) + + # 2. XDG_CACHE_HOME + res = rctx.getenv("XDG_CACHE_HOME") + if res: + path = repo_utils.mkdir(rctx, rctx.path(res).get_child(suffix)) + if path: + return path + + # 3. TMP or TEMP + res = rctx.getenv("TMP") or rctx.getenv("TEMP") + if res: + path = repo_utils.mkdir(rctx, rctx.path(res).get_child(suffix)) + if path: + return path + + # 4. /tmp or Windows equivalent + if is_windows: + path = rctx.path("C:/Temp").get_child(suffix) + else: + path = rctx.path("/tmp").get_child(suffix) + + return repo_utils.mkdir(rctx, path) + +def _create_pycache_symlinks(rctx, logger): + """Finds all directories with a .py file and creates __pycache__ symlinks. + + Args: + rctx: {type}`repository_ctx` The repository rule's context object. + logger: Optional logger to use for operations. + """ + pycache_root = _get_pycache_root(rctx) + logger.info(lambda: "pycache root: {}".format(pycache_root)) + pycache_root_str = str(pycache_root) if pycache_root else None + + os_name = repo_utils.get_platforms_os_name(rctx) + null_device = "NUL" if os_name == "windows" else "/dev/null" + + queue = [rctx.path(".")] + + # Starlark doesn't support recursion, use a loop with a queue. + # Using a large range as a safeguard. + for _ in range(1000000): + if not queue: + break + p = queue.pop() + + has_py = False + for child in p.readdir(): + # Skip hidden files and directories + if child.basename.startswith("."): + continue + + if child.is_dir: + if child.basename == "__pycache__" or str(child) == pycache_root_str: + continue + queue.append(child) + elif child.basename.endswith(".py"): + has_py = True + + if has_py: + pycache_dir = p.get_child("__pycache__") + if pycache_root: + pycache_relative = repo_utils.repo_root_relative_path(rctx, pycache_dir) + target_dir = pycache_root.get_child(pycache_relative) + + repo_utils.mkdir(rctx, target_dir) + rctx.delete(pycache_dir) + rctx.symlink(target_dir, pycache_dir) + else: + rctx.delete(pycache_dir) + rctx.symlink(null_device, pycache_dir) + def _python_repository_impl(rctx): if rctx.attr.distutils and rctx.attr.distutils_content: fail("Only one of (distutils, distutils_content) should be set.") @@ -123,45 +215,7 @@ def _python_repository_impl(rctx): logger = logger, ) - # Make the Python installation read-only. This is to prevent issues due to - # pycs being generated at runtime: - # * The pycs are not deterministic (they contain timestamps) - # * Multiple processes trying to write the same pycs can result in errors. - # - # Note, when on Windows the `chmod` may not work - if "windows" not in platform and "windows" != repo_utils.get_platforms_os_name(rctx): - repo_utils.execute_checked( - rctx, - op = "python_repository.MakeReadOnly", - arguments = [repo_utils.which_checked(rctx, "chmod"), "-R", "ugo-w", "lib"], - logger = logger, - ) - - # If the user is not ignoring the warnings, then proceed to run a check, - # otherwise these steps can be skipped, as they both result in some warning. - if not rctx.attr.ignore_root_user_error: - exec_result = repo_utils.execute_unchecked( - rctx, - op = "python_repository.TestReadOnly", - arguments = [repo_utils.which_checked(rctx, "touch"), "lib/.test"], - logger = logger, - ) - - # The issue with running as root is the installation is no longer - # read-only, so the problems due to pyc can resurface. - if exec_result.return_code == 0: - stdout = repo_utils.execute_checked_stdout( - rctx, - op = "python_repository.GetUserId", - arguments = [repo_utils.which_checked(rctx, "id"), "-u"], - logger = logger, - ) - uid = int(stdout.strip()) - if uid == 0: - logger.warn("The current user is root, which can cause spurious cache misses or build failures with the hermetic Python interpreter. See https://github.com/bazel-contrib/rules_python/pull/713.") - else: - logger.warn("The current user has CAP_DAC_OVERRIDE set, which can cause spurious cache misses or build failures with the hermetic Python interpreter. See https://github.com/bazel-contrib/rules_python/pull/713.") - + _create_pycache_symlinks(rctx, logger) python_bin = "python.exe" if ("windows" in platform) else "bin/python3" if "linux" in platform: @@ -186,17 +240,15 @@ def _python_repository_impl(rctx): break glob_include = [] - glob_exclude = [] - if rctx.attr.ignore_root_user_error or "windows" in platform: - glob_exclude += [ - # These pycache files are created on first use of the associated python files. - # Exclude them from the glob because otherwise between the first time and second time a python toolchain is used," - # the definition of this filegroup will change, and depending rules will get invalidated." - # See https://github.com/bazel-contrib/rules_python/issues/1008 for unconditionally adding these to toolchains so we can stop ignoring them." - # pyc* is ignored because pyc creation creates temporary .pyc.NNNN files - "**/__pycache__/*.pyc*", - "**/__pycache__/*.pyo*", - ] + glob_exclude = [ + # These pycache files are created on first use of the associated python files. + # Exclude them from the glob because otherwise between the first time and second time a python toolchain is used," + # the definition of this filegroup will change, and depending rules will get invalidated." + # See https://github.com/bazel-contrib/rules_python/issues/1008 for unconditionally adding these to toolchains so we can stop ignoring them." + # pyc* is ignored because pyc creation creates temporary .pyc.NNNN files + "**/__pycache__/*.pyc*", + "**/__pycache__/*.pyo*", + ] if "windows" in platform: glob_include += [ @@ -249,7 +301,6 @@ define_hermetic_runtime_toolchain_impl( "coverage_tool": rctx.attr.coverage_tool, "distutils": rctx.attr.distutils, "distutils_content": rctx.attr.distutils_content, - "ignore_root_user_error": rctx.attr.ignore_root_user_error, "name": rctx.attr.name, "netrc": rctx.attr.netrc, "patch_strip": rctx.attr.patch_strip, @@ -266,7 +317,15 @@ define_hermetic_runtime_toolchain_impl( else: attrs["urls"] = urls - return attrs + # Bazel <8.3.0 lacks repository_ctx.repo_metadata + if not hasattr(rctx, "repo_metadata"): + return attrs + + reproducible = rctx.attr.sha256 != "" + return rctx.repo_metadata( + reproducible = reproducible, + attrs_for_reproducibility = {} if reproducible else attrs, + ) python_repository = repository_rule( _python_repository_impl, @@ -299,7 +358,7 @@ For more information see {attr}`py_runtime.coverage_tool`. ), "ignore_root_user_error": attr.bool( default = True, - doc = "Whether the check for root should be ignored or not. This causes cache misses with .pyc files.", + doc = "Noop, will be removed in the next major release", mandatory = False, ), "netrc": attr.string( diff --git a/python/private/pythons_hub.bzl b/python/private/pythons_hub.bzl index cc25b4ba1d..c64abf9887 100644 --- a/python/private/pythons_hub.bzl +++ b/python/private/pythons_hub.bzl @@ -15,6 +15,7 @@ "Repo rule used by bzlmod extension to create a repo that has a map of Python interpreters and their labels" load("//python:versions.bzl", "PLATFORMS") +load(":pbs_manifest.bzl", "parse_runtime_manifest") load(":text_util.bzl", "render") load(":toolchains_repo.bzl", "toolchain_suite_content") @@ -30,13 +31,13 @@ load("@@{rules_python}//python/private:py_toolchain_suite.bzl", "py_toolchain_su load("@bazel_skylib//:bzl_library.bzl", "bzl_library") bzl_library( - name = "interpreters_bzl", + name = "interpreters", srcs = ["interpreters.bzl"], visibility = ["@rules_python//:__subpackages__"], ) bzl_library( - name = "versions_bzl", + name = "versions", srcs = ["versions.bzl"], visibility = ["@rules_python//:__subpackages__"], ) @@ -118,15 +119,24 @@ def _hub_repo_impl(rctx): executable = False, ) + python_versions = rctx.attr.python_versions + if not python_versions and not rctx.attr.toolchain_python_versions: + content = rctx.read(rctx.path(Label("//python/private:runtimes_manifest.txt"))) + entries = parse_runtime_manifest(content) + python_versions_str = render.list(sorted({getattr(e, "python_version", ""): None for e in entries if getattr(e, "python_version", "")})) + + else: + python_versions_str = render.list(python_versions) if python_versions else render.list(sorted({ + v: None + for v in rctx.attr.toolchain_python_versions + })) + rctx.file( "versions.bzl", _versions_bzl_template.format( default_python_version = rctx.attr.default_python_version, minor_mapping = render.dict(rctx.attr.minor_mapping), - python_versions = rctx.attr.python_versions or render.list(sorted({ - v: None - for v in rctx.attr.toolchain_python_versions - })), + python_versions = python_versions_str, ), executable = False, ) diff --git a/python/private/repo_utils.bzl b/python/private/repo_utils.bzl index 32a5b70e15..0e83fda28c 100644 --- a/python/private/repo_utils.bzl +++ b/python/private/repo_utils.bzl @@ -29,9 +29,9 @@ def _is_repo_debug_enabled(mrctx): Returns: True if enabled, False if not. """ - return _getenv(mrctx, REPO_DEBUG_ENV_VAR) == "1" + return mrctx.getenv(REPO_DEBUG_ENV_VAR) == "1" -def _logger(mrctx = None, name = None, verbosity_level = None): +def _logger(mrctx = None, name = None, verbosity_level = None, printer = None, mod = None): """Creates a logger instance for printing messages. Args: @@ -39,7 +39,10 @@ def _logger(mrctx = None, name = None, verbosity_level = None): `_rule_name` is present, it will be included in log messages. name: name for the logger. Optional for repository_ctx usage. verbosity_level: {type}`int | None` verbosity level. If not set, - taken from `mrctx` + taken from `mrctx`. + printer: a function to use for printing. Defaults to `print` or `fail` depending + on the logging method. + mod: {type}`module_ctx.module`. The module for which the logger is created. Returns: A struct with attributes logging: trace, debug, info, warn, fail. @@ -48,20 +51,30 @@ def _logger(mrctx = None, name = None, verbosity_level = None): the logger injected into the function work as expected by terminating on the given line. """ + default_verbosity_level = "WARN" + if mod: + if name: + name = "{}:{}".format(mod.name, name) + else: + name = mod.name + + if not mod.is_root: + default_verbosity_level = "ERROR" # the warnings are non actionable anyway, but we should keep them. + if verbosity_level == None: if _is_repo_debug_enabled(mrctx): - verbosity_level = "DEBUG" - else: - verbosity_level = "WARN" + default_verbosity_level = "DEBUG" - env_var_verbosity = _getenv(mrctx, REPO_VERBOSITY_ENV_VAR) - verbosity_level = env_var_verbosity or verbosity_level + env_var_verbosity = mrctx.getenv(REPO_VERBOSITY_ENV_VAR) + verbosity_level = env_var_verbosity or default_verbosity_level verbosity = { "DEBUG": 2, + "ERROR": -1, "FAIL": -1, "INFO": 1, "TRACE": 3, + "WARN": 0, }.get(verbosity_level, 0) if hasattr(mrctx, "attr"): @@ -70,10 +83,15 @@ def _logger(mrctx = None, name = None, verbosity_level = None): elif not name: fail("The name has to be specified when using the logger with `module_ctx`") + failures = [] + def _log(enabled_on_verbosity, level, message_cb_or_str, printer = print): if verbosity < enabled_on_verbosity: return + if level == "FAIL": + failures.append(None) + if type(message_cb_or_str) == "string": message = message_cb_or_str else: @@ -86,11 +104,12 @@ def _logger(mrctx = None, name = None, verbosity_level = None): ), message) # buildifier: disable=print return struct( - trace = lambda message_cb: _log(3, "TRACE", message_cb), - debug = lambda message_cb: _log(2, "DEBUG", message_cb), - info = lambda message_cb: _log(1, "INFO", message_cb), - warn = lambda message_cb: _log(0, "WARNING", message_cb), - fail = lambda message_cb: _log(-1, "FAIL", message_cb, fail), + trace = lambda message_cb: _log(3, "TRACE", message_cb, printer or print), + debug = lambda message_cb: _log(2, "DEBUG", message_cb, printer or print), + info = lambda message_cb: _log(1, "INFO", message_cb, printer or print), + warn = lambda message_cb: _log(0, "WARNING", message_cb, printer or print), + fail = lambda message_cb: _log(-1, "FAIL", message_cb, printer or fail), + failed = lambda: len(failures) != 0, ) def _execute_internal( @@ -183,7 +202,7 @@ def _execute_internal( output = _outputs_to_str(result, log_stdout = log_stdout, log_stderr = log_stderr), )) - result_kwargs = {k: getattr(result, k) for k in dir(result)} + result_kwargs = {k: getattr(result, k) for k in dir(result) if k not in ["to_json", "to_proto"]} return struct( describe_failure = lambda: _execute_describe_failure( op = op, @@ -291,10 +310,10 @@ def _which_unchecked(mrctx, binary_name): """ binary = mrctx.which(binary_name) if binary: - _watch(mrctx, binary) + mrctx.watch(binary) describe_failure = None else: - path = _getenv(mrctx, "PATH", "") + path = mrctx.getenv("PATH", "") describe_failure = lambda: _which_describe_failure(binary_name, path) return struct( @@ -303,17 +322,80 @@ def _which_unchecked(mrctx, binary_name): ) def _which_describe_failure(binary_name, path): + if "\\" in path or ";" in path: + path_parts = path.split(";") + else: + path_parts = path.split(":") + for i, v in enumerate(path_parts): + path_parts[i] = " [{}]: {}".format(i, v) return ( "Unable to find the binary '{binary_name}' on PATH.\n" + - " PATH = {path}" + " PATH entries:\n" + + "{path_str}" ).format( binary_name = binary_name, - path = path, + path_str = "\n".join(path_parts), ) -def _getenv(mrctx, name, default = None): - # Bazel 7+ API has (repository|module)_ctx.getenv - return getattr(mrctx, "getenv", mrctx.os.environ.get)(name, default) +def _mkdir(mrctx, path): + path = mrctx.path(path) + if path.exists: + return path + + repo_root = str(mrctx.path(".")) + path_str = str(path) + + if not _is_relative_to(mrctx, path_str, repo_root): + mkdir_bin = mrctx.which("mkdir") + if not mkdir_bin: + return None + res = mrctx.execute([mkdir_bin, "-p", path_str]) + if res.return_code != 0: + return None + return path + else: + placeholder = path.get_child(".placeholder") + mrctx.file(placeholder) + mrctx.delete(placeholder) + return path + +def _norm_path(mrctx, p): + p = str(p) + + # Windows is case-insensitive + if _get_platforms_os_name(mrctx) == "windows": + return p.lower() + return p + +def _relative_to(mrctx, path, parent, fail = fail): + path_str = str(path) + parent_str = str(parent) + path_d = _norm_path(mrctx, path_str) + "/" + parent_d = _norm_path(mrctx, parent_str) + "/" + if path_d.startswith(parent_d): + return path_str[len(parent_str):].removeprefix("/") + else: + fail("{} is not relative to {}".format(path, parent)) + +def _is_relative_to(mrctx, path, parent): + """Tell if `path` is equal to or beneath `parent`.""" + path_d = _norm_path(mrctx, path) + "/" + parent_d = _norm_path(mrctx, parent) + "/" + return path_d.startswith(parent_d) + +def _repo_root_relative_path(mrctx, path): + """Takes a path object and returns a repo-relative path string. + + Args: + mrctx: module_ctx or repository_ctx + path: {type}`path` a path within `mrctx` + + Returns: + {type}`str` a repo-root-relative path string. + """ + repo_root = str(mrctx.path(".")) + path_str = str(path) + return _relative_to(mrctx, path_str, repo_root) def _args_to_str(arguments): return " ".join([_arg_repr(a) for a in arguments]) @@ -429,36 +511,97 @@ def _get_platforms_cpu_name(mrctx): return "riscv64" return arch -# TODO: Remove after Bazel 6 support dropped -def _watch(mrctx, *args, **kwargs): - """Calls mrctx.watch, if available.""" - if not args and not kwargs: - fail("'watch' needs at least a single argument.") +def _extract(mrctx, *, archive, supports_whl_extraction = False, extract_needs_chmod = False, **kwargs): + """Extract an archive - if hasattr(mrctx, "watch"): - mrctx.watch(*args, **kwargs) + TODO: remove when the earliest supported bazel version is at least 8.3. -# TODO: Remove after Bazel 6 support dropped -def _watch_tree(mrctx, *args, **kwargs): - """Calls mrctx.watch_tree, if available.""" - if not args and not kwargs: - fail("'watch_tree' needs at least a single argument.") + Note, we are using the parameter here because there is very little ways how we can detect + whether we can support just extracting the whl. + """ + archive_original = None + if not supports_whl_extraction and archive.basename.endswith(".whl"): + archive_original = archive + archive = mrctx.path(archive.basename + ".zip") + mrctx.symlink(archive_original, archive) + + mrctx.extract( + archive = archive, + **kwargs + ) + if archive_original: + if not mrctx.delete(archive): + fail("Failed to remove the symlink after extracting") + + if extract_needs_chmod: + _maybe_fix_permissions(mrctx, whl_path = archive) + +def _maybe_fix_permissions(mrctx, *, whl_path, logger = None): + if not logger and hasattr(mrctx, "attr"): + logger = _logger(mrctx) + elif not logger: + fail("logger must be specified when using 'module_ctx'") + + os_name = _get_platforms_os_name(mrctx) + if os_name != "windows": + result = _execute_unchecked( + mrctx, + op = "Fixing wheel permissions {}".format(whl_path), + arguments = ["chmod", "-R", "a+rX", "."], + logger = logger, + ) + if result.return_code != 0: + logger.warn(lambda: "Failed to fix file permissions: {}".format(result.stderr)) + +def _rename(mrctx, src, dest): + """Rename a file or directory. + + TODO: remove when the earliest supported bazel version is at least 8.0. + + Args: + mrctx: module_ctx or repository_ctx object + src: {type}`path` the source path + dest: {type}`path` the destination path + """ + if hasattr(mrctx, "rename"): + mrctx.rename(src, dest) + return + + # Fallback for Bazel < 8.0 + os_name = _get_platforms_os_name(mrctx) + if os_name == "windows": + # On Windows, we use `cmd.exe /c move` to rename files/directories. + # We need to use backslashes for the paths. + res = mrctx.execute([ + "cmd.exe", + "/c", + "move", + str(src).replace("/", "\\"), + str(dest).replace("/", "\\"), + ]) + else: + res = mrctx.execute(["mv", str(src), str(dest)]) - if hasattr(mrctx, "watch_tree"): - mrctx.watch_tree(*args, **kwargs) + if res.return_code != 0: + fail("Failed to rename {} to {}: {}".format(src, dest, res.stderr)) repo_utils = struct( # keep sorted execute_checked = _execute_checked, execute_checked_stdout = _execute_checked_stdout, execute_unchecked = _execute_unchecked, + extract = _extract, get_platforms_cpu_name = _get_platforms_cpu_name, get_platforms_os_name = _get_platforms_os_name, - getenv = _getenv, is_repo_debug_enabled = _is_repo_debug_enabled, logger = _logger, - watch = _watch, - watch_tree = _watch_tree, + maybe_fix_permissions = _maybe_fix_permissions, + mkdir = _mkdir, + norm_path = _norm_path, + relative_to = _relative_to, + is_relative_to = _is_relative_to, + rename = _rename, + repo_root_relative_path = _repo_root_relative_path, which_checked = _which_checked, which_unchecked = _which_unchecked, ) diff --git a/python/private/runtimes_manifest.txt b/python/private/runtimes_manifest.txt new file mode 100755 index 0000000000..df02a3049c --- /dev/null +++ b/python/private/runtimes_manifest.txt @@ -0,0 +1,621 @@ +# Manifest of runtimes to make available +# Originally generated circa 2026-06 from the runtimes in TOOL_VERSIONS from the original python/versions.bzl +# To sort this file, execute: ./python/private/tools/sort_manifest.py python/private/runtimes_manifest.txt + +1409acd9a506e2d1d3b65c1488db4e40d8f19d09a7df099667c87a506f71c0ef 20220227/cpython-3.10.2+20220227-aarch64-apple-darwin-install_only.tar.gz +8f351a8cc348bb45c0f95b8634c8345ec6e749e483384188ad865b7428342703 20220227/cpython-3.10.2+20220227-aarch64-unknown-linux-gnu-install_only.tar.gz +8146ad4390710ec69b316a5649912df0247d35f4a42e2aa9615bffd87b3e235a 20220227/cpython-3.10.2+20220227-x86_64-apple-darwin-install_only.tar.gz +a1d9a594cd3103baa24937ad9150c1a389544b4350e859200b3e5c036ac352bd 20220227/cpython-3.10.2+20220227-x86_64-pc-windows-msvc-shared-install_only.tar.gz +9b64eca2a94f7aff9409ad70bdaa7fbbf8148692662e764401883957943620dd 20220227/cpython-3.10.2+20220227-x86_64-unknown-linux-gnu-install_only.tar.gz +2c99983d1e83e4b6e7411ed9334019f193fba626344a50c36fba6c25d4de78a2 20220502/cpython-3.10.4+20220502-aarch64-apple-darwin-install_only.tar.gz +d8098c0c54546637e7516f93b13403b11f9db285def8d7abd825c31407a13d7e 20220502/cpython-3.10.4+20220502-aarch64-unknown-linux-gnu-install_only.tar.gz +f2711eaffff3477826a401d09a013c6802f11c04c63ab3686aa72664f1216a05 20220502/cpython-3.10.4+20220502-x86_64-apple-darwin-install_only.tar.gz +bee24a3a5c83325215521d261d73a5207ab7060ef3481f76f69b4366744eb81d 20220502/cpython-3.10.4+20220502-x86_64-pc-windows-msvc-shared-install_only.tar.gz +f6f871e53a7b1469c13f9bd7920ad98c4589e549acad8e5a1e14760fff3dd5c9 20220502/cpython-3.10.4+20220502-x86_64-unknown-linux-gnu-install_only.tar.gz +efaf66acdb9a4eb33d57702607d2e667b1a319d58c167a43c96896b97419b8b7 20220802/cpython-3.10.6+20220802-aarch64-apple-darwin-install_only.tar.gz +81625f5c97f61e2e3d7e9f62c484b1aa5311f21bd6545451714b949a29da5435 20220802/cpython-3.10.6+20220802-aarch64-unknown-linux-gnu-install_only.tar.gz +7718411adf3ea1480f3f018a643eb0550282aefe39e5ecb3f363a4a566a9398c 20220802/cpython-3.10.6+20220802-x86_64-apple-darwin-install_only.tar.gz +91889a7dbdceea585ff4d3b7856a6bb8f8a4eca83a0ff52a73542c2e67220eaa 20220802/cpython-3.10.6+20220802-x86_64-pc-windows-msvc-shared-install_only.tar.gz +55aa2190d28dcfdf414d96dc5dcea9fe048fadcd583dc3981fec020869826111 20220802/cpython-3.10.6+20220802-x86_64-unknown-linux-gnu-install_only.tar.gz +d52b03817bd245d28e0a8b2f715716cd0fcd112820ccff745636932c76afa20a 20221106/cpython-3.10.8+20221106-aarch64-apple-darwin-install_only.tar.gz +33170bef18c811906b738be530f934640491b065bf16c4d276c6515321918132 20221106/cpython-3.10.8+20221106-aarch64-unknown-linux-gnu-install_only.tar.gz +525b79c7ce5de90ab66bd07b0ac1008bafa147ddc8a41bef15ffb7c9c1e9e7c5 20221106/cpython-3.10.8+20221106-x86_64-apple-darwin-install_only.tar.gz +f2b6d2f77118f06dd2ca04dae1175e44aaa5077a5ed8ddc63333c15347182bfe 20221106/cpython-3.10.8+20221106-x86_64-pc-windows-msvc-shared-install_only.tar.gz +6c8db44ae0e18e320320bbaaafd2d69cde8bfea171ae2d651b7993d1396260b7 20221106/cpython-3.10.8+20221106-x86_64-unknown-linux-gnu-install_only.tar.gz +018d05a779b2de7a476f3b3ff2d10f503d69d14efcedd0774e6dab8c22ef84ff 20230116/cpython-3.10.9+20230116-aarch64-apple-darwin-install_only.tar.gz +2003750f40cd09d4bf7a850342613992f8d9454f03b3c067989911fb37e7a4d1 20230116/cpython-3.10.9+20230116-aarch64-unknown-linux-gnu-install_only.tar.gz +0e685f98dce0e5bc8da93c7081f4e6c10219792e223e4b5886730fd73a7ba4c6 20230116/cpython-3.10.9+20230116-x86_64-apple-darwin-install_only.tar.gz +59c6970cecb357dc1d8554bd0540eb81ee7f6d16a07acf3d14ed294ece02c035 20230116/cpython-3.10.9+20230116-x86_64-pc-windows-msvc-shared-install_only.tar.gz +d196347aeb701a53fe2bb2b095abec38d27d0fa0443f8a1c2023a1bed6e18cdf 20230116/cpython-3.10.9+20230116-x86_64-unknown-linux-gnu-install_only.tar.gz +4918cdf1cab742a90f85318f88b8122aeaa2d04705803c7b6e78e81a3dd40f80 20230116/cpython-3.11.1+20230116-aarch64-apple-darwin-install_only.tar.gz +debf15783bdcb5530504f533d33fda75a7b905cec5361ae8f33da5ba6599f8b4 20230116/cpython-3.11.1+20230116-aarch64-unknown-linux-gnu-install_only.tar.gz +20a4203d069dc9b710f70b09e7da2ce6f473d6b1110f9535fb6f4c469ed54733 20230116/cpython-3.11.1+20230116-x86_64-apple-darwin-install_only.tar.gz +edc08979cb0666a597466176511529c049a6f0bba8adf70df441708f766de5bf 20230116/cpython-3.11.1+20230116-x86_64-pc-windows-msvc-shared-install_only.tar.gz +02a551fefab3750effd0e156c25446547c238688a32fabde2995c941c03a6423 20230116/cpython-3.11.1+20230116-x86_64-unknown-linux-gnu-install_only.tar.gz +8348bc3c2311f94ec63751fb71bd0108174be1c4def002773cf519ee1506f96f 20230507/cpython-3.10.11+20230507-aarch64-apple-darwin-install_only.tar.gz +c7573fdb00239f86b22ea0e8e926ca881d24fde5e5890851339911d76110bc35 20230507/cpython-3.10.11+20230507-aarch64-unknown-linux-gnu-install_only.tar.gz +73a9d4c89ed51be39dd2de4e235078281087283e9fdedef65bec02f503e906ee 20230507/cpython-3.10.11+20230507-ppc64le-unknown-linux-gnu-install_only.tar.gz +bd3fc6e4da6f4033ebf19d66704e73b0804c22641ddae10bbe347c48f82374ad 20230507/cpython-3.10.11+20230507-x86_64-apple-darwin-install_only.tar.gz +9c2d3604a06fcd422289df73015cd00e7271d90de28d2c910f0e2309a7f73a68 20230507/cpython-3.10.11+20230507-x86_64-pc-windows-msvc-shared-install_only.tar.gz +c5bcaac91bc80bfc29cf510669ecad12d506035ecb3ad85ef213416d54aecd79 20230507/cpython-3.10.11+20230507-x86_64-unknown-linux-gnu-install_only.tar.gz +09e412506a8d63edbb6901742b54da9aa7faf120b8dbdce56c57b303fc892c86 20230507/cpython-3.11.3+20230507-aarch64-apple-darwin-install_only.tar.gz +8190accbbbbcf7620f1ff6d668e4dd090c639665d11188ce864b62554d40e5ab 20230507/cpython-3.11.3+20230507-aarch64-unknown-linux-gnu-install_only.tar.gz +767d24f3570b35fedb945f5ac66224c8983f2d556ab83c5cfaa5f3666e9c212c 20230507/cpython-3.11.3+20230507-ppc64le-unknown-linux-gnu-install_only.tar.gz +f710b8d60621308149c100d5175fec39274ed0b9c99645484fd93d1716ef4310 20230507/cpython-3.11.3+20230507-x86_64-apple-darwin-install_only.tar.gz +24741066da6f35a7ff67bee65ce82eae870d84e1181843e64a7076d1571e95af 20230507/cpython-3.11.3+20230507-x86_64-pc-windows-msvc-shared-install_only.tar.gz +da50b87d1ec42b3cb577dfd22a3655e43a53150f4f98a4bfb40757c9d7839ab5 20230507/cpython-3.11.3+20230507-x86_64-unknown-linux-gnu-install_only.tar.gz +bc66c706ea8c5fc891635fda8f9da971a1a901d41342f6798c20ad0b2a25d1d6 20230726/cpython-3.10.12+20230726-aarch64-apple-darwin-install_only.tar.gz +fee80e221663eca5174bd794cb5047e40d3910dbeadcdf1f09d405a4c1c15fe4 20230726/cpython-3.10.12+20230726-aarch64-unknown-linux-gnu-install_only.tar.gz +bb5e8cb0d2e44241725fa9b342238245503e7849917660006b0246a9c97b1d6c 20230726/cpython-3.10.12+20230726-ppc64le-unknown-linux-gnu-install_only.tar.gz +8d33d435ae6fb93ded7fc26798cc0a1a4f546a4e527012a1e2909cc314b332df 20230726/cpython-3.10.12+20230726-s390x-unknown-linux-gnu-install_only.tar.gz +8a6e3ed973a671de468d9c691ed9cb2c3a4858c5defffcf0b08969fba9c1dd04 20230726/cpython-3.10.12+20230726-x86_64-apple-darwin-install_only.tar.gz +c1a31c353ca44de7d1b1a3b6c55a823e9c1eed0423d4f9f66e617bdb1b608685 20230726/cpython-3.10.12+20230726-x86_64-pc-windows-msvc-shared-install_only.tar.gz +a476dbca9184df9fc69fe6309cda5ebaf031d27ca9e529852437c94ec1bc43d3 20230726/cpython-3.10.12+20230726-x86_64-unknown-linux-gnu-install_only.tar.gz +cb6d2948384a857321f2aa40fa67744cd9676a330f08b6dad7070bda0b6120a4 20230726/cpython-3.11.4+20230726-aarch64-apple-darwin-install_only.tar.gz +2e84fc53f4e90e11963281c5c871f593abcb24fc796a50337fa516be99af02fb 20230726/cpython-3.11.4+20230726-aarch64-unknown-linux-gnu-install_only.tar.gz +df7b92ed9cec96b3bb658fb586be947722ecd8e420fb23cee13d2e90abcfcf25 20230726/cpython-3.11.4+20230726-ppc64le-unknown-linux-gnu-install_only.tar.gz +e477f0749161f9aa7887964f089d9460a539f6b4a8fdab5166f898210e1a87a4 20230726/cpython-3.11.4+20230726-s390x-unknown-linux-gnu-install_only.tar.gz +47e1557d93a42585972772e82661047ca5f608293158acb2778dccf120eabb00 20230726/cpython-3.11.4+20230726-x86_64-apple-darwin-install_only.tar.gz +878614c03ea38538ae2f758e36c85d2c0eb1eaaca86cd400ff8c76693ee0b3e1 20230726/cpython-3.11.4+20230726-x86_64-pc-windows-msvc-shared-install_only.tar.gz +e26247302bc8e9083a43ce9e8dd94905b40d464745b1603041f7bc9a93c65d05 20230726/cpython-3.11.4+20230726-x86_64-unknown-linux-gnu-install_only.tar.gz +dab64b3580118ad2073babd7c29fd2053b616479df5c107d31fe2af1f45e948b 20230826/cpython-3.11.5+20230826-aarch64-apple-darwin-install_only.tar.gz +bb5c5d1ea0f199fe2d3f0996fff4b48ca6ddc415a3dbd98f50bff7fce48aac80 20230826/cpython-3.11.5+20230826-aarch64-unknown-linux-gnu-install_only.tar.gz +14121b53e9c8c6d0741f911ae00102a35adbcf5c3cdf732687ef7617b7d7304d 20230826/cpython-3.11.5+20230826-ppc64le-unknown-linux-gnu-install_only.tar.gz +fe459da39874443579d6fe88c68777c6d3e331038e1fb92a0451879fb6beb16d 20230826/cpython-3.11.5+20230826-s390x-unknown-linux-gnu-install_only.tar.gz +4a4efa7378c72f1dd8ebcce1afb99b24c01b07023aa6b8fea50eaedb50bf2bfc 20230826/cpython-3.11.5+20230826-x86_64-apple-darwin-install_only.tar.gz +00f002263efc8aea896bcfaaf906b1f4dab3e5cd3db53e2b69ab9a10ba220b97 20230826/cpython-3.11.5+20230826-x86_64-pc-windows-msvc-shared-install_only.tar.gz +fbed6f7694b2faae5d7c401a856219c945397f772eea5ca50c6eb825cbc9d1e1 20230826/cpython-3.11.5+20230826-x86_64-unknown-linux-gnu-install_only.tar.gz +916c35125b5d8323a21526d7a9154ca626453f63d0878e95b9f613a95006c990 20231002/cpython-3.11.6+20231002-aarch64-apple-darwin-install_only.tar.gz +3e26a672df17708c4dc928475a5974c3fb3a34a9b45c65fb4bd1e50504cc84ec 20231002/cpython-3.11.6+20231002-aarch64-unknown-linux-gnu-install_only.tar.gz +7937035f690a624dba4d014ffd20c342e843dd46f89b0b0a1e5726b85deb8eaf 20231002/cpython-3.11.6+20231002-ppc64le-unknown-linux-gnu-install_only.tar.gz +f9f19823dba3209cedc4647b00f46ed0177242917db20fb7fb539970e384531c 20231002/cpython-3.11.6+20231002-s390x-unknown-linux-gnu-install_only.tar.gz +178cb1716c2abc25cb56ae915096c1a083e60abeba57af001996e8bc6ce1a371 20231002/cpython-3.11.6+20231002-x86_64-apple-darwin-install_only.tar.gz +3933545e6d41462dd6a47e44133ea40995bc6efeed8c2e4cbdf1a699303e95ea 20231002/cpython-3.11.6+20231002-x86_64-pc-windows-msvc-shared-install_only.tar.gz +ee37a7eae6e80148c7e3abc56e48a397c1664f044920463ad0df0fc706eacea8 20231002/cpython-3.11.6+20231002-x86_64-unknown-linux-gnu-install_only.tar.gz +4734a2be2becb813830112c780c9879ac3aff111a0b0cd590e65ec7465774d02 20231002/cpython-3.12.0+20231002-aarch64-apple-darwin-install_only.tar.gz +bccfe67cf5465a3dfb0336f053966e2613a9bc85a6588c2fcf1366ef930c4f88 20231002/cpython-3.12.0+20231002-aarch64-unknown-linux-gnu-install_only.tar.gz +b5dae075467ace32c594c7877fe6ebe0837681f814601d5d90ba4c0dfd87a1f2 20231002/cpython-3.12.0+20231002-ppc64le-unknown-linux-gnu-install_only.tar.gz +5681621349dd85d9726d1b67c84a9686ce78f72e73a6f9e4cc4119911655759e 20231002/cpython-3.12.0+20231002-s390x-unknown-linux-gnu-install_only.tar.gz +5a9e88c8aa52b609d556777b52ebde464ae4b4f77e4aac4eb693af57395c9abf 20231002/cpython-3.12.0+20231002-x86_64-apple-darwin-install_only.tar.gz +facfaa1fbc8653f95057f3c4a0f8aa833dab0e0b316e24ee8686bc761d4b4f8d 20231002/cpython-3.12.0+20231002-x86_64-pc-windows-msvc-shared-install_only.tar.gz +e51a5293f214053ddb4645b2c9f84542e2ef86870b8655704367bd4b29d39fe9 20231002/cpython-3.12.0+20231002-x86_64-unknown-linux-gnu-install_only.tar.gz +b042c966920cf8465385ca3522986b12d745151a72c060991088977ca36d3883 20240107/cpython-3.11.7+20240107-aarch64-apple-darwin-install_only.tar.gz +b102eaf865eb715aa98a8a2ef19037b6cc3ae7dfd4a632802650f29de635aa13 20240107/cpython-3.11.7+20240107-aarch64-unknown-linux-gnu-install_only.tar.gz +b44e1b74afe75c7b19143413632c4386708ae229117f8f950c2094e9681d34c7 20240107/cpython-3.11.7+20240107-ppc64le-unknown-linux-gnu-install_only.tar.gz +49520e3ff494708020f306e30b0964f079170be83e956be4504f850557378a22 20240107/cpython-3.11.7+20240107-s390x-unknown-linux-gnu-install_only.tar.gz +a0e615eef1fafdc742da0008425a9030b7ea68a4ae4e73ac557ef27b112836d4 20240107/cpython-3.11.7+20240107-x86_64-apple-darwin-install_only.tar.gz +67077e6fa918e4f4fd60ba169820b00be7c390c497bf9bc9cab2c255ea8e6f3e 20240107/cpython-3.11.7+20240107-x86_64-pc-windows-msvc-shared-install_only.tar.gz +4a51ce60007a6facf64e5495f4cf322e311ba9f39a8cd3f3e4c026eae488e140 20240107/cpython-3.11.7+20240107-x86_64-unknown-linux-gnu-install_only.tar.gz +f93f8375ca6ac0a35d58ff007043cbd3a88d9609113f1cb59cf7c8d215f064af 20240107/cpython-3.12.1+20240107-aarch64-apple-darwin-install_only.tar.gz +236533ef20e665007a111c2f36efb59c87ae195ad7dca223b6dc03fb07064f0b 20240107/cpython-3.12.1+20240107-aarch64-unknown-linux-gnu-install_only.tar.gz +78051f0d1411ee62bc2af5edfccf6e8400ac4ef82887a2affc19a7ace6a05267 20240107/cpython-3.12.1+20240107-ppc64le-unknown-linux-gnu-install_only.tar.gz +60631211c701f8d2c56e5dd7b154e68868128a019b9db1d53a264f56c0d4aee2 20240107/cpython-3.12.1+20240107-s390x-unknown-linux-gnu-install_only.tar.gz +eca96158c1568dedd9a0b3425375637a83764d1fa74446438293089a8bfac1f8 20240107/cpython-3.12.1+20240107-x86_64-apple-darwin-install_only.tar.gz +fd5a9e0f41959d0341246d3643f2b8794f638adc0cec8dd5e1b6465198eae08a 20240107/cpython-3.12.1+20240107-x86_64-pc-windows-msvc-shared-install_only.tar.gz +74e330b8212ca22fd4d9a2003b9eec14892155566738febc8e5e572f267b9472 20240107/cpython-3.12.1+20240107-x86_64-unknown-linux-gnu-install_only.tar.gz +5fdc0f6a5b5a90fd3c528e8b1da8e3aac931ea8690126c2fdb4254c84a3ff04a 20240224/cpython-3.10.13+20240224-aarch64-apple-darwin-install_only.tar.gz +a898a88705611b372297bb8fe4d23cc16b8603ce5f24494c3a8cfa65d83787f9 20240224/cpython-3.10.13+20240224-aarch64-unknown-linux-gnu-install_only.tar.gz +c23706e138a0351fc1e9def2974af7b8206bac7ecbbb98a78f5aa9e7535fee42 20240224/cpython-3.10.13+20240224-ppc64le-unknown-linux-gnu-install_only.tar.gz +09be8fb2cdfbb4a93d555f268f244dbe4d8ff1854b2658e8043aa4ec08aede3e 20240224/cpython-3.10.13+20240224-s390x-unknown-linux-gnu-install_only.tar.gz +6378dfd22f58bb553ddb02be28304d739cd730c1f95c15c74955c923a1bc3d6a 20240224/cpython-3.10.13+20240224-x86_64-apple-darwin-install_only.tar.gz +086f7fe9156b897bb401273db8359017104168ac36f60f3af4e31ac7acd6634e 20240224/cpython-3.10.13+20240224-x86_64-pc-windows-msvc-shared-install_only.tar.gz +d995d032ca702afd2fc3a689c1f84a6c64972ecd82bba76a61d525f08eb0e195 20240224/cpython-3.10.13+20240224-x86_64-unknown-linux-gnu-install_only.tar.gz +389a51139f5abe071a0d70091ca5df3e7a3dfcfcbe3e0ba6ad85fb4c5638421e 20240224/cpython-3.11.8+20240224-aarch64-apple-darwin-install_only.tar.gz +389b9005fb78dd5a6f68df5ea45ab7b30d9a4b3222af96999e94fd20d4ad0c6a 20240224/cpython-3.11.8+20240224-aarch64-unknown-linux-gnu-install_only.tar.gz +eb2b31f8e50309aae493c6a359c32b723a676f07c641f5e8fe4b6aa4dbb50946 20240224/cpython-3.11.8+20240224-ppc64le-unknown-linux-gnu-install_only.tar.gz +844f64f4c16e24965778281da61d1e0e6cd1358a581df1662da814b1eed096b9 20240224/cpython-3.11.8+20240224-s390x-unknown-linux-gnu-install_only.tar.gz +097f467b0c36706bfec13f199a2eaf924e668f70c6e2bd1f1366806962f7e86e 20240224/cpython-3.11.8+20240224-x86_64-apple-darwin-install_only.tar.gz +b618f1f047349770ee1ef11d1b05899840abd53884b820fd25c7dfe2ec1664d4 20240224/cpython-3.11.8+20240224-x86_64-pc-windows-msvc-shared-install_only.tar.gz +94e13d0e5ad417035b80580f3e893a72e094b0900d5d64e7e34ab08e95439987 20240224/cpython-3.11.8+20240224-x86_64-unknown-linux-gnu-install_only.tar.gz +01c064c00013b0175c7858b159989819ead53f4746d40580b5b0b35b6e80fba6 20240224/cpython-3.12.2+20240224-aarch64-apple-darwin-install_only.tar.gz +e52550379e7c4ac27a87de832d172658bc04150e4e27d4e858e6d8cbb96fd709 20240224/cpython-3.12.2+20240224-aarch64-unknown-linux-gnu-install_only.tar.gz +74bc02c4bbbd26245c37b29b9e12d0a9c1b7ab93477fed8b651c988b6a9a6251 20240224/cpython-3.12.2+20240224-ppc64le-unknown-linux-gnu-install_only.tar.gz +ecd6b0285e5eef94deb784b588b4b425a15a43ae671bf206556659dc141a9825 20240224/cpython-3.12.2+20240224-s390x-unknown-linux-gnu-install_only.tar.gz +a53a6670a202c96fec0b8c55ccc780ea3af5307eb89268d5b41a9775b109c094 20240224/cpython-3.12.2+20240224-x86_64-apple-darwin-install_only.tar.gz +1e5655a6ccb1a64a78460e4e3ee21036c70246800f176a6c91043a3fe3654a3b 20240224/cpython-3.12.2+20240224-x86_64-pc-windows-msvc-shared-install_only.tar.gz +57a37b57f8243caa4cdac016176189573ad7620f0b6da5941c5e40660f9468ab 20240224/cpython-3.12.2+20240224-x86_64-unknown-linux-gnu-install_only.tar.gz +ccc40e5af329ef2af81350db2a88bbd6c17b56676e82d62048c15d548401519e 20240415/cpython-3.12.3+20240415-aarch64-apple-darwin-install_only.tar.gz +ec8126de97945e629cca9aedc80a29c4ae2992c9d69f2655e27ae73906ba187d 20240415/cpython-3.12.3+20240415-aarch64-unknown-linux-gnu-install_only.tar.gz +c5dcf08b8077e617d949bda23027c49712f583120b3ed744f9b143da1d580572 20240415/cpython-3.12.3+20240415-ppc64le-unknown-linux-gnu-install_only.tar.gz +872fc321363b8cdd826fd2cb1adfd1ceb813bc1281f9d410c1c2c4e177e8df86 20240415/cpython-3.12.3+20240415-s390x-unknown-linux-gnu-install_only.tar.gz +c37a22fca8f57d4471e3708de6d13097668c5f160067f264bb2b18f524c890c8 20240415/cpython-3.12.3+20240415-x86_64-apple-darwin-install_only.tar.gz +f7cfa4ad072feb4578c8afca5ba9a54ad591d665a441dd0d63aa366edbe19279 20240415/cpython-3.12.3+20240415-x86_64-pc-windows-msvc-shared-install_only.tar.gz +a73ba777b5d55ca89edef709e6b8521e3f3d4289581f174c8699adfb608d09d6 20240415/cpython-3.12.3+20240415-x86_64-unknown-linux-gnu-install_only.tar.gz +164d89f0df2feb689981864ecc1dffb19e6aa3696c8880166de555494fe92607 20240726/cpython-3.10.14+20240726-aarch64-apple-darwin-install_only.tar.gz +39bcd46b4d70e40da177c55259be16d5c2be7a3f7f93f1e3bde47e71b4833f29 20240726/cpython-3.10.14+20240726-aarch64-unknown-linux-gnu-install_only.tar.gz +549d38b9ef59cba9ab2990025255231bfa1cb32b4bc5eac321667640fdee19d1 20240726/cpython-3.10.14+20240726-ppc64le-unknown-linux-gnu-install_only.tar.gz +de4bc878a8666c734f983db971610980870148f333bda8b0c34abfaeae88d7ec 20240726/cpython-3.10.14+20240726-s390x-unknown-linux-gnu-install_only.tar.gz +1a1455838cd1e8ed0da14a152a2d559a2fd3a6047ba7013e841db4a35a228c1d 20240726/cpython-3.10.14+20240726-x86_64-apple-darwin-install_only.tar.gz +7f68821a8b5445267eca480660364ebd06ec84632b336770c6e39de07ac0f6c3 20240726/cpython-3.10.14+20240726-x86_64-pc-windows-msvc-shared-install_only.tar.gz +32b34cd13d9d745b3db3f3b8398ab2c07de74544829915dbebd8dce39bdc405e 20240726/cpython-3.10.14+20240726-x86_64-unknown-linux-gnu-install_only.tar.gz +cbdac9462bab9671c8e84650e425d3f43b775752a930a2ef954a0d457d5c00c3 20240726/cpython-3.11.9+20240726-aarch64-apple-darwin-install_only.tar.gz +4d17cf988abe24449d649aad3ef974091ab76807904d41839907061925b4c9e3 20240726/cpython-3.11.9+20240726-aarch64-unknown-linux-gnu-install_only.tar.gz +fc4f3c9ef9bfac2ed0282126ff376e544697ad04a5408d6429d46899d7d3bf21 20240726/cpython-3.11.9+20240726-ppc64le-unknown-linux-gnu-install_only.tar.gz +e69b66e53e926460df044f44846eef3fea642f630e829719e1a4112fc370dc56 20240726/cpython-3.11.9+20240726-s390x-unknown-linux-gnu-install_only.tar.gz +dc3174666a30f4c38d04e79a80c3159b4b3aa69597c4676701c8386696811611 20240726/cpython-3.11.9+20240726-x86_64-apple-darwin-install_only.tar.gz +f694be48bdfec1dace6d69a19906b6083f4dd7c7c61f1138ba520e433e5598f8 20240726/cpython-3.11.9+20240726-x86_64-pc-windows-msvc-shared-install_only.tar.gz +f6e955dc9ddfcad74e77abe6f439dac48ebca14b101ed7c85a5bf3206ed2c53d 20240726/cpython-3.11.9+20240726-x86_64-unknown-linux-gnu-install_only.tar.gz +1801025e825c04b3907e4ef6220a13607bc0397628c9485897073110ef7fde15 20240726/cpython-3.12.4+20240726-aarch64-apple-darwin-install_only.tar.gz +a098b18b7e9fea0c66867b76c0124fce9465765017572b2e7b522154c87c78d7 20240726/cpython-3.12.4+20240726-aarch64-unknown-linux-gnu-install_only.tar.gz +04011c4c5b7fe34b0b895edf4ad8748e410686c1d69aaee11d6688d481023bcb 20240726/cpython-3.12.4+20240726-ppc64le-unknown-linux-gnu-install_only.tar.gz +8f8f3e29cf0c2facdbcfee70660939fda7667ac24fee8656d3388fc72f3acc7c 20240726/cpython-3.12.4+20240726-s390x-unknown-linux-gnu-install_only.tar.gz +4c325838c1b0ed13698506fcd515be25c73dcbe195f8522cf98f9148a97601ed 20240726/cpython-3.12.4+20240726-x86_64-apple-darwin-install_only.tar.gz +74309b0f322716409883d38c621743ea7fa0376eb00927b8ee1e1671d3aff450 20240726/cpython-3.12.4+20240726-x86_64-pc-windows-msvc-shared-install_only.tar.gz +e133dd6fc6a2d0033e2658637cc22e9c95f9d7073b80115037ee1f16417a54ac 20240726/cpython-3.12.4+20240726-x86_64-unknown-linux-gnu-install_only.tar.gz +f64776f455a44c24d50f947c813738cfb7b9ac43732c44891bc831fa7940a33c 20241016/cpython-3.10.15+20241016-aarch64-apple-darwin-install_only.tar.gz +eb58581f85fde83d1f3e8e1f8c6f5a15c7ae4fdbe3b1d1083931f9167fdd8dbc 20241016/cpython-3.10.15+20241016-aarch64-unknown-linux-gnu-install_only.tar.gz +0c45af4e7525e2db59901606db32b2896ac1e9830c6f95551402207f537c2ce4 20241016/cpython-3.10.15+20241016-ppc64le-unknown-linux-gnu-install_only.tar.gz +de205896b070e6f5259ac0f2b3379eead875ea84e6a6ef533b89886fcbb46a4c 20241016/cpython-3.10.15+20241016-s390x-unknown-linux-gnu-install_only.tar.gz +90b46dfb1abd98d45663c7a2a8c45d3047a59391d8586d71b459cec7b75f662b 20241016/cpython-3.10.15+20241016-x86_64-apple-darwin-install_only.tar.gz +e48952619796c66ec9719867b87be97edca791c2ef7fbf87d42c417c3331609e 20241016/cpython-3.10.15+20241016-x86_64-pc-windows-msvc-shared-install_only.tar.gz +3db2171e03c1a7acdc599fba583c1b92306d3788b375c9323077367af1e9d9de 20241016/cpython-3.10.15+20241016-x86_64-unknown-linux-gnu-install_only.tar.gz +ed519c47d9620eb916a6f95ec2875396e7b1a9ab993ee40b2f31b837733f318c 20241016/cpython-3.10.15+20241016-x86_64-unknown-linux-musl-install_only.tar.gz +5a69382da99c4620690643517ca1f1f53772331b347e75f536088c42a4cf6620 20241016/cpython-3.11.10+20241016-aarch64-apple-darwin-install_only.tar.gz +803e49259280af0f5466d32829cd9d65a302b0226e424b3f0b261f9daf6aee8f 20241016/cpython-3.11.10+20241016-aarch64-unknown-linux-gnu-install_only.tar.gz +92b666d103902001322f42badbd68da92adc5cebb826af9c1c906c33166e2f34 20241016/cpython-3.11.10+20241016-ppc64le-unknown-linux-gnu-install_only.tar.gz +6d584317651c1ad4a857cb32d1999707e8bb3046fcb2f156d80381814fa19fde 20241016/cpython-3.11.10+20241016-s390x-unknown-linux-gnu-install_only.tar.gz +1e23ffe5bc473e1323ab8f51464da62d77399afb423babf67f8e13c82b69c674 20241016/cpython-3.11.10+20241016-x86_64-apple-darwin-install_only.tar.gz +647b66ff4552e70aec3bf634dd470891b4a2b291e8e8715b3bdb162f577d4c55 20241016/cpython-3.11.10+20241016-x86_64-pc-windows-msvc-shared-install_only.tar.gz +8b50a442b04724a24c1eebb65a36a0c0e833d35374dbdf9c9470d8a97b164cd9 20241016/cpython-3.11.10+20241016-x86_64-unknown-linux-gnu-install_only.tar.gz +d36fc77a8dd76155a7530f6235999a693b9e7c48aa11afeb5610a091cae5aa6f 20241016/cpython-3.11.10+20241016-x86_64-unknown-linux-musl-install_only.tar.gz +4c18852bf9c1a11b56f21bcf0df1946f7e98ee43e9e4c0c5374b2b3765cf9508 20241016/cpython-3.12.7+20241016-aarch64-apple-darwin-install_only.tar.gz +bba3c6be6153f715f2941da34f3a6a69c2d0035c9c5396bc5bb68c6d2bd1065a 20241016/cpython-3.12.7+20241016-aarch64-unknown-linux-gnu-install_only.tar.gz +0a1d1d92e33a969bd2f40a80af53c97b6c0cc1060d384ceff50ff801593bf9d6 20241016/cpython-3.12.7+20241016-ppc64le-unknown-linux-gnu-install_only.tar.gz +935676a0c960b552f95e9ac2e1e385de5de4b34038ff65ffdc688838f1189c17 20241016/cpython-3.12.7+20241016-s390x-unknown-linux-gnu-install_only.tar.gz +60c5271e7edc3c2ab47440b7abf4ed50fbc693880b474f74f05768f5b657045a 20241016/cpython-3.12.7+20241016-x86_64-apple-darwin-install_only.tar.gz +f05531bff16fa77b53be0776587b97b466070e768e6d5920894de988bdcd547a 20241016/cpython-3.12.7+20241016-x86_64-pc-windows-msvc-shared-install_only.tar.gz +43576f7db1033dd57b900307f09c2e86f371152ac8a2607133afa51cbfc36064 20241016/cpython-3.12.7+20241016-x86_64-unknown-linux-gnu-install_only.tar.gz +5ed4a4078db3cbac563af66403aaa156cd6e48831d90382a1820db2b120627b5 20241016/cpython-3.12.7+20241016-x86_64-unknown-linux-musl-install_only.tar.gz +efc2e71c0e05bc5bedb7a846e05f28dd26491b1744ded35ed82f8b49ccfa684b 20241016/cpython-3.13.0+20241016-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +31397953849d275aa2506580f3fa1cb5a85b6a3d392e495f8030e8b6412f5556 20241016/cpython-3.13.0+20241016-aarch64-apple-darwin-install_only.tar.gz +59b50df9826475d24bb7eff781fa3949112b5e9c92adb29e96a09cdf1216d5bd 20241016/cpython-3.13.0+20241016-aarch64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +e8378c0162b2e0e4cc1f62b29443a3305d116d09583304dbb0149fecaff6347b 20241016/cpython-3.13.0+20241016-aarch64-unknown-linux-gnu-install_only.tar.gz +1217efa5f4ce67fcc9f7eb64165b1bd0912b2a21bc25c1a7e2cb174a21a5df7e 20241016/cpython-3.13.0+20241016-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +fc4b7f27c4e84c78f3c8e6c7f8e4023e4638d11f1b36b6b5ce457b1926cebb53 20241016/cpython-3.13.0+20241016-ppc64le-unknown-linux-gnu-install_only.tar.gz +6c3e1e4f19d2b018b65a7e3ef4cd4225c5b9adfbc490218628466e636d5c4b8c 20241016/cpython-3.13.0+20241016-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +66b19e6a07717f6cfcd3a8ca953f0a2eaa232291142f3d26a8d17c979ec0f467 20241016/cpython-3.13.0+20241016-s390x-unknown-linux-gnu-install_only.tar.gz +2e07dfea62fe2215738551a179c87dbed1cc79d1b3654f4d7559889a6d5ce4eb 20241016/cpython-3.13.0+20241016-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +cff1b7e7cd26f2d47acac1ad6590e27d29829776f77e8afa067e9419f2f6ce77 20241016/cpython-3.13.0+20241016-x86_64-apple-darwin-install_only.tar.gz +bfd89f9acf866463bc4baf01733da5e767d13f5d0112175a4f57ba91f1541310 20241016/cpython-3.13.0+20241016-x86_64-pc-windows-msvc-shared-freethreaded+pgo-full.tar.zst +b25926e8ce4164cf103bacc4f4d154894ea53e07dd3fdd5ebb16fb1a82a7b1a0 20241016/cpython-3.13.0+20241016-x86_64-pc-windows-msvc-shared-install_only.tar.gz +a73adeda301ad843cce05f31a2d3e76222b656984535a7b87696a24a098b216c 20241016/cpython-3.13.0+20241016-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +2c8cb15c6a2caadaa98af51df6fe78a8155b8471cb3dd7b9836038e0d3657fb4 20241016/cpython-3.13.0+20241016-x86_64-unknown-linux-gnu-install_only.tar.gz +2f61ee3b628a56aceea63b46c7afe2df3e22a61da706606b0c8efda57f953cf4 20241016/cpython-3.13.0+20241016-x86_64-unknown-linux-musl-install_only.tar.gz +08f05618bdcf8064a7960b25d9ba92155447c9b08e0cf2f46a981e4c6a1bb5a5 20241205/cpython-3.13.1+20241205-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +88b88b609129c12f4b3841845aca13230f61e97ba97bd0fb28ee64b0e442a34f 20241205/cpython-3.13.1+20241205-aarch64-apple-darwin-install_only.tar.gz +9f2fcb809f9ba6c7c014a8803073a88786701a98971135bce684355062e4bb35 20241205/cpython-3.13.1+20241205-aarch64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +fdfa86c2746d2ae700042c461846e6c37f70c249925b58de8cd02eb8d1423d4e 20241205/cpython-3.13.1+20241205-aarch64-unknown-linux-gnu-install_only.tar.gz +15ceea78dff78ca8ccaac8d9c54b808af30daaa126f1f561e920a6896e098634 20241205/cpython-3.13.1+20241205-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +27b20b3237c55430ca1304e687d021f88373f906249f9cd272c5ff2803d5e5c3 20241205/cpython-3.13.1+20241205-ppc64le-unknown-linux-gnu-install_only.tar.gz +ed3c6118d1d12603309c930e93421ac7a30a69045ffd43006f63ecf71d72c317 20241205/cpython-3.13.1+20241205-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +7d0187e20cb5e36c689eec27e4d3de56d8b7f1c50dc5523550fc47377801521f 20241205/cpython-3.13.1+20241205-s390x-unknown-linux-gnu-install_only.tar.gz +dc780fecd215d2cc9e573abf1e13a175fcfa8f6efd100ef888494a248a16cda8 20241205/cpython-3.13.1+20241205-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +47eef6efb8664e2d1d23a7cdaf56262d784f8ace48f3bfca1b183e95a49888d6 20241205/cpython-3.13.1+20241205-x86_64-apple-darwin-install_only.tar.gz +7537b2ab361c0eabc0eabfca9ffd9862d7f5f6576eda13b97e98aceb5eea4fd3 20241205/cpython-3.13.1+20241205-x86_64-pc-windows-msvc-shared-freethreaded+pgo-full.tar.zst +f51f0493a5f979ff0b8d8c598a8d74f2a4d86a190c2729c85e0af65c36a9cbbe 20241205/cpython-3.13.1+20241205-x86_64-pc-windows-msvc-shared-install_only.tar.gz +9ec1b81213f849d91f5ebe6a16196e85cd6ff7c05ca823ce0ab7ba5b0e9fee84 20241205/cpython-3.13.1+20241205-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +242b2727df6c1e00de6a9f0f0dcb4562e168d27f428c785b0eb41a6aeb34d69a 20241205/cpython-3.13.1+20241205-x86_64-unknown-linux-gnu-install_only.tar.gz +76b30c6373b9c0aa2ba610e07da02f384aa210ac79643da38c66d3e6171c6ef5 20241205/cpython-3.13.1+20241205-x86_64-unknown-linux-musl-install_only.tar.gz +e3c4aa607717b23903ca2650d5c3ee24f89b97543e2db2b0f463bddc7a9e92f3 20241206/cpython-3.12.8+20241206-aarch64-apple-darwin-install_only.tar.gz +ce674b55442b732973afb2932c281bb1ded4ad7e22bcf9b07071165770758c7e 20241206/cpython-3.12.8+20241206-aarch64-unknown-linux-gnu-install_only.tar.gz +b7214790b273de9ed0532420054b72ba1393d62d2fc844ec55ade193771bd90c 20241206/cpython-3.12.8+20241206-ppc64le-unknown-linux-gnu-install_only.tar.gz +73102f5dbd7d1e7e9c2f2c80aedf2893d99a7fa407f6674ec8b2f57ba07daee5 20241206/cpython-3.12.8+20241206-s390x-unknown-linux-gnu-install_only.tar.gz +3ba35c706577d755e8e52a4c161a042464577c0e695e2a605362fa469e26de10 20241206/cpython-3.12.8+20241206-x86_64-apple-darwin-install_only.tar.gz +767b4be3ddf6b99e5ade519789c1615c191d8cf99d5aff4685cc18b48931f1e6 20241206/cpython-3.12.8+20241206-x86_64-pc-windows-msvc-shared-install_only.tar.gz +b9d6ee5ddac1198e72d53112698773fc8bb597de095592eb849ca794306699ba 20241206/cpython-3.12.8+20241206-x86_64-unknown-linux-gnu-install_only.tar.gz +6f305888703691dd04cfff85284d23ea0b0146ed7c4415e472f1fb72b3f32cdf 20241206/cpython-3.12.8+20241206-x86_64-unknown-linux-musl-install_only.tar.gz +e99f8457d9c79592c036489c5cfa78df76e4762d170665e499833e045d82608f 20250317/cpython-3.10.16+20250317-aarch64-apple-darwin-install_only.tar.gz +76d0f04d2444e77200fdc70d1c57480e29cca78cb7420d713bc1c523709c198d 20250317/cpython-3.10.16+20250317-aarch64-unknown-linux-gnu-install_only.tar.gz +39c9b3486de984fe1d72d90278229c70d6b08bcf69cd55796881b2d75077b603 20250317/cpython-3.10.16+20250317-ppc64le-unknown-linux-gnu-install_only.tar.gz +ebe949ada9293581c17d9bcdaa8f645f67d95f73eac65def760a71ef9dd6600d 20250317/cpython-3.10.16+20250317-riscv64-unknown-linux-gnu-install_only.tar.gz +9b2fc0b7f1c75b48e799b6fa14f7e24f5c61f2db82e3c65d13ed25e08f7f0857 20250317/cpython-3.10.16+20250317-s390x-unknown-linux-gnu-install_only.tar.gz +e03e62dbe95afa2f56b7344ff3bd061b180a0b690ff77f9a1d7e6601935e05ca 20250317/cpython-3.10.16+20250317-x86_64-apple-darwin-install_only.tar.gz +c7e0eb0ff5b36758b7a8cacd42eb223c056b9c4d36eded9bf5b9fe0c0b9aeb08 20250317/cpython-3.10.16+20250317-x86_64-pc-windows-msvc-install_only.tar.gz +b350c7e63956ca8edb856b91316328e0fd003a840cbd63d08253af43b2c63643 20250317/cpython-3.10.16+20250317-x86_64-unknown-linux-gnu-install_only.tar.gz +6ed64923ee4fbea4c5780f1a5a66651d239191ac10bd23420db4f5e4e0bf79c4 20250317/cpython-3.10.16+20250317-x86_64-unknown-linux-musl-install_only.tar.gz +7c7fd9809da0382a601a79287b5d62d61ce0b15f5a5ee836233727a516e85381 20250317/cpython-3.12.9+20250317-aarch64-apple-darwin-install_only.tar.gz +00c6bf9acef21ac741fea24dc449d0149834d30e9113429e50a95cce4b00bb80 20250317/cpython-3.12.9+20250317-aarch64-unknown-linux-gnu-install_only.tar.gz +25d77599dfd5849f17391d92da0da99079e4e94f19a881f763f5cc62530ef7e1 20250317/cpython-3.12.9+20250317-ppc64le-unknown-linux-gnu-install_only.tar.gz +e97ab0fdf443b302c56a52b4fd08f513bf3be66aa47263f0f9df3c6e60e05f2e 20250317/cpython-3.12.9+20250317-riscv64-unknown-linux-gnu-install_only.tar.gz +7492d079ffa8425c8f6c58e43b237c37e3fb7b31e2e14635927bb4d3397ba21e 20250317/cpython-3.12.9+20250317-s390x-unknown-linux-gnu-install_only.tar.gz +1ee1b1bb9fbce5c145c4bec9a3c98d7a4fa22543e09a7c1d932bc8599283c2dc 20250317/cpython-3.12.9+20250317-x86_64-apple-darwin-install_only.tar.gz +d15361fd202dd74ae9c3eece1abdab7655f1eba90bf6255cad1d7c53d463ed4d 20250317/cpython-3.12.9+20250317-x86_64-pc-windows-msvc-install_only.tar.gz +ef382fb88cbb41a3b0801690bd716b8a1aec07a6c6471010bcc6bd14cd575226 20250317/cpython-3.12.9+20250317-x86_64-unknown-linux-gnu-install_only.tar.gz +94e3837da1adf9964aab2d6047b33f70167de3096d1f9a2d1fa9340b1bbf537d 20250317/cpython-3.12.9+20250317-x86_64-unknown-linux-musl-install_only.tar.gz +c98c9c977e6fa05c3813bd49f3553904d89d60fed27e2e36468da7afa1d6d5e2 20250317/cpython-3.13.2+20250317-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +faa44274a331eb39786362818b21b3a4e74514e8805000b20b0e55c590cecb94 20250317/cpython-3.13.2+20250317-aarch64-apple-darwin-install_only.tar.gz +b8635e59e3143fd17f19a3dfe8ccc246ee6587c87da359bd1bcab35eefbb5f19 20250317/cpython-3.13.2+20250317-aarch64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +9c67260446fee6ea706dad577a0b32936c63f449c25d66e4383d5846b2ab2e36 20250317/cpython-3.13.2+20250317-aarch64-unknown-linux-gnu-install_only.tar.gz +6ae8fa44cb2edf4ab49cff1820b53c40c10349c0f39e11b8cd76ce7f3e7e1def 20250317/cpython-3.13.2+20250317-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +345b53d2f86c9dbd7f1320657cb227ff9a42ef63ff21f129abbbc8c82a375147 20250317/cpython-3.13.2+20250317-ppc64le-unknown-linux-gnu-install_only.tar.gz +2af1b8850c52801fb6189e7a17a51e0c93d9e46ddefcca72247b76329c97d02a 20250317/cpython-3.13.2+20250317-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +172d22b2330737f3a028ea538ffe497c39a066a8d3200b22dd4d177a3332ad85 20250317/cpython-3.13.2+20250317-riscv64-unknown-linux-gnu-install_only.tar.gz +c074144cc80c2af32c420b79a9df26e8db405212619990c1fbdd308bd75afe3f 20250317/cpython-3.13.2+20250317-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +ec3b16ea8a97e3138acec72bc5ff35949950c62c8994a8ec8e213fd93f0e806b 20250317/cpython-3.13.2+20250317-s390x-unknown-linux-gnu-install_only.tar.gz +0d73e4348d8d4b5159058609d2303705190405b485dd09ad05d870d7e0f36e0f 20250317/cpython-3.13.2+20250317-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +ee4526e84b5ce5b11141c50060b385320f2773616249a741f90c96d460ce8e8f 20250317/cpython-3.13.2+20250317-x86_64-apple-darwin-install_only.tar.gz +c51b4845fda5421e044067c111192f645234081d704313f74ee77fa013a186ea 20250317/cpython-3.13.2+20250317-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +84d7b52f3558c8e35c670a4fa14080c75e3ec584adfae49fec8b51008b75b21e 20250317/cpython-3.13.2+20250317-x86_64-pc-windows-msvc-install_only.tar.gz +1aea5062614c036904b55c1cc2fb4b500b7f6f7a4cacc263f4888889d355eef8 20250317/cpython-3.13.2+20250317-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +db011f0cd29cab2291584958f4e2eb001b0e6051848d89b38a2dc23c5c54e512 20250317/cpython-3.13.2+20250317-x86_64-unknown-linux-gnu-install_only.tar.gz +00bb2d629f7eacbb5c6b44dc04af26d1f1da64cee3425b0d8eb5135a93830296 20250317/cpython-3.13.2+20250317-x86_64-unknown-linux-musl-install_only.tar.gz +278dccade56b4bbeecb9a613b77012cf5c1433a5e9b8ef99230d5e61f31d9e02 20250610/cpython-3.13.4+20250610-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +c2ce6601b2668c7bd1f799986af5ddfbff36e88795741864aba6e578cb02ed7f 20250610/cpython-3.13.4+20250610-aarch64-apple-darwin-install_only.tar.gz +b1c1bd6ab9ef95b464d92a6a911cef1a8d9f0b0f6a192f694ef18ed15d882edf 20250610/cpython-3.13.4+20250610-aarch64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +3c2596ece08ffe17e11bc1f27aeb4ce1195d2490a83d695d36ef4933d5c5ca53 20250610/cpython-3.13.4+20250610-aarch64-unknown-linux-gnu-install_only.tar.gz +ed66ae213a62b286b9b7338b816ccd2815f5248b7a28a185dc8159fe004149ae 20250610/cpython-3.13.4+20250610-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +b3cc13ee177b8db1d3e9b2eac413484e3c6a356f97d91dc59de8d3fd8cf79d6b 20250610/cpython-3.13.4+20250610-ppc64le-unknown-linux-gnu-install_only.tar.gz +913264545215236660e4178bc3e5b57a20a444a8deb5c11680c95afc960b4016 20250610/cpython-3.13.4+20250610-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +d1b989e57a9ce29f6c945eeffe0e9750c222fdd09e99d2f8d6b0d8532a523053 20250610/cpython-3.13.4+20250610-riscv64-unknown-linux-gnu-install_only.tar.gz +7556a38ab5e507c1ec22bc38f9859982bc956cab7f4de05a2faac114feb306db 20250610/cpython-3.13.4+20250610-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +d1d19fb01961ac6476712fdd6c5031f74c83666f6f11aa066207e9a158f7e3d8 20250610/cpython-3.13.4+20250610-s390x-unknown-linux-gnu-install_only.tar.gz +64ab7ac8c88002d9ba20a92f72945bfa350268e944a7922500af75d20330574d 20250610/cpython-3.13.4+20250610-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +79feb6ca68f3921d07af52d9db06cf134e6f36916941ea850ab0bc20f5ff638b 20250610/cpython-3.13.4+20250610-x86_64-apple-darwin-install_only.tar.gz +9457504547edb2e0156bf76b53c7e4941c7f61c0eff9fd5f4d816d3df51c58e3 20250610/cpython-3.13.4+20250610-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +29ac3585cc2dcfd79e3fe380c272d00e9d34351fc456e149403c86d3fea34057 20250610/cpython-3.13.4+20250610-x86_64-pc-windows-msvc-install_only.tar.gz +864df6e6819e8f8e855ce30f34410fdc5867d0616e904daeb9a40e5806e970d7 20250610/cpython-3.13.4+20250610-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +44e5477333ebca298a7a0a316985c6c3533b8645f92a83f7f73c44033832bf32 20250610/cpython-3.13.4+20250610-x86_64-unknown-linux-gnu-install_only.tar.gz +a3afbfa94b9ff4d9fc426b47eb3c8446cada535075b8d51b7bdc9d9ab9911fc2 20250610/cpython-3.13.4+20250610-x86_64-unknown-linux-musl-install_only.tar.gz +a94c02b2d597cd6b075a713fe4e9a909cc97ca6a3b2b2ce86eda21be2062d48e 20250808/cpython-3.10.18+20250808-aarch64-apple-darwin-install_only.tar.gz +ef7de3b715d519e246d98ff7856247f7f7b357068705f09c6f300b7e7b76c701 20250808/cpython-3.10.18+20250808-aarch64-unknown-linux-gnu-install_only.tar.gz +f580efed11cc54e1a221c052e8bc88bfbc12844d3ca8949da828351a1232386e 20250808/cpython-3.10.18+20250808-ppc64le-unknown-linux-gnu-install_only.tar.gz +0d7e460e30203a9225b6f417ae972f66415a1cc0e32b37ebc48d195816282669 20250808/cpython-3.10.18+20250808-riscv64-unknown-linux-gnu-install_only.tar.gz +d4ada974daadb08a0184c19232ee3b03b3137aa70609760e1a94aaf7b12989ef 20250808/cpython-3.10.18+20250808-s390x-unknown-linux-gnu-install_only.tar.gz +da96fe2ba841640215788ddb9f151f03629360e37fcb94d4f76e5095b87df0d4 20250808/cpython-3.10.18+20250808-x86_64-apple-darwin-install_only.tar.gz +a648f3c9d136985ccfe57a5507e73d9d0839f7fd09eebd7c247857f2feaecb2a 20250808/cpython-3.10.18+20250808-x86_64-pc-windows-msvc-install_only.tar.gz +0b310a73bb9e7a495dbcad5f685e508ca2e7b36ee8f29301a52285730c425789 20250808/cpython-3.10.18+20250808-x86_64-unknown-linux-gnu-install_only.tar.gz +9cecf6ea2effbe183faebcf7e1160425a4ee17a68e49f2eefe5e1c59c51fa7ee 20250808/cpython-3.10.18+20250808-x86_64-unknown-linux-musl-install_only.tar.gz +d089bfd2c7b98a0942750a195e70d3172beda76d7747097b8afd87028b6e59b6 20250808/cpython-3.11.13+20250808-aarch64-apple-darwin-install_only.tar.gz +a632857c966237e7fd38b44c47c350f6e30d8ec54dcad6c832865ad670f0f22f 20250808/cpython-3.11.13+20250808-aarch64-pc-windows-msvc-install_only.tar.gz +bc57105f8a16acd57b71d926143c7f6ecf61729b40c8b4656f1b98bebd47c710 20250808/cpython-3.11.13+20250808-aarch64-unknown-linux-gnu-install_only.tar.gz +16a0165b0744940702b8fff80b8bf973ac914f78cb6fca28d389583f675e84de 20250808/cpython-3.11.13+20250808-ppc64le-unknown-linux-gnu-install_only.tar.gz +d8e62306be8f41c46bcd62ca68f91a1467f47adff632a35ff413dc1043ed56e8 20250808/cpython-3.11.13+20250808-riscv64-unknown-linux-gnu-install_only.tar.gz +4e302a4514a73baefdd9b327062bdafeb4115a799deec91c185f6ab45a857241 20250808/cpython-3.11.13+20250808-s390x-unknown-linux-gnu-install_only.tar.gz +d946d618f8bba8308b67e460a30612a71e2ccc309f85f6628aaae24e2b816981 20250808/cpython-3.11.13+20250808-x86_64-apple-darwin-install_only.tar.gz +ed963aee33d29ad8abfbb5fe63e42f57a2638a4a11a88e11d8bb66e61f20a6e5 20250808/cpython-3.11.13+20250808-x86_64-pc-windows-msvc-install_only.tar.gz +3ad988c702cbb017fef1208d47dea4138a2e85fd0f7f01ec5e1e335e597131b9 20250808/cpython-3.11.13+20250808-x86_64-unknown-linux-gnu-install_only.tar.gz +3a5810f0696f844289aa06d5c3a1efeab66eee999c25196b7d1954192a2c2100 20250808/cpython-3.11.13+20250808-x86_64-unknown-linux-musl-install_only.tar.gz +8792c4a84c364ab975feca0c27d3157a5435b7baab325a346ae56b223893b661 20250808/cpython-3.12.11+20250808-aarch64-apple-darwin-install_only.tar.gz +00bf7d7e8bcf5d1e9c4dfca0247d8e035147777cd57ee9d4c64dedca86b0a464 20250808/cpython-3.12.11+20250808-aarch64-pc-windows-msvc-install_only.tar.gz +4d7ba5314fab02130d6538f074961ffbf61310cade9180e59026074f9a8939cb 20250808/cpython-3.12.11+20250808-aarch64-unknown-linux-gnu-install_only.tar.gz +2c862eb40a81549d9c11e6bf5a7f07c3406310b14e6a4d16dcdf1c4763ef7090 20250808/cpython-3.12.11+20250808-ppc64le-unknown-linux-gnu-install_only.tar.gz +0bb729b95fabd49c7b495f7c44a9086e3970ea57daf66365741574bd36a17e81 20250808/cpython-3.12.11+20250808-riscv64-unknown-linux-gnu-install_only.tar.gz +99e465882d217d24ac90e99fac8f32e6a644d0340ac05ee510fb5cdf53f0cfb8 20250808/cpython-3.12.11+20250808-s390x-unknown-linux-gnu-install_only.tar.gz +e0c932709dafb05f00e528a7560ef8ee559ac82b75faca60dd1245bca1c1553f 20250808/cpython-3.12.11+20250808-x86_64-apple-darwin-install_only.tar.gz +81214ef71964a40ec269a79067ca490d45298c350583bc3af0e5781451a05c3c 20250808/cpython-3.12.11+20250808-x86_64-pc-windows-msvc-install_only.tar.gz +63d78840bf209af8da8f24e335d910f88387b892ca9187be571d481c071751bb 20250808/cpython-3.12.11+20250808-x86_64-unknown-linux-gnu-install_only.tar.gz +d633d070780590aa03ac5575cd9d7b9e17682d80f14b400313c009c387cf706b 20250808/cpython-3.12.11+20250808-x86_64-unknown-linux-musl-install_only.tar.gz +f2143304012e021a603bf1807bf3e4ce163832e43ab9a9829e53cb136497f207 20250808/cpython-3.13.6+20250808-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +8a1efa6af4e80f08e2c97dda822a3d6c24d6c98e518242f802c6a43ae8401488 20250808/cpython-3.13.6+20250808-aarch64-apple-darwin-install_only.tar.gz +552cfabcc3b103f4b1c4036d2592d5f0373c9554a2c4d2b6631b04ef7e592067 20250808/cpython-3.13.6+20250808-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +8e1617bd407ec1a874499daab26ae95080d1e0267ae616d34490137a28705827 20250808/cpython-3.13.6+20250808-aarch64-pc-windows-msvc-install_only.tar.gz +d84a7d64c284be387386b9f5da273f6d05486eb6bd8f9e86e2575cb59604cb22 20250808/cpython-3.13.6+20250808-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +11fa0591ae2211c08a42ae54944260e36ddf88a1d5604ea0c49e2477be4e5388 20250808/cpython-3.13.6+20250808-aarch64-unknown-linux-gnu-install_only.tar.gz +e76fcaf1bf80a615520dbe7f85ca0bb557fad96d132d836b0ac721e7cc1e2a37 20250808/cpython-3.13.6+20250808-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +8dcf34ae1a685fe1893b52917ae04f23328edadc4acae28499d43850c2bdd26c 20250808/cpython-3.13.6+20250808-ppc64le-unknown-linux-gnu-install_only.tar.gz +24e08a39ba4fc77753e61541e52eed39cc871f4a92a80a3c5dd495056bd8eff9 20250808/cpython-3.13.6+20250808-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +f8ed75aa6cc2011a046be00b629c3c8295267f34280324feaff34c73e7afce39 20250808/cpython-3.13.6+20250808-riscv64-unknown-linux-gnu-install_only.tar.gz +1609b223fd38a4a7a4d20e7173d7d9390fe2258f7dd9a15dc9ef0fa49613735d 20250808/cpython-3.13.6+20250808-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +7707ee5d19a78bc64ef8a66751ec7f97b64ea06714c7b1b52e8b321c2923ead8 20250808/cpython-3.13.6+20250808-s390x-unknown-linux-gnu-install_only.tar.gz +4360a1278dd0a96b526d108c8fd23498a9d2028dd7791e510fd51ff5ea3f462a 20250808/cpython-3.13.6+20250808-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +27badce7201321a8363219e438a6205165e5b4884012b1046532203df2ec9379 20250808/cpython-3.13.6+20250808-x86_64-apple-darwin-install_only.tar.gz +4e727cdbe4057b16a170f887c0fa4227a825ac59bcda84ae946c77cc932af78c 20250808/cpython-3.13.6+20250808-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +af5cc733c33b9aa9f1d74c81a59351e9b27215486d8b6cdbc06d97646a58c953 20250808/cpython-3.13.6+20250808-x86_64-pc-windows-msvc-install_only.tar.gz +e48c13c59cc3c01b79f63c8bccec27d2db6e97f64213b8731e2077b6ed8ed52c 20250808/cpython-3.13.6+20250808-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +f844e8c8b6847628b472f7e97d8893a4e93acd5382a902b465776063668c4d64 20250808/cpython-3.13.6+20250808-x86_64-unknown-linux-gnu-install_only.tar.gz +70076dea0ff65b3c05aae1a97b4a556bf613cc73db30309e59134f9d318f4f7b 20250808/cpython-3.13.6+20250808-x86_64-unknown-linux-musl-install_only.tar.gz +43bda24c2fc073bc308bf631203b917a72640d59b59fdad4ba14503d84727012 20251031/cpython-3.10.19+20251031-aarch64-apple-darwin-install_only.tar.gz +f77a8a8aa77f3f943126fa9215a25309da4bf20398fc8f4b4eec54b5fc7570ef 20251031/cpython-3.10.19+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz +1c55d160fc4c3b93528cd6aaa2bb4ca6018a99e5a45919d33dc761a43a69f860 20251031/cpython-3.10.19+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz +21134d35721cdad4c881f35d0957cc19df9a45d194afb38a099faded3c1cfb4d 20251031/cpython-3.10.19+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz +df0db070f1eb73ab4e371eea32213ddb3500737ea5560a6f0ffd65c82af64ddc 20251031/cpython-3.10.19+20251031-s390x-unknown-linux-gnu-install_only.tar.gz +76c12e633c09c2a790f8a958a55df4495527e0718d1875310c836e757c0c7b55 20251031/cpython-3.10.19+20251031-x86_64-apple-darwin-install_only.tar.gz +cfa08a4caf2df1b43551b843c052d6a8814e2ea0c97268b021f0423646c244c3 20251031/cpython-3.10.19+20251031-x86_64-pc-windows-msvc-install_only.tar.gz +fb1caac917d7b6497bb6f5950da5f1e48d05c43a498948dd97f85760c4382d9f 20251031/cpython-3.10.19+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz +ba85013ed5ac7733fc6840168cc33ed19e9959b363dc80227d54f8fd9c92c0f4 20251031/cpython-3.10.19+20251031-x86_64-unknown-linux-musl-install_only.tar.gz +6de5572b33c65af1c9b7caf00ec593fb04cffb7e14fa393a98261bb9bc464713 20251031/cpython-3.11.14+20251031-aarch64-apple-darwin-install_only.tar.gz +38d0d1466561e15965e8d2c20f5e5be649598f55c761ecab553d087fbd217337 20251031/cpython-3.11.14+20251031-aarch64-pc-windows-msvc-install_only.tar.gz +510edb027527413c4249256194cb8ad2590b52dd93f7123b4cb341aff5d05894 20251031/cpython-3.11.14+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz +4e0bc6a818e0c6a9d7d3ebe1a95591fd84440520577aa837facc96a4b7a80e35 20251031/cpython-3.11.14+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz +16519e69297144f81b2421333bc9e0b6466cf3c84749b216b695cfb4c9deb32f 20251031/cpython-3.11.14+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz +5f9c1b203cdf34c8bff1aef69b63bbf11309bd16ca6e429d8c3651eaa2b3d080 20251031/cpython-3.11.14+20251031-s390x-unknown-linux-gnu-install_only.tar.gz +4891cbf34e8652b7bd1054b9502395e4b7e048e2e517c040fbf6c8297cb954d6 20251031/cpython-3.11.14+20251031-x86_64-apple-darwin-install_only.tar.gz +5223b83ed9e2aa5e9e17d2ebcf767956e998876339b9cde1980a47e9d4655fb6 20251031/cpython-3.11.14+20251031-x86_64-pc-windows-msvc-install_only.tar.gz +60f0bd473d861cc45d3401d9914e47ccb9fa037f88a91879ed517a62042b8477 20251031/cpython-3.11.14+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz +25e82d1e85b90a8ab724ee633a1811b1921797f5c25ee69c6595052371b91a87 20251031/cpython-3.11.14+20251031-x86_64-unknown-linux-musl-install_only.tar.gz +5e110cb821d2eb8246065d3b46faa655180c976c4e17250f7883c634a629bc63 20251031/cpython-3.12.12+20251031-aarch64-apple-darwin-install_only.tar.gz +b190fed7c2b0f6e1010f554a0d1fd191c0754c4c0718e69d9d795ae559613780 20251031/cpython-3.12.12+20251031-aarch64-pc-windows-msvc-install_only.tar.gz +81b644d166e0bfb918615af8a2363f8fcf26eccdcc60a5334b6a62c088470bac 20251031/cpython-3.12.12+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz +024f5e5678c9768d45cc24d37a8e9d265aae86c4a4602352dee3d7deba367052 20251031/cpython-3.12.12+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz +b13c57fc372c131e667a99b9680f41c0b4da571cf99ed412103c2fe9ad5ed1fb 20251031/cpython-3.12.12+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz +2bf05bdd56cdf5ea4fd9f2faf151ea4211be96a0d1f4230b85f5dcae620d6400 20251031/cpython-3.12.12+20251031-s390x-unknown-linux-gnu-install_only.tar.gz +687052a046d33be49dc95dd671816709067cf6176ed36c93ea61b1fe0b883b0f 20251031/cpython-3.12.12+20251031-x86_64-apple-darwin-install_only.tar.gz +cff398b3f520c442a1b085dd347126c10c1b03f01ccc0decd8c897a687e893f1 20251031/cpython-3.12.12+20251031-x86_64-pc-windows-msvc-install_only.tar.gz +80c3882f14e15cef8260ef5257d198e8f4371ca265887431d939e0d561de3253 20251031/cpython-3.12.12+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz +0a461330b9b89f2ea3088dde10d7a3f96aa65897b7c5ce2404fa3b5c4b8daa14 20251031/cpython-3.12.12+20251031-x86_64-unknown-linux-musl-install_only.tar.gz +eae1272a72ccce601590a10a9ca2a58199b5fcdf022aa603a527e3e2a04de9bc 20251031/cpython-3.13.9+20251031-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +1f3568d17383426d52350c2ef7c93c1a5a043198b860cb05e5d19b35f9c25cef 20251031/cpython-3.13.9+20251031-aarch64-apple-darwin-install_only.tar.gz +743ff69935ef28834621647dab30f032dfcd80315732917531eea333210941c7 20251031/cpython-3.13.9+20251031-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +20db43873d3c4c2175d866806545e4ad4ec6bb72ca95e60082a4df6c24567e8c 20251031/cpython-3.13.9+20251031-aarch64-pc-windows-msvc-install_only.tar.gz +a6e72f9de5d9b46cf6968d6a492f2401a919f9b959f8da2d87f43484b80169ee 20251031/cpython-3.13.9+20251031-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +0a56d11b0fb1662e67f892b9d5d1717aef06f24dbb8362bc25b8f784e620d44e 20251031/cpython-3.13.9+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz +0ed5c65437f875c58ba1bee2b8d261d18698d3d0347a2e66f8902fce022a2cda 20251031/cpython-3.13.9+20251031-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +99492123902bd5e9a6b1a30135061e93a2e6a11d25107a741d5a756e91054448 20251031/cpython-3.13.9+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz +584e481d9b5225ffaf02f158fb26d2818207e65fc3c6dc21a6d500277f739220 20251031/cpython-3.13.9+20251031-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +b3dce3e4ef508773521e1ee1be989fff6118f8fd1fbbd0491d7ff7dfbc98ef06 20251031/cpython-3.13.9+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz +7fa7fb912ca989ceac026a332d56a2c7d6d16ab0e94d89e690de5aade26103e2 20251031/cpython-3.13.9+20251031-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +f10e34aaa856c1b8a69c2ea4a9a6723d520443d1a957bf66dc55491334ca0c1e 20251031/cpython-3.13.9+20251031-s390x-unknown-linux-gnu-install_only.tar.gz +e2bf5fa6a3ef443ade362e08b0a19bbc172f7bfe34dabe933ccaad31d53af5da 20251031/cpython-3.13.9+20251031-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +48c0f3ca5d31e90658ef99138dc21865bb62f388ab97a1ce72cac176da194ab0 20251031/cpython-3.13.9+20251031-x86_64-apple-darwin-install_only.tar.gz +318a9a1e43dd52054327de3bccc0c5b7afde7b7f2a398ccb4d38e03d28b05386 20251031/cpython-3.13.9+20251031-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +874593f641f31ea101440c70f81768c35d4d7d6df111fde63094db67465ef787 20251031/cpython-3.13.9+20251031-x86_64-pc-windows-msvc-install_only.tar.gz +dcc29b069d0588fbd4ea29c6df840c8d1207d2a3bce8cd5cd57d1b85373b6048 20251031/cpython-3.13.9+20251031-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +6f05b91ee8c7e6dd0f9c60b95bb29130e2d623961de6578b643e80ddd83f96b6 20251031/cpython-3.13.9+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz +ad987197034185e628715da504a50613af213dc21ba6d5ccaeab3db2c464aa6c 20251031/cpython-3.13.9+20251031-x86_64-unknown-linux-musl-install_only.tar.gz +d9c7b430b25bd3837dbb03f945dbe6b7bc526c5940ca96f5db7cdc42f6b2b801 20251031/cpython-3.14.0+20251031-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +b4bcd3c6c24cab32ae99e1b05c89312b783b4d69431d702e5012fe1fdcad4087 20251031/cpython-3.14.0+20251031-aarch64-apple-darwin-install_only.tar.gz +40266e60f655e49cd1d5303295255909a4b593b08b88be6e6a55b2c9fe6ed13d 20251031/cpython-3.14.0+20251031-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +599a8b7e12439cd95a201dbdfe95cf363146b1ff91f379555dafd86b170caab9 20251031/cpython-3.14.0+20251031-aarch64-pc-windows-msvc-install_only.tar.gz +f383ef50d1da6ca511212e5ae601923b56636b87351fd5fc847e0ea0a19fa9b3 20251031/cpython-3.14.0+20251031-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +128a9cbfb9645d5237ec01704d9d1d2ac5f084464cc43c37a4cd96aa9c3b1ad5 20251031/cpython-3.14.0+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz +cb0e4ff781b856a47f0f461ceb41c78c7eeff65effd0957857ec4702ef1e1bd3 20251031/cpython-3.14.0+20251031-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +e16ca51f018e99a609faf953bd3a3aea31f45ee84262d1a517fb3abd98f1f4af 20251031/cpython-3.14.0+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz +929223470d11a55cd75f880ac3bd4969e42407e2cdf08d4e7e38ba721cf4abec 20251031/cpython-3.14.0+20251031-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +fca340d8fb7a05cd90e216ce601b25d492ed8c1a3b6a6d77703e0f15ab3711a7 20251031/cpython-3.14.0+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz +613fb1f7b249f798b52af957d181305244e936c8e5c94c84688fcdf93fe14253 20251031/cpython-3.14.0+20251031-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +c5803644970eee931bb0581b3b64511d1a8612f67bc98951a7f7ab5581a9ed04 20251031/cpython-3.14.0+20251031-s390x-unknown-linux-gnu-install_only.tar.gz +b3196f6b57bbb3dc2ee07f348f1d51117ffa376979eceafbf50c15f0f7980bf8 20251031/cpython-3.14.0+20251031-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +4e71a3ce973be377ef18637826648bb936e2f9490f64a9e4f33a49bcc431d344 20251031/cpython-3.14.0+20251031-x86_64-apple-darwin-install_only.tar.gz +b81de5fc9e783ea6dfcf1098c28a278c874999c71afbb0309f6a8b4276c769d0 20251031/cpython-3.14.0+20251031-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +39acfcb3857d83eab054a3de11756ffc16b3d49c31393b9800dd2704d1f07fdf 20251031/cpython-3.14.0+20251031-x86_64-pc-windows-msvc-install_only.tar.gz +f4acbef0fbfaf7ab31ac63986da1d93dfa1c5cb797de1dcdc1a988aa18670120 20251031/cpython-3.14.0+20251031-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +3dec1ab70758a3467ac3313bbcdabf7a9b3016db5c072c4537e3cf0a9e6290f6 20251031/cpython-3.14.0+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz +d0a2a6d3b1bb00dce2105377fda8aa79675d187f8d6d7010a42f651af25018dc 20251031/cpython-3.14.0+20251031-x86_64-unknown-linux-musl-install_only.tar.gz +12f1b16be4017181ad67904caf9e59e525b9b5d62f49105017d837e27b832959 20251031/cpython-3.15.0a1+20251031-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +3acf7aa3559b746498b18929456c5cacb84bae4e09249834cbc818970d71de87 20251031/cpython-3.15.0a1+20251031-aarch64-apple-darwin-install_only.tar.gz +54ca78dae455ece6fefbd7f5f287cc55d5ce197caf51921f6d871d15069d9489 20251031/cpython-3.15.0a1+20251031-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +1508bcd7195008479ed156aad3afbb3a3793097ed530690f0304a8107f0e53e8 20251031/cpython-3.15.0a1+20251031-aarch64-pc-windows-msvc-install_only.tar.gz +981fe8dfc6e7e1d0ffefa945a18d5c4c759bbe21722acf3a5cc7e62f16aa5f3c 20251031/cpython-3.15.0a1+20251031-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +d55c2aeece827e6bec83fd18515ee281d9ea0efaa3e2d20130db8f1c7cbb71c6 20251031/cpython-3.15.0a1+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz +088400dec25139f38eeecb48f090ff2ce06a96a1dd79fa8f1dfec1cd1786f5ef 20251031/cpython-3.15.0a1+20251031-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +c28beda791c499b16f06256339522f0002a3e9acba003e6b8374755d7be1def2 20251031/cpython-3.15.0a1+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz +938061a0a31a06672526885de36037ddefd8c4acdb09424691b7000a8c8f8d01 20251031/cpython-3.15.0a1+20251031-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +36619f576b8154e4b56643c5c4a85c352f152df2989c4e602cbbe9c2b7ded870 20251031/cpython-3.15.0a1+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz +2003e7e40bb44b3db7bca81087bfb738fe6af40e5db61cda8e23b59bf55d409e 20251031/cpython-3.15.0a1+20251031-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +5ea47be2a3a563ddd87ff510dae26b7aa7f3855ca00c5f1056ff8114c067c4e4 20251031/cpython-3.15.0a1+20251031-s390x-unknown-linux-gnu-install_only.tar.gz +64fc29e6c7a2f02a18645d968f1b3fc1d00d12a5ef3fcbb0d077fa8c62c08904 20251031/cpython-3.15.0a1+20251031-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +0ab19d3ac25f99da438b088751e5ec2421f9f6aa4292fd2dc0f8e49eb3e16bdf 20251031/cpython-3.15.0a1+20251031-x86_64-apple-darwin-install_only.tar.gz +34abc5603e1b4131f753d29b7deac865b9277912b851cbed5a149cf3e6745d3d 20251031/cpython-3.15.0a1+20251031-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +5f5d6bec2b381cfc771c49972d2a6f7b7e7ab6a1651d8fb6ef3983f3571722b3 20251031/cpython-3.15.0a1+20251031-x86_64-pc-windows-msvc-install_only.tar.gz +0e0272186d9f5169394dbc4d4d72a3f4a5762a04c2e5ac2ab1e23aa41fc8538a 20251031/cpython-3.15.0a1+20251031-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +1f356288c2b2713619cb7a4e453d33bf8882f812af2987e21e01e7ae382fefba 20251031/cpython-3.15.0a1+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz +caf5311f333eef082dd69a669ca65aceba09a08fc1e78aad602ad649106f294c 20251031/cpython-3.15.0a1+20251031-x86_64-unknown-linux-musl-install_only.tar.gz +87275619c2706affa4d1090d2ca3dad354b6d69f8b85dbfafe38785870751b9a 20251031/cpython-3.9.25+20251031-aarch64-apple-darwin-install_only.tar.gz +6112d46355857680b81849764a6cf9f38cc4cd0d1cf29d432bc12fe5aeedf9d0 20251031/cpython-3.9.25+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz +828364b6f54fa45ac2dc91f8e45d5b74306372af374a9ef16eeb2ea81253ed3f 20251031/cpython-3.9.25+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz +17467e0158e5ad04453c447d6773c23b044172276441e22e23058fd3ea053e27 20251031/cpython-3.9.25+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz +3e9539f83e67faa813fd06171199b2d33c89821dfa9a33bf6e27ad67f1b6932d 20251031/cpython-3.9.25+20251031-s390x-unknown-linux-gnu-install_only.tar.gz +ace63cfe27a9487c4d72e1cb518be01c1d985271da0b2158e813801f7d3e5503 20251031/cpython-3.9.25+20251031-x86_64-apple-darwin-install_only.tar.gz +4fb1b416482ce94d73cfa140317a670c596c830671d137b07c26afe8c461768a 20251031/cpython-3.9.25+20251031-x86_64-pc-windows-msvc-install_only.tar.gz +42834f61eb6df43432c3dd6ab9ca3fdf8c06d10a404ebdb53d6902e6b9570b08 20251031/cpython-3.9.25+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz +76593e8c889e81e82db5fe117fe15b69466f85100ab2ec0e4035aa86242b4e93 20251031/cpython-3.9.25+20251031-x86_64-unknown-linux-musl-install_only.tar.gz +3c9fdd76447c1549a0d3bc2a70c63f1daec997ab034206ac0260a03237166dbb 20251202/cpython-3.13.10+20251202-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +37afe4e77ab62ac50f197b1cb1f3bc02c82735c6be893da0996afcde5dc41048 20251202/cpython-3.13.10+20251202-aarch64-apple-darwin-install_only.tar.gz +cdb7141327bdc244715b25752593e2c9eeb3cc2764f37dfe81cfbc92db9d6d57 20251202/cpython-3.13.10+20251202-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +9060d644bd32ac0e0af970d0b21e207e6ff416b7c4dc26ffc4f9b043fb45b463 20251202/cpython-3.13.10+20251202-aarch64-pc-windows-msvc-install_only.tar.gz +6d277221fa4b172e00b29c7158ca9661917bc8db9a0084b1a0ff5c3a0ba8b648 20251202/cpython-3.13.10+20251202-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +c68280591cda1c9515a04809fa6926020177e8e5892300206e0496ea1d10290e 20251202/cpython-3.13.10+20251202-aarch64-unknown-linux-gnu-install_only.tar.gz +d265d8d1c51e25ed70279540223589f79cf99ad00b50d28b6150c2658c973885 20251202/cpython-3.13.10+20251202-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +1507e5528bd88131dc742a2941176aceea1838bc09860c21f179285b7865133b 20251202/cpython-3.13.10+20251202-ppc64le-unknown-linux-gnu-install_only.tar.gz +ec411b4a2d167c3be0a9aeb3905e045d62c8e3c3db0caeade5d47d5f60b98dd0 20251202/cpython-3.13.10+20251202-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +70169e916860b2e5b34c37c302d699eb2b8f24f28090968881942a37aeb7ed08 20251202/cpython-3.13.10+20251202-riscv64-unknown-linux-gnu-install_only.tar.gz +4fc6443948bf5b729481ea02cc5c68e80cd0da42631f6936587a2b8fd45bc62c 20251202/cpython-3.13.10+20251202-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +c5448863b64aacae62f3a213a6e6cf94ec63f96ee4d518491cd62fd3c81d952f 20251202/cpython-3.13.10+20251202-s390x-unknown-linux-gnu-install_only.tar.gz +6ce608684df0f90350c7a1742e9685a7782d9b26ec99d1bd9d55c8cf9a405040 20251202/cpython-3.13.10+20251202-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +a02761a4f189f71c0512e88df7ca2843696d61da659e47f8a5c8a9bd2c0d16f4 20251202/cpython-3.13.10+20251202-x86_64-apple-darwin-install_only.tar.gz +6a8b0372ded655e0d55318089fbce3122a446e69bcd120c79aaadfe9b017299c 20251202/cpython-3.13.10+20251202-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +8b00014c7c35f9ad4cb1c565f067500bacc4125c8bc30e4389ee0be9fd6ffa3d 20251202/cpython-3.13.10+20251202-x86_64-pc-windows-msvc-install_only.tar.gz +e39127fbe8d2ae7d86099f18b4da0918f9b60ce73ed491774d6dcfaa42b5c9ae 20251202/cpython-3.13.10+20251202-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +0cac1495fff920219904b1d573aaec0df54d549c226cb45f5c60cb6d2c72727a 20251202/cpython-3.13.10+20251202-x86_64-unknown-linux-gnu-install_only.tar.gz +04108190972ac98e13098abd972ec3f4f8b0880f83c0bb68249ce1a6164fa041 20251202/cpython-3.13.10+20251202-x86_64-unknown-linux-musl-install_only.tar.gz +61f38e947449cf00f32f0838e813358f6bf61025d0797531e5b8b8b175c617f0 20251202/cpython-3.14.1+20251202-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +cdf1ba0789f529fa34bb5b5619c5da9757ac1067d6b8dd0ee8b78e50078fc561 20251202/cpython-3.14.1+20251202-aarch64-apple-darwin-install_only.tar.gz +ddb10b645de2b1f6f2832a80b115a9cd34a4a760249983027efe46618a8efc48 20251202/cpython-3.14.1+20251202-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +19129cf8b4d68c4e64c25bae43bca139d871267b59cf7f02b9dcf25f0bf59497 20251202/cpython-3.14.1+20251202-aarch64-pc-windows-msvc-install_only.tar.gz +1a88a1fe21eb443d280999464b1a397605a7ca950d8ab73813ca6868835439a2 20251202/cpython-3.14.1+20251202-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +5dde7dba0b8ef34c0d5cb8a721254b1e11028bfc09ff06664879c245fe8df73f 20251202/cpython-3.14.1+20251202-aarch64-unknown-linux-gnu-install_only.tar.gz +7207b736ed2569f307649ffd4b615a5346631bc244730b8702babee377cef528 20251202/cpython-3.14.1+20251202-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +d2774701d53e2ac06f8c8c8e52dfa4ff346890de9b417c9a7664195443a4c766 20251202/cpython-3.14.1+20251202-ppc64le-unknown-linux-gnu-install_only.tar.gz +d1356ccd279920edc31bf0350674d966beb9522f9503846ed7855dbb109ccc14 20251202/cpython-3.14.1+20251202-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +af840506efbcd5026d9140c0a0230e45e46bb1f339a65c10a22875930b2c0159 20251202/cpython-3.14.1+20251202-riscv64-unknown-linux-gnu-install_only.tar.gz +477758eabc06dbc7e5e5d16e97c4672478acd409f420dd2e1b84d3452c0668d1 20251202/cpython-3.14.1+20251202-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +43f8f79bf4c66689d2019f193671d1df3e5e5dbb293382036285e8ce55fc55bb 20251202/cpython-3.14.1+20251202-s390x-unknown-linux-gnu-install_only.tar.gz +c2cb2a9b44285fbc13c3c9b7eea813db6ed8d94909406b059db7afd39b32e786 20251202/cpython-3.14.1+20251202-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +f25ce050e1d370f9c05c9623b769ffa4b269a6ae17e611b435fd2b8b09972a88 20251202/cpython-3.14.1+20251202-x86_64-apple-darwin-install_only.tar.gz +8ef7048315cac6d26bdbef18512a87b1a24fffa21cec86e32f9a9425f2af9bf6 20251202/cpython-3.14.1+20251202-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +cb478a5a37eb93ce4d3c27ae64d211d6a5a42475ae53f666a8d1570e71fcf409 20251202/cpython-3.14.1+20251202-x86_64-pc-windows-msvc-install_only.tar.gz +c5d5b89aab7de683e465e36de2477a131435076badda775ef6e9ea21109c1c32 20251202/cpython-3.14.1+20251202-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +a72f313bad49846e5e9671af2be7476033a877c80831cf47f431400ccb520090 20251202/cpython-3.14.1+20251202-x86_64-unknown-linux-gnu-install_only.tar.gz +15d50b15713097c38c67b1a06a0498ad102377f9b3999e98e4eefd6bf91bd82d 20251202/cpython-3.14.1+20251202-x86_64-unknown-linux-musl-install_only.tar.gz +4213058b7fcd875596c12b58cd46a399358b0a87ecde4b349cbdd00cf87ed79a 20251209/cpython-3.13.11+20251209-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +295a9f7bc899ea1cc08baf60bbf511bdd1e4a29b2dd7e5f59b48f18bfa6bf585 20251209/cpython-3.13.11+20251209-aarch64-apple-darwin-install_only.tar.gz +6daf6d092c7294cfe68c4c7bf2698ac134235489c874b3bf796c7972b9dbba30 20251209/cpython-3.13.11+20251209-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +ba646d0c3b7dd7bdfb770d9b2ebd6cd2df02a37fda90c9c79a7cf59c7df6f165 20251209/cpython-3.13.11+20251209-aarch64-pc-windows-msvc-install_only.tar.gz +290ca3bd0007db9e551f90b08dfcb6c1b2d62c33b2fc3e9a43e77d385d94f569 20251209/cpython-3.13.11+20251209-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +ea1e678e6e82301bb32bf3917732125949b6e46d541504465972024a3f165343 20251209/cpython-3.13.11+20251209-aarch64-unknown-linux-gnu-install_only.tar.gz +09d4b50f8abb443f7e3af858c920aa61c2430b0954df465e861caa7078e55e69 20251209/cpython-3.13.11+20251209-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +7660e53aad9d35ee256913c6d98427f81f078699962035c5fa8b5c3138695109 20251209/cpython-3.13.11+20251209-ppc64le-unknown-linux-gnu-install_only.tar.gz +5406f2a7cacafbd2aac3ce2de066a0929aab55423824276c36e04cb83babc36c 20251209/cpython-3.13.11+20251209-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +763fa1548e6a432e9402916e690c74ea30f26dcd2e131893dd506f72b87c27c9 20251209/cpython-3.13.11+20251209-riscv64-unknown-linux-gnu-install_only.tar.gz +3984b67c4292892eaccdd1c094c7ec788884c4c9b3534ab6995f6be96d5ed51d 20251209/cpython-3.13.11+20251209-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +ffb6af51fbfabfc6fbc4e7379bdec70c2f51e972b1d2f45c053493b9da3a1bbe 20251209/cpython-3.13.11+20251209-s390x-unknown-linux-gnu-install_only.tar.gz +d6f489464045d6895ae68b0a04a9e16477e74fe3185a75f3a9a0af8ccd25eade 20251209/cpython-3.13.11+20251209-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +dac4a0a0a9b71f6b02a8b0886547fa22814474239bffb948e3e77185406ea136 20251209/cpython-3.13.11+20251209-x86_64-apple-darwin-install_only.tar.gz +bb9a29a7ba8f179273b79971da6aaa7be592d78c606a63f99eff3e4c12fb0fae 20251209/cpython-3.13.11+20251209-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +87822417007045a28a7eccc47fe67b8c61265b99b10dbbfa24d231a3622b1c27 20251209/cpython-3.13.11+20251209-x86_64-pc-windows-msvc-install_only.tar.gz +33f89c957d986d525529b8a980103735776f4d20cf52f55960a057c760188ac3 20251209/cpython-3.13.11+20251209-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +1ffa06d714a44aea14c0c54c30656413e5955a6c92074b4b3cb4351dcc28b63b 20251209/cpython-3.13.11+20251209-x86_64-unknown-linux-gnu-install_only.tar.gz +969fe24017380b987c4e3ce15e9edf82a4618c1e61672b2cc9b021a1c98eae78 20251209/cpython-3.13.11+20251209-x86_64-unknown-linux-musl-install_only.tar.gz +d6d17b8ef28326552cdeb2a7541c8a0cb711b378df9b93ebdb461dca065edfea 20251209/cpython-3.14.2+20251209-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +2f74bd26bd16487aca357c879d11f7b16c0521328e5148a1930ab6357bcb89fe 20251209/cpython-3.14.2+20251209-aarch64-apple-darwin-install_only.tar.gz +43aac5bb4cdba71fc6775d26f47348d573a0b1210911438be71d7d96f4b18b51 20251209/cpython-3.14.2+20251209-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +0be0d2557d73efa7f6f3f99679f05252d57fe2aad2d81cac3cad410a9b1eacbd 20251209/cpython-3.14.2+20251209-aarch64-pc-windows-msvc-install_only.tar.gz +adfcb90f3a7e1b3fbc6a99f9c8c8dce1f2e26ea72b724bbe4e9fa39e81e2b0db 20251209/cpython-3.14.2+20251209-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +869af31b2963194e8a2ecfadc36027c4c1c86a10f4960baec36dadb41b2acf02 20251209/cpython-3.14.2+20251209-aarch64-unknown-linux-gnu-install_only.tar.gz +2b1ce0c5a5f5e5add7e4f934f5bd35ac41660895a30b3098db7f7303d6952a4f 20251209/cpython-3.14.2+20251209-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +86129976403fb5d64cf576329f94148f28cf6f82834e94df81ff31e9d5f404e0 20251209/cpython-3.14.2+20251209-ppc64le-unknown-linux-gnu-install_only.tar.gz +4efb610fa07a6ee2639d14d78fc3b6ecb47431c14e1e4bda03c7f7dd60a5c1e5 20251209/cpython-3.14.2+20251209-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +318dceecf119ea903aef1fb03a552cc592ecd61c08da891b68f5755e21e13511 20251209/cpython-3.14.2+20251209-riscv64-unknown-linux-gnu-install_only.tar.gz +e62f3bb3e66dac6c459690f9e9cd8cc2f6fe1dcf8bfed452af4c3df24cd7874f 20251209/cpython-3.14.2+20251209-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +53875c849a14194344ead1d9cd1e128cadd42a4b83c35eeb212417909ef05a6a 20251209/cpython-3.14.2+20251209-s390x-unknown-linux-gnu-install_only.tar.gz +1fd76c79f7fc1753e8d2ed2f71406c0b65776c75f3e95ed99ffde8c95af2adc1 20251209/cpython-3.14.2+20251209-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +58fa3e17d13ab956fd11055fb774c98ecfddcdf3b588e5f2369bdbc14ef9d76a 20251209/cpython-3.14.2+20251209-x86_64-apple-darwin-install_only.tar.gz +9927951e3997c186d2813ca1a0f4a8f5a2f771463f7f8ad0752fd3d2be2b74e4 20251209/cpython-3.14.2+20251209-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +0d660bba9f58cb552e7e99e1f96a9c67b41618c9b8d29f9f3515fe2b5ad1966e 20251209/cpython-3.14.2+20251209-x86_64-pc-windows-msvc-install_only.tar.gz +3728872ffd74989a7b4bbf3f0c629ae8fe821cda2bd6544012c1b92b9f5d5a5b 20251209/cpython-3.14.2+20251209-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +121c3249bef497adf601df76a4d89aed6053fc5ec2f8c0ec656b86f0142e8ddd 20251209/cpython-3.14.2+20251209-x86_64-unknown-linux-gnu-install_only.tar.gz +71639cc5d1fb79840467531c5b53ca77170a58edd3f7e2d29330dd736e477469 20251209/cpython-3.14.2+20251209-x86_64-unknown-linux-musl-install_only.tar.gz +5b34488580df13df051a2e84e43cfca2ab28fdd7a61052f35988eb8b481b894a 20251209/cpython-3.15.0a2+20251209-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +5851f3744fbd39e3e323844cf4f68d7763fb25546aa5ffbb71b1b5ab69c56616 20251209/cpython-3.15.0a2+20251209-aarch64-apple-darwin-install_only.tar.gz +3d99152b4e29b947fb1cfc8d035d1d511e50aeed72886ff4a5fd0a3694bd0b51 20251209/cpython-3.15.0a2+20251209-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +39bc2fcac13aeba7d650f76badf63350a81c86167a62174cb092eab7a749f4a5 20251209/cpython-3.15.0a2+20251209-aarch64-pc-windows-msvc-install_only.tar.gz +0c2c83236f6e28c103e2660a82be94b2459ee8cfdd90f5dd82f0d503ca2aec09 20251209/cpython-3.15.0a2+20251209-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +17ba65d669be3052524e03b4d1426c072ef38df2a9065ff4525d1f4d1bc9f82c 20251209/cpython-3.15.0a2+20251209-aarch64-unknown-linux-gnu-install_only.tar.gz +216842df2377fd032f279ded7fd23d7bdbd92d4c1fa7619523bc0dbdef5bd212 20251209/cpython-3.15.0a2+20251209-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +5585bd7c5eefe28b9bf544d902cad9a2f81f33c618f2a1d3c006cbfcdec77abc 20251209/cpython-3.15.0a2+20251209-ppc64le-unknown-linux-gnu-install_only.tar.gz +2a8b56f318d2e21b01b54909554c53d81871b9bb05d23ea7808dde9acec4dc7e 20251209/cpython-3.15.0a2+20251209-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +bb7252edaffd422bd1c044a4764dfcf83a5d7159942f445abbef524e54ea79a0 20251209/cpython-3.15.0a2+20251209-riscv64-unknown-linux-gnu-install_only.tar.gz +06c4ca3983aad20723f68786e3663ab49fee1bf09326f341649205ed79d34fc6 20251209/cpython-3.15.0a2+20251209-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +03a90ffa9f92d4cf4caeefb9d15f0b39c05c1e60ade6688f32165f957db4f8f3 20251209/cpython-3.15.0a2+20251209-s390x-unknown-linux-gnu-install_only.tar.gz +4d8102b70ea9fe726ee3ae9ad9e9bc4cbe0b6ed18f7989c81aef81de578f0163 20251209/cpython-3.15.0a2+20251209-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +cee576de4919cd422dbc31eb85d3c145ee82acec84f651daaf32dc669b5149c9 20251209/cpython-3.15.0a2+20251209-x86_64-apple-darwin-install_only.tar.gz +6ff71bac78d650ce621fe6db49f06290e48bcceb61f69cccc7728584f70b6346 20251209/cpython-3.15.0a2+20251209-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +e538475ee249eacf63bfdae0e70af73e9c47360e6dd3d6825e7a35107e177de5 20251209/cpython-3.15.0a2+20251209-x86_64-pc-windows-msvc-install_only.tar.gz +70f552e213734c0e260a57603bee504dd7ed0e78a10558b591e724ea8730fef5 20251209/cpython-3.15.0a2+20251209-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +58addaabfab2de422180d32543fb3878ffc984c8a2e4005ff658a5cd83b31fc7 20251209/cpython-3.15.0a2+20251209-x86_64-unknown-linux-gnu-install_only.tar.gz +dcf844400dc2e7f5f3604e994532e4d49db45f4deefe9afdf6809ca1bc6532ee 20251209/cpython-3.15.0a2+20251209-x86_64-unknown-linux-musl-install_only.tar.gz +e7cf7bc717082bb38f5ca75988ecd8e5dbc1b0535192129371e30235d29d67b5 20260325/cpython-3.13.12+20260325-aarch64-apple-darwin-freethreaded-install_only.tar.gz +688da81bcaa6ed91792397c7d5433b13a4f02f021f940637c3972639bc516dca 20260325/cpython-3.13.12+20260325-aarch64-apple-darwin-install_only.tar.gz +54187be504ea5be2f8ed455e9377112bb04f34c9259eae263779e56b403e3e3f 20260325/cpython-3.13.12+20260325-aarch64-pc-windows-msvc-freethreaded-install_only.tar.gz +d2c8b00044cd2e4c5fc7e697e63d5e481ed44b87c2def0beb42991d59f65d930 20260325/cpython-3.13.12+20260325-aarch64-pc-windows-msvc-install_only.tar.gz +9794866e9a464f349055d791ea8f14dfa7f339ecac5aa9b1084bb2ce388fc598 20260325/cpython-3.13.12+20260325-aarch64-unknown-linux-gnu-freethreaded-install_only.tar.gz +31c6e61eed48ca4e156d0e473025a792338641109e8277a63518ded438390c96 20260325/cpython-3.13.12+20260325-aarch64-unknown-linux-gnu-install_only.tar.gz +1a8a4a97f33740a1cb9fa480321818cdc610c79c9137e511e76dc53635615494 20260325/cpython-3.13.12+20260325-ppc64le-unknown-linux-gnu-freethreaded-install_only.tar.gz +654939bc40d5f76f08eb17335bb19e9efa11eb48a0818eda2293a3f7c3570ae7 20260325/cpython-3.13.12+20260325-ppc64le-unknown-linux-gnu-install_only.tar.gz +178d20e568c25abcca9b1dbedf77e904cc3f10a79d22e31f87ddabd2d28f87dc 20260325/cpython-3.13.12+20260325-riscv64-unknown-linux-gnu-freethreaded-install_only.tar.gz +fc7e1fb553c47b831ed7fa529575145207f000f967513f7b9ea809cce006ed79 20260325/cpython-3.13.12+20260325-riscv64-unknown-linux-gnu-install_only.tar.gz +d23c93ea7502420c71e4acf02999c72ab80797d51843b1b6a315ca7bac3cb780 20260325/cpython-3.13.12+20260325-s390x-unknown-linux-gnu-freethreaded-install_only.tar.gz +7d7919358e88fcc672b061be8c2316c3a604c7074200515d7104166ed611f7f9 20260325/cpython-3.13.12+20260325-s390x-unknown-linux-gnu-install_only.tar.gz +6aff211689e30889cfe90b0b2a76b6f5a7b9e6e0bb28d6a66fd5ba35d36dc78a 20260325/cpython-3.13.12+20260325-x86_64-apple-darwin-freethreaded-install_only.tar.gz +7411e47939783708381017a90944a69641ac84d43f74fb6e2d52576c599a2717 20260325/cpython-3.13.12+20260325-x86_64-apple-darwin-install_only.tar.gz +088754e90ff22962a4ab6f7cb6bdabe5d9e7618266595df2cf7b211766e15132 20260325/cpython-3.13.12+20260325-x86_64-pc-windows-msvc-freethreaded-install_only.tar.gz +5b4093f92d9bffcb0d92aea050f3d77d5a4fc8e918b31cea000ee4b3ca751f1d 20260325/cpython-3.13.12+20260325-x86_64-pc-windows-msvc-install_only.tar.gz +6070796c894ef0a25b5a944c8c0327e155df534302e1612a5ddd57d177ddadf7 20260325/cpython-3.13.12+20260325-x86_64-unknown-linux-gnu-freethreaded-install_only.tar.gz +ebb1051ca2822b9803f46a5f10b6d51d153189ef1b1f1e142f733c0cbeaf86eb 20260325/cpython-3.13.12+20260325-x86_64-unknown-linux-gnu-install_only.tar.gz +b2e9400731c7f18069ec2804ba87a404385fe440f93b7dcb59004b9f56651202 20260325/cpython-3.13.12+20260325-x86_64-unknown-linux-musl-install_only.tar.gz +21f297bc1e0503fa077364417e2213c60951d94fd65d837ae6d9d9201ae27483 20260325/cpython-3.14.3+20260325-aarch64-apple-darwin-freethreaded-install_only.tar.gz +80c996c23aab828134821f078a8a77a6f33f3f2c14000f071718c540e20c64d4 20260325/cpython-3.14.3+20260325-aarch64-apple-darwin-install_only.tar.gz +d0e355df7362d12542108f78b3f8085b21e6824420769117c262ac86569bb2a7 20260325/cpython-3.14.3+20260325-aarch64-pc-windows-msvc-freethreaded-install_only.tar.gz +b35fe7c2fe169574f382cef125e95cbd904ddcb98fc337167356371b6d2e8c60 20260325/cpython-3.14.3+20260325-aarch64-pc-windows-msvc-install_only.tar.gz +112cf42bdf4d04f69ff4f9bf18c8ce45f494bac1645310bfdeff6f2ffb30dd9a 20260325/cpython-3.14.3+20260325-aarch64-unknown-linux-gnu-freethreaded-install_only.tar.gz +6faf5478f910741c477830f5fd842011208af0f9678faf77106c9421b325bfc1 20260325/cpython-3.14.3+20260325-aarch64-unknown-linux-gnu-install_only.tar.gz +9d7e5ba8020fd942a89a57179d9015eb0237c2d95cdbf8378639723663f11706 20260325/cpython-3.14.3+20260325-ppc64le-unknown-linux-gnu-freethreaded-install_only.tar.gz +5eafe32e12f33f98c40de920482b013170dcf97d8c7f5dc780271ccf4cded76a 20260325/cpython-3.14.3+20260325-ppc64le-unknown-linux-gnu-install_only.tar.gz +32955ad52ec7931e76f4509134a2ba5a6ba6ea0cd55e05217c1ccca3967c4a5c 20260325/cpython-3.14.3+20260325-riscv64-unknown-linux-gnu-freethreaded-install_only.tar.gz +481d3faef258964e57b7102c63de12b2bb388c7ed07cfe456f33e63b4e061202 20260325/cpython-3.14.3+20260325-riscv64-unknown-linux-gnu-install_only.tar.gz +7a1d36a1567cd747411c9c2bc7e2b5c1ac277ea7c734f74b158b94101fd5ea43 20260325/cpython-3.14.3+20260325-s390x-unknown-linux-gnu-freethreaded-install_only.tar.gz +d706eae2f4d963187b7c866603aed75d7eb3ea59590b06fb34f5fd7d0fe8e432 20260325/cpython-3.14.3+20260325-s390x-unknown-linux-gnu-install_only.tar.gz +3788781d0f9704f91ab5f7ad2a040d26b0f9b6aba0a2535db21755aebb69e620 20260325/cpython-3.14.3+20260325-x86_64-apple-darwin-freethreaded-install_only.tar.gz +847a49fea36c066f8df7a57cd8c4c02d17667e25d30b7930e8f8ba15e72d7efc 20260325/cpython-3.14.3+20260325-x86_64-apple-darwin-install_only.tar.gz +99dd7e425b3dac23e03f37787d77ee0af531e96b1c748275185342bc6642eb6b 20260325/cpython-3.14.3+20260325-x86_64-pc-windows-msvc-freethreaded-install_only.tar.gz +8b4e1329c4901ce2c0f1c20ac5d2ffa62fc13f12e26b5d1e5a1000f910f980d4 20260325/cpython-3.14.3+20260325-x86_64-pc-windows-msvc-install_only.tar.gz +20d3bcd7f175e09fa08f4cb3039e5f90fe7e4ce2476534e83f5aa21eb0d7cee9 20260325/cpython-3.14.3+20260325-x86_64-unknown-linux-gnu-freethreaded-install_only.tar.gz +18270c5a7b1a572599df5e68b497ba5254811dac43ba6f542245807d821fcb44 20260325/cpython-3.14.3+20260325-x86_64-unknown-linux-gnu-install_only.tar.gz +726a28734d2878a637b0d16ce07ce24c7d6ca1043d8e6f4a23b1b0a3478eedb9 20260325/cpython-3.14.3+20260325-x86_64-unknown-linux-musl-install_only.tar.gz +f76cc83c7db16cfc8794bf6e44d834152b57d8bab4e04e823cbc59ed23ec22f8 20260414/cpython-3.10.20+20260414-aarch64-apple-darwin-install_only.tar.gz +64932c8e8bbdf9d6b66ee85934f6f8ad1d18218b51a87ea06cefd3b84554a3e4 20260414/cpython-3.10.20+20260414-aarch64-unknown-linux-gnu-install_only.tar.gz +76b48eb26ef274045772186e63431419294c41baf6d5a372b722d4c9e711082e 20260414/cpython-3.10.20+20260414-ppc64le-unknown-linux-gnu-install_only.tar.gz +76e1ec72717d17493976fc176ec661f02412666d4f19e50908d8e4303c0511d5 20260414/cpython-3.10.20+20260414-riscv64-unknown-linux-gnu-install_only.tar.gz +2edf241199d11a3ef79a312737c1bcdb86908352c585ca14b667539080630e85 20260414/cpython-3.10.20+20260414-s390x-unknown-linux-gnu-install_only.tar.gz +95a2d794b8981723095190fa94b574ceb4272bb49d83b9e418bb90341e304d09 20260414/cpython-3.10.20+20260414-x86_64-apple-darwin-install_only.tar.gz +0d828683d30185ab9f1110ad2194ef384cef0533b8e0da7e03ce837548841788 20260414/cpython-3.10.20+20260414-x86_64-pc-windows-msvc-install_only.tar.gz +303047011b2c9f58504a930fc974d84547477cf69a3f2962f25552e2395c13af 20260414/cpython-3.10.20+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +84eb198d318f8b1b8bf59eef5d30d742e13afd97c213fa229578f8fdab0c406f 20260414/cpython-3.10.20+20260414-x86_64-unknown-linux-musl-install_only.tar.gz +a57ffd435652092d16b30e783f9826c55e9c64b0f0a72cbae0a9f39e663137fb 20260414/cpython-3.11.15+20260414-aarch64-apple-darwin-install_only.tar.gz +a882abe4876985c9dc3d433420548506fb0cc9bb9d9fe336a2d3aaf28922aa45 20260414/cpython-3.11.15+20260414-aarch64-pc-windows-msvc-install_only.tar.gz +77836944ae15b74e0b25bdc68a4703a340f2ccb684effc0f45fbd7910e1a1f39 20260414/cpython-3.11.15+20260414-aarch64-unknown-linux-gnu-install_only.tar.gz +30a2107f000dbe304820627cbe2cc257027c20f3241d96e6c7df796b69ac2062 20260414/cpython-3.11.15+20260414-ppc64le-unknown-linux-gnu-install_only.tar.gz +373b98fbf2d04099139a2f6be57593714382ed790be7e7419e358830c23ddd0f 20260414/cpython-3.11.15+20260414-riscv64-unknown-linux-gnu-install_only.tar.gz +7838efa839158c80568de35ac78d438f564f4c32272a2fe7d9e14a9b351d1a62 20260414/cpython-3.11.15+20260414-s390x-unknown-linux-gnu-install_only.tar.gz +317055d80e553764feeaef432d833dd8385c14b83465a8b3fa7c2b7819cba681 20260414/cpython-3.11.15+20260414-x86_64-apple-darwin-install_only.tar.gz +8e69ecf1d9fc194e029aafa608d483bf24ccaa8f56d456d7009f20462d62ad23 20260414/cpython-3.11.15+20260414-x86_64-pc-windows-msvc-install_only.tar.gz +8b14030dd3af9ea7f7c51b4c90feb04afd8a8f45435727e67b875270bd08f3bc 20260414/cpython-3.11.15+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +ca92d3a68a39fa330498b09714733f347bead7313ba9d9b7fbed837aa4ba7796 20260414/cpython-3.11.15+20260414-x86_64-unknown-linux-musl-install_only.tar.gz +8966b2bcd9fa03ba22c080ad15a86bc12e41a00122b16f4b3740e302261124d9 20260414/cpython-3.12.13+20260414-aarch64-apple-darwin-install_only.tar.gz +f55326c894fde76fc0faffe95d2bce60be533c88a8c44c1b88bbbc17bf6a5cd5 20260414/cpython-3.12.13+20260414-aarch64-pc-windows-msvc-install_only.tar.gz +355d981eafb9b2870af79ddc106ced7266b6f6d2101d8fbcb05620fa386642b9 20260414/cpython-3.12.13+20260414-aarch64-unknown-linux-gnu-install_only.tar.gz +4aef4cffe73c4a65ea486f14d684a9ad3f831a354174d163bb531b5baa70fc49 20260414/cpython-3.12.13+20260414-ppc64le-unknown-linux-gnu-install_only.tar.gz +c2629d69324155132343913f064be93509bd162531e08a292e50c3973ec8b5db 20260414/cpython-3.12.13+20260414-riscv64-unknown-linux-gnu-install_only.tar.gz +e5baafd64180f45165d2751b25d1bcc89254eefc7926f3ab341fc61b541d7606 20260414/cpython-3.12.13+20260414-s390x-unknown-linux-gnu-install_only.tar.gz +801b03fbe004181d55a02ebd8b4e04d74973e70d716062aebe3b3cf32e9be297 20260414/cpython-3.12.13+20260414-x86_64-apple-darwin-install_only.tar.gz +c5a9e011e284c49c48106ca177342f3e3f64e95b4c6652d4a382cc7c9bb1cc46 20260414/cpython-3.12.13+20260414-x86_64-pc-windows-msvc-install_only.tar.gz +cdcf8724d46e4857f8db5ee9f4252dc2f5da34f7940294ec6b312389dd3f41e0 20260414/cpython-3.12.13+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +d10e971238c130fdf25e577c6538a3effa5589d5fcf53665e3c711edd6a6ff2f 20260414/cpython-3.12.13+20260414-x86_64-unknown-linux-musl-install_only.tar.gz +2662b1c3f6d5ed4d02d877c07f9384acc0d18b9046d54cd2853dad3ca172784f 20260414/cpython-3.13.13+20260414-aarch64-apple-darwin-freethreaded-install_only.tar.gz +c652dad552122cd2e76968ec41c803f8222038169b11310dba0c85928265f5c1 20260414/cpython-3.13.13+20260414-aarch64-apple-darwin-install_only.tar.gz +c6c1aae3809ef585271f6f1bb3643a2c6e0c82b811b93284c6218b31f0b931d7 20260414/cpython-3.13.13+20260414-aarch64-pc-windows-msvc-freethreaded-install_only.tar.gz +586ba71c75f341e1d111399b7f719ae784dc11e8672e93e017388f28684226d0 20260414/cpython-3.13.13+20260414-aarch64-pc-windows-msvc-install_only.tar.gz +46ac7e9476b938ef19f71029a77d28ed1e201335dd0aa0237fcfed2e5ce0ee61 20260414/cpython-3.13.13+20260414-aarch64-unknown-linux-gnu-freethreaded-install_only.tar.gz +6a65f68043d7fadcd580415493d2929d1fd686013f9ae44ddbd3a81307ab256d 20260414/cpython-3.13.13+20260414-aarch64-unknown-linux-gnu-install_only.tar.gz +abe26a6cab523a5d00d75f1353cbad9c5dc04262dcb0dc4a2b47d02384e2a7d7 20260414/cpython-3.13.13+20260414-ppc64le-unknown-linux-gnu-freethreaded-install_only.tar.gz +aef73894107300264222b19e357baf5bad616b1c4bf5daa5c3b97cfee8f5ed7b 20260414/cpython-3.13.13+20260414-ppc64le-unknown-linux-gnu-install_only.tar.gz +eea71fc3625fcc2408171b17fb97e0c6286ed60ed225ca7fd6e2fc5d9cc21dce 20260414/cpython-3.13.13+20260414-riscv64-unknown-linux-gnu-freethreaded-install_only.tar.gz +f47c09f8e7f2fb0bc4afe52422705af4016c8d3ec1cf004b67bb56a86caa62cb 20260414/cpython-3.13.13+20260414-riscv64-unknown-linux-gnu-install_only.tar.gz +dd8b6161c4af3c2f5f29b3535decdcf146ce90d7a062687c9e5229b4151198b0 20260414/cpython-3.13.13+20260414-s390x-unknown-linux-gnu-freethreaded-install_only.tar.gz +4d205af9654e1f33cefd23ff798af470e565f3ac0eba18d2f98f18a2abd07166 20260414/cpython-3.13.13+20260414-s390x-unknown-linux-gnu-install_only.tar.gz +27edbaad8f0c1a8814647d24df3f87eb13c89bbc2cb90e2fc23d8fa48dd64b15 20260414/cpython-3.13.13+20260414-x86_64-apple-darwin-freethreaded-install_only.tar.gz +540337412d2c4220e99280f741dbf45c1e3da3a39edaaab20c6ba1d53e1692ef 20260414/cpython-3.13.13+20260414-x86_64-apple-darwin-install_only.tar.gz +002c07103bfbe1b889f41eb1b9fade81651a21aed35a3512e2a916c5d7903cfe 20260414/cpython-3.13.13+20260414-x86_64-pc-windows-msvc-freethreaded-install_only.tar.gz +ee0cb26453d6e025d36502d765c1639c34830355e46ab3ad31c0360bc4cd9b79 20260414/cpython-3.13.13+20260414-x86_64-pc-windows-msvc-install_only.tar.gz +a183ec7a10c38ab8c3f19968614f1e69ec697199e94525583662dfbc22b70d9a 20260414/cpython-3.13.13+20260414-x86_64-unknown-linux-gnu-freethreaded-install_only.tar.gz +e5ec3b2c5693215d153c434ac018e75511b2c4f96d2bce30468a477cb3a89d5e 20260414/cpython-3.13.13+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +24ac6bf80dd2991c8be348f777c96c6eb69b71e78d8fa28c09beb3ddca015a47 20260414/cpython-3.13.13+20260414-x86_64-unknown-linux-musl-install_only.tar.gz +a4bfd77675740a0362c137b094f3cd9995775e8e6c0a7874a095dd055fd1ea99 20260414/cpython-3.14.4+20260414-aarch64-apple-darwin-freethreaded-install_only.tar.gz +8b7865e511b17093e090449bf71eb52933c17d45ad5257ddeacaffbb2c7239df 20260414/cpython-3.14.4+20260414-aarch64-apple-darwin-install_only.tar.gz +0458cb9885c30df690cdf304a16ec335cbc7344792ef0e8a904614b24a61316d 20260414/cpython-3.14.4+20260414-aarch64-pc-windows-msvc-freethreaded-install_only.tar.gz +82613380d582d806e562d7701496c34c87753ab13c37aa0afe2039003651f389 20260414/cpython-3.14.4+20260414-aarch64-pc-windows-msvc-install_only.tar.gz +6d84fb153ccb5cb650652aadc490d99881a8d9b68cf273d44cb553e8cd087734 20260414/cpython-3.14.4+20260414-aarch64-unknown-linux-gnu-freethreaded-install_only.tar.gz +5c8db1c21023316adad827a46d917bbbd6a85ae4e39bc3a58febda712c2f963d 20260414/cpython-3.14.4+20260414-aarch64-unknown-linux-gnu-install_only.tar.gz +b5e025e340d0faa1772ef234e320401b0aa5cf6c9d16ed63a8c44be7c531bc58 20260414/cpython-3.14.4+20260414-ppc64le-unknown-linux-gnu-freethreaded-install_only.tar.gz +055977a09de092744bbb22db64144e6afef8592eaac5e2bce4cca33f2592281a 20260414/cpython-3.14.4+20260414-ppc64le-unknown-linux-gnu-install_only.tar.gz +4373553133eb4712bc10f720da29e091a23153f587fdb2c38f1fb105e70db53a 20260414/cpython-3.14.4+20260414-riscv64-unknown-linux-gnu-freethreaded-install_only.tar.gz +e959df167c502fb0bbcacc31a997e25c6b0ff6b5e496321b691955aa702d0c09 20260414/cpython-3.14.4+20260414-riscv64-unknown-linux-gnu-install_only.tar.gz +a6797ad05c7d7f74a2cea28bf012f9199f4d6c1ed6d09f7adfeb9b3c538c6258 20260414/cpython-3.14.4+20260414-s390x-unknown-linux-gnu-freethreaded-install_only.tar.gz +35f70ad05b2c4045889ee0c3d93f61b012654c1d91e10e671f0e5b4d4a6c6637 20260414/cpython-3.14.4+20260414-s390x-unknown-linux-gnu-install_only.tar.gz +1c366767d203b722efbd5b3796d16a08436e8a328afd31e551289efba9bf56d1 20260414/cpython-3.14.4+20260414-x86_64-apple-darwin-freethreaded-install_only.tar.gz +9ecb2b942e6698c04af10a63a3d73c0b2e8d8e11ce44933fbffe8651bef4577d 20260414/cpython-3.14.4+20260414-x86_64-apple-darwin-install_only.tar.gz +5ccaecdb899431f393209647182def14b36d7398bd59be4fa73dd79b48b3f290 20260414/cpython-3.14.4+20260414-x86_64-pc-windows-msvc-freethreaded-install_only.tar.gz +9647bb46d3c236e34c1c11bbb7113444d9711811f0d11c39956168807a955b1a 20260414/cpython-3.14.4+20260414-x86_64-pc-windows-msvc-install_only.tar.gz +c1a845a79da56265dc49628bc3b9e20d34f04674fd2d637ee40cbe259d2b1b95 20260414/cpython-3.14.4+20260414-x86_64-unknown-linux-gnu-freethreaded-install_only.tar.gz +e17275eaf95ceb5877aa6816e209b7733f41fee401d39c3921b88fb73fc4a4ba 20260414/cpython-3.14.4+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +12687a989a2384665577e1ef9864f33d4c074a1e69b38a8bac8d656531aefa3e 20260414/cpython-3.14.4+20260414-x86_64-unknown-linux-musl-install_only.tar.gz +5791a69a73b76b908f5bdf96da1928de8db696ab198f4ced04b77b22fe712ce0 20260414/cpython-3.15.0a8+20260414-aarch64-apple-darwin-freethreaded-install_only.tar.gz +780d46b3da0e58e15c620d9e7dfd29b54c8359c195f625858f85df9c2c7ecc32 20260414/cpython-3.15.0a8+20260414-aarch64-apple-darwin-install_only.tar.gz +95ddfe7dd52185f7e5d55524eafb48e54d1eab0b0cf013966f144a411f3ddd0f 20260414/cpython-3.15.0a8+20260414-aarch64-pc-windows-msvc-freethreaded-install_only.tar.gz +10fb470e900e65df4e37f8deaf1726397c914861ffc37b43ae3743a7eee88377 20260414/cpython-3.15.0a8+20260414-aarch64-pc-windows-msvc-install_only.tar.gz +b72908bce86036a0a1ba98ca9917ea0b99dc1e6c5d715d3d463c4f330880c09b 20260414/cpython-3.15.0a8+20260414-aarch64-unknown-linux-gnu-freethreaded-install_only.tar.gz +8f6dda4d8ff44976f1aa6a94674a09a503dc50b015297e1b62c8cdc591c90f4f 20260414/cpython-3.15.0a8+20260414-aarch64-unknown-linux-gnu-install_only.tar.gz +b3c8210674140a4c5beefa2d4afd752979222638a0fb68de672c60300b4a6642 20260414/cpython-3.15.0a8+20260414-ppc64le-unknown-linux-gnu-freethreaded-install_only.tar.gz +09f076c63fadbf675143674aa3b23229482b9a44840b8b1808a216def2a9af15 20260414/cpython-3.15.0a8+20260414-ppc64le-unknown-linux-gnu-install_only.tar.gz +1a4984207974563c6aea7dc934579d058dbac7436642081113e86011114b9fdf 20260414/cpython-3.15.0a8+20260414-riscv64-unknown-linux-gnu-freethreaded-install_only.tar.gz +9d41ce752e8b731872f0f5c9c48199e63c789d24ce3ae9e91d6c8008f36e7c51 20260414/cpython-3.15.0a8+20260414-riscv64-unknown-linux-gnu-install_only.tar.gz +f525a6244d73450e0c0a7ba125b5934894ab25ee171f7099c239d4eb7ce2f5f2 20260414/cpython-3.15.0a8+20260414-s390x-unknown-linux-gnu-freethreaded-install_only.tar.gz +1de2593c40cce2d8ea883f8c8580223bfa1478cbd9d0191ba3640aed083c2202 20260414/cpython-3.15.0a8+20260414-s390x-unknown-linux-gnu-install_only.tar.gz +3dcee23c21e4a3518947e988e115c1d824f07540f4326d93d4ea2028918e0193 20260414/cpython-3.15.0a8+20260414-x86_64-apple-darwin-freethreaded-install_only.tar.gz +a7744d34148969a2ec010da6f0a46ddeceda7c02e5cdfa2b4e1811487381491a 20260414/cpython-3.15.0a8+20260414-x86_64-apple-darwin-install_only.tar.gz +6e69670347e3a6ac1d0cd89b9506d825bd2f2690cc51ead5dec61aec6857d08d 20260414/cpython-3.15.0a8+20260414-x86_64-pc-windows-msvc-freethreaded-install_only.tar.gz +3ded476f676fdf260d56a5e49aa083d5ffd218fc3390e4480ed42bee1acfb3fb 20260414/cpython-3.15.0a8+20260414-x86_64-pc-windows-msvc-install_only.tar.gz +2d06d97e230b7f74de0fe4f661918a0ee827b08127b9372e0890e167de52a8c6 20260414/cpython-3.15.0a8+20260414-x86_64-unknown-linux-gnu-freethreaded-install_only.tar.gz +c93f4b15287ac48d7e3a475b245cb59cc51079382747e3e6213d6406c158969d 20260414/cpython-3.15.0a8+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +0568e953f837f09689eb4dd1af0043ba5e2ebae0c6395b8b9f8344a53b1f1da5 20260414/cpython-3.15.0a8+20260414-x86_64-unknown-linux-musl-freethreaded-install_only.tar.gz +9fbd6f243a424d4ae973e72aa0075122a7cfe05ac8f6cfde986e7b00d0dbc0bf 20260414/cpython-3.15.0a8+20260414-x86_64-unknown-linux-musl-install_only.tar.gz diff --git a/python/private/runtimes_manifest_workspace.bzl b/python/private/runtimes_manifest_workspace.bzl new file mode 100644 index 0000000000..c2da00ac46 --- /dev/null +++ b/python/private/runtimes_manifest_workspace.bzl @@ -0,0 +1,633 @@ +"""Manifest of runtimes for workspace mode builds. + +This is the workspace equivalent of runtimes_manifest.txt. It's a bzl file +to simplify loading of the data under workspace mode, which doesn't +support parsing a runtimes_manifest.txt file. + +NOTE: This file is automatically generated by sync_runtimes_manifest_workspace.py. +Do not edit directly! +""" + +MANIFEST_TEXT = """ +# Manifest of runtimes to make available +# Originally generated circa 2026-06 from the runtimes in TOOL_VERSIONS from the original python/versions.bzl +# To sort this file, execute: ./python/private/tools/sort_manifest.py python/private/runtimes_manifest.txt + +1409acd9a506e2d1d3b65c1488db4e40d8f19d09a7df099667c87a506f71c0ef 20220227/cpython-3.10.2+20220227-aarch64-apple-darwin-install_only.tar.gz +8f351a8cc348bb45c0f95b8634c8345ec6e749e483384188ad865b7428342703 20220227/cpython-3.10.2+20220227-aarch64-unknown-linux-gnu-install_only.tar.gz +8146ad4390710ec69b316a5649912df0247d35f4a42e2aa9615bffd87b3e235a 20220227/cpython-3.10.2+20220227-x86_64-apple-darwin-install_only.tar.gz +a1d9a594cd3103baa24937ad9150c1a389544b4350e859200b3e5c036ac352bd 20220227/cpython-3.10.2+20220227-x86_64-pc-windows-msvc-shared-install_only.tar.gz +9b64eca2a94f7aff9409ad70bdaa7fbbf8148692662e764401883957943620dd 20220227/cpython-3.10.2+20220227-x86_64-unknown-linux-gnu-install_only.tar.gz +2c99983d1e83e4b6e7411ed9334019f193fba626344a50c36fba6c25d4de78a2 20220502/cpython-3.10.4+20220502-aarch64-apple-darwin-install_only.tar.gz +d8098c0c54546637e7516f93b13403b11f9db285def8d7abd825c31407a13d7e 20220502/cpython-3.10.4+20220502-aarch64-unknown-linux-gnu-install_only.tar.gz +f2711eaffff3477826a401d09a013c6802f11c04c63ab3686aa72664f1216a05 20220502/cpython-3.10.4+20220502-x86_64-apple-darwin-install_only.tar.gz +bee24a3a5c83325215521d261d73a5207ab7060ef3481f76f69b4366744eb81d 20220502/cpython-3.10.4+20220502-x86_64-pc-windows-msvc-shared-install_only.tar.gz +f6f871e53a7b1469c13f9bd7920ad98c4589e549acad8e5a1e14760fff3dd5c9 20220502/cpython-3.10.4+20220502-x86_64-unknown-linux-gnu-install_only.tar.gz +efaf66acdb9a4eb33d57702607d2e667b1a319d58c167a43c96896b97419b8b7 20220802/cpython-3.10.6+20220802-aarch64-apple-darwin-install_only.tar.gz +81625f5c97f61e2e3d7e9f62c484b1aa5311f21bd6545451714b949a29da5435 20220802/cpython-3.10.6+20220802-aarch64-unknown-linux-gnu-install_only.tar.gz +7718411adf3ea1480f3f018a643eb0550282aefe39e5ecb3f363a4a566a9398c 20220802/cpython-3.10.6+20220802-x86_64-apple-darwin-install_only.tar.gz +91889a7dbdceea585ff4d3b7856a6bb8f8a4eca83a0ff52a73542c2e67220eaa 20220802/cpython-3.10.6+20220802-x86_64-pc-windows-msvc-shared-install_only.tar.gz +55aa2190d28dcfdf414d96dc5dcea9fe048fadcd583dc3981fec020869826111 20220802/cpython-3.10.6+20220802-x86_64-unknown-linux-gnu-install_only.tar.gz +d52b03817bd245d28e0a8b2f715716cd0fcd112820ccff745636932c76afa20a 20221106/cpython-3.10.8+20221106-aarch64-apple-darwin-install_only.tar.gz +33170bef18c811906b738be530f934640491b065bf16c4d276c6515321918132 20221106/cpython-3.10.8+20221106-aarch64-unknown-linux-gnu-install_only.tar.gz +525b79c7ce5de90ab66bd07b0ac1008bafa147ddc8a41bef15ffb7c9c1e9e7c5 20221106/cpython-3.10.8+20221106-x86_64-apple-darwin-install_only.tar.gz +f2b6d2f77118f06dd2ca04dae1175e44aaa5077a5ed8ddc63333c15347182bfe 20221106/cpython-3.10.8+20221106-x86_64-pc-windows-msvc-shared-install_only.tar.gz +6c8db44ae0e18e320320bbaaafd2d69cde8bfea171ae2d651b7993d1396260b7 20221106/cpython-3.10.8+20221106-x86_64-unknown-linux-gnu-install_only.tar.gz +018d05a779b2de7a476f3b3ff2d10f503d69d14efcedd0774e6dab8c22ef84ff 20230116/cpython-3.10.9+20230116-aarch64-apple-darwin-install_only.tar.gz +2003750f40cd09d4bf7a850342613992f8d9454f03b3c067989911fb37e7a4d1 20230116/cpython-3.10.9+20230116-aarch64-unknown-linux-gnu-install_only.tar.gz +0e685f98dce0e5bc8da93c7081f4e6c10219792e223e4b5886730fd73a7ba4c6 20230116/cpython-3.10.9+20230116-x86_64-apple-darwin-install_only.tar.gz +59c6970cecb357dc1d8554bd0540eb81ee7f6d16a07acf3d14ed294ece02c035 20230116/cpython-3.10.9+20230116-x86_64-pc-windows-msvc-shared-install_only.tar.gz +d196347aeb701a53fe2bb2b095abec38d27d0fa0443f8a1c2023a1bed6e18cdf 20230116/cpython-3.10.9+20230116-x86_64-unknown-linux-gnu-install_only.tar.gz +4918cdf1cab742a90f85318f88b8122aeaa2d04705803c7b6e78e81a3dd40f80 20230116/cpython-3.11.1+20230116-aarch64-apple-darwin-install_only.tar.gz +debf15783bdcb5530504f533d33fda75a7b905cec5361ae8f33da5ba6599f8b4 20230116/cpython-3.11.1+20230116-aarch64-unknown-linux-gnu-install_only.tar.gz +20a4203d069dc9b710f70b09e7da2ce6f473d6b1110f9535fb6f4c469ed54733 20230116/cpython-3.11.1+20230116-x86_64-apple-darwin-install_only.tar.gz +edc08979cb0666a597466176511529c049a6f0bba8adf70df441708f766de5bf 20230116/cpython-3.11.1+20230116-x86_64-pc-windows-msvc-shared-install_only.tar.gz +02a551fefab3750effd0e156c25446547c238688a32fabde2995c941c03a6423 20230116/cpython-3.11.1+20230116-x86_64-unknown-linux-gnu-install_only.tar.gz +8348bc3c2311f94ec63751fb71bd0108174be1c4def002773cf519ee1506f96f 20230507/cpython-3.10.11+20230507-aarch64-apple-darwin-install_only.tar.gz +c7573fdb00239f86b22ea0e8e926ca881d24fde5e5890851339911d76110bc35 20230507/cpython-3.10.11+20230507-aarch64-unknown-linux-gnu-install_only.tar.gz +73a9d4c89ed51be39dd2de4e235078281087283e9fdedef65bec02f503e906ee 20230507/cpython-3.10.11+20230507-ppc64le-unknown-linux-gnu-install_only.tar.gz +bd3fc6e4da6f4033ebf19d66704e73b0804c22641ddae10bbe347c48f82374ad 20230507/cpython-3.10.11+20230507-x86_64-apple-darwin-install_only.tar.gz +9c2d3604a06fcd422289df73015cd00e7271d90de28d2c910f0e2309a7f73a68 20230507/cpython-3.10.11+20230507-x86_64-pc-windows-msvc-shared-install_only.tar.gz +c5bcaac91bc80bfc29cf510669ecad12d506035ecb3ad85ef213416d54aecd79 20230507/cpython-3.10.11+20230507-x86_64-unknown-linux-gnu-install_only.tar.gz +09e412506a8d63edbb6901742b54da9aa7faf120b8dbdce56c57b303fc892c86 20230507/cpython-3.11.3+20230507-aarch64-apple-darwin-install_only.tar.gz +8190accbbbbcf7620f1ff6d668e4dd090c639665d11188ce864b62554d40e5ab 20230507/cpython-3.11.3+20230507-aarch64-unknown-linux-gnu-install_only.tar.gz +767d24f3570b35fedb945f5ac66224c8983f2d556ab83c5cfaa5f3666e9c212c 20230507/cpython-3.11.3+20230507-ppc64le-unknown-linux-gnu-install_only.tar.gz +f710b8d60621308149c100d5175fec39274ed0b9c99645484fd93d1716ef4310 20230507/cpython-3.11.3+20230507-x86_64-apple-darwin-install_only.tar.gz +24741066da6f35a7ff67bee65ce82eae870d84e1181843e64a7076d1571e95af 20230507/cpython-3.11.3+20230507-x86_64-pc-windows-msvc-shared-install_only.tar.gz +da50b87d1ec42b3cb577dfd22a3655e43a53150f4f98a4bfb40757c9d7839ab5 20230507/cpython-3.11.3+20230507-x86_64-unknown-linux-gnu-install_only.tar.gz +bc66c706ea8c5fc891635fda8f9da971a1a901d41342f6798c20ad0b2a25d1d6 20230726/cpython-3.10.12+20230726-aarch64-apple-darwin-install_only.tar.gz +fee80e221663eca5174bd794cb5047e40d3910dbeadcdf1f09d405a4c1c15fe4 20230726/cpython-3.10.12+20230726-aarch64-unknown-linux-gnu-install_only.tar.gz +bb5e8cb0d2e44241725fa9b342238245503e7849917660006b0246a9c97b1d6c 20230726/cpython-3.10.12+20230726-ppc64le-unknown-linux-gnu-install_only.tar.gz +8d33d435ae6fb93ded7fc26798cc0a1a4f546a4e527012a1e2909cc314b332df 20230726/cpython-3.10.12+20230726-s390x-unknown-linux-gnu-install_only.tar.gz +8a6e3ed973a671de468d9c691ed9cb2c3a4858c5defffcf0b08969fba9c1dd04 20230726/cpython-3.10.12+20230726-x86_64-apple-darwin-install_only.tar.gz +c1a31c353ca44de7d1b1a3b6c55a823e9c1eed0423d4f9f66e617bdb1b608685 20230726/cpython-3.10.12+20230726-x86_64-pc-windows-msvc-shared-install_only.tar.gz +a476dbca9184df9fc69fe6309cda5ebaf031d27ca9e529852437c94ec1bc43d3 20230726/cpython-3.10.12+20230726-x86_64-unknown-linux-gnu-install_only.tar.gz +cb6d2948384a857321f2aa40fa67744cd9676a330f08b6dad7070bda0b6120a4 20230726/cpython-3.11.4+20230726-aarch64-apple-darwin-install_only.tar.gz +2e84fc53f4e90e11963281c5c871f593abcb24fc796a50337fa516be99af02fb 20230726/cpython-3.11.4+20230726-aarch64-unknown-linux-gnu-install_only.tar.gz +df7b92ed9cec96b3bb658fb586be947722ecd8e420fb23cee13d2e90abcfcf25 20230726/cpython-3.11.4+20230726-ppc64le-unknown-linux-gnu-install_only.tar.gz +e477f0749161f9aa7887964f089d9460a539f6b4a8fdab5166f898210e1a87a4 20230726/cpython-3.11.4+20230726-s390x-unknown-linux-gnu-install_only.tar.gz +47e1557d93a42585972772e82661047ca5f608293158acb2778dccf120eabb00 20230726/cpython-3.11.4+20230726-x86_64-apple-darwin-install_only.tar.gz +878614c03ea38538ae2f758e36c85d2c0eb1eaaca86cd400ff8c76693ee0b3e1 20230726/cpython-3.11.4+20230726-x86_64-pc-windows-msvc-shared-install_only.tar.gz +e26247302bc8e9083a43ce9e8dd94905b40d464745b1603041f7bc9a93c65d05 20230726/cpython-3.11.4+20230726-x86_64-unknown-linux-gnu-install_only.tar.gz +dab64b3580118ad2073babd7c29fd2053b616479df5c107d31fe2af1f45e948b 20230826/cpython-3.11.5+20230826-aarch64-apple-darwin-install_only.tar.gz +bb5c5d1ea0f199fe2d3f0996fff4b48ca6ddc415a3dbd98f50bff7fce48aac80 20230826/cpython-3.11.5+20230826-aarch64-unknown-linux-gnu-install_only.tar.gz +14121b53e9c8c6d0741f911ae00102a35adbcf5c3cdf732687ef7617b7d7304d 20230826/cpython-3.11.5+20230826-ppc64le-unknown-linux-gnu-install_only.tar.gz +fe459da39874443579d6fe88c68777c6d3e331038e1fb92a0451879fb6beb16d 20230826/cpython-3.11.5+20230826-s390x-unknown-linux-gnu-install_only.tar.gz +4a4efa7378c72f1dd8ebcce1afb99b24c01b07023aa6b8fea50eaedb50bf2bfc 20230826/cpython-3.11.5+20230826-x86_64-apple-darwin-install_only.tar.gz +00f002263efc8aea896bcfaaf906b1f4dab3e5cd3db53e2b69ab9a10ba220b97 20230826/cpython-3.11.5+20230826-x86_64-pc-windows-msvc-shared-install_only.tar.gz +fbed6f7694b2faae5d7c401a856219c945397f772eea5ca50c6eb825cbc9d1e1 20230826/cpython-3.11.5+20230826-x86_64-unknown-linux-gnu-install_only.tar.gz +916c35125b5d8323a21526d7a9154ca626453f63d0878e95b9f613a95006c990 20231002/cpython-3.11.6+20231002-aarch64-apple-darwin-install_only.tar.gz +3e26a672df17708c4dc928475a5974c3fb3a34a9b45c65fb4bd1e50504cc84ec 20231002/cpython-3.11.6+20231002-aarch64-unknown-linux-gnu-install_only.tar.gz +7937035f690a624dba4d014ffd20c342e843dd46f89b0b0a1e5726b85deb8eaf 20231002/cpython-3.11.6+20231002-ppc64le-unknown-linux-gnu-install_only.tar.gz +f9f19823dba3209cedc4647b00f46ed0177242917db20fb7fb539970e384531c 20231002/cpython-3.11.6+20231002-s390x-unknown-linux-gnu-install_only.tar.gz +178cb1716c2abc25cb56ae915096c1a083e60abeba57af001996e8bc6ce1a371 20231002/cpython-3.11.6+20231002-x86_64-apple-darwin-install_only.tar.gz +3933545e6d41462dd6a47e44133ea40995bc6efeed8c2e4cbdf1a699303e95ea 20231002/cpython-3.11.6+20231002-x86_64-pc-windows-msvc-shared-install_only.tar.gz +ee37a7eae6e80148c7e3abc56e48a397c1664f044920463ad0df0fc706eacea8 20231002/cpython-3.11.6+20231002-x86_64-unknown-linux-gnu-install_only.tar.gz +4734a2be2becb813830112c780c9879ac3aff111a0b0cd590e65ec7465774d02 20231002/cpython-3.12.0+20231002-aarch64-apple-darwin-install_only.tar.gz +bccfe67cf5465a3dfb0336f053966e2613a9bc85a6588c2fcf1366ef930c4f88 20231002/cpython-3.12.0+20231002-aarch64-unknown-linux-gnu-install_only.tar.gz +b5dae075467ace32c594c7877fe6ebe0837681f814601d5d90ba4c0dfd87a1f2 20231002/cpython-3.12.0+20231002-ppc64le-unknown-linux-gnu-install_only.tar.gz +5681621349dd85d9726d1b67c84a9686ce78f72e73a6f9e4cc4119911655759e 20231002/cpython-3.12.0+20231002-s390x-unknown-linux-gnu-install_only.tar.gz +5a9e88c8aa52b609d556777b52ebde464ae4b4f77e4aac4eb693af57395c9abf 20231002/cpython-3.12.0+20231002-x86_64-apple-darwin-install_only.tar.gz +facfaa1fbc8653f95057f3c4a0f8aa833dab0e0b316e24ee8686bc761d4b4f8d 20231002/cpython-3.12.0+20231002-x86_64-pc-windows-msvc-shared-install_only.tar.gz +e51a5293f214053ddb4645b2c9f84542e2ef86870b8655704367bd4b29d39fe9 20231002/cpython-3.12.0+20231002-x86_64-unknown-linux-gnu-install_only.tar.gz +b042c966920cf8465385ca3522986b12d745151a72c060991088977ca36d3883 20240107/cpython-3.11.7+20240107-aarch64-apple-darwin-install_only.tar.gz +b102eaf865eb715aa98a8a2ef19037b6cc3ae7dfd4a632802650f29de635aa13 20240107/cpython-3.11.7+20240107-aarch64-unknown-linux-gnu-install_only.tar.gz +b44e1b74afe75c7b19143413632c4386708ae229117f8f950c2094e9681d34c7 20240107/cpython-3.11.7+20240107-ppc64le-unknown-linux-gnu-install_only.tar.gz +49520e3ff494708020f306e30b0964f079170be83e956be4504f850557378a22 20240107/cpython-3.11.7+20240107-s390x-unknown-linux-gnu-install_only.tar.gz +a0e615eef1fafdc742da0008425a9030b7ea68a4ae4e73ac557ef27b112836d4 20240107/cpython-3.11.7+20240107-x86_64-apple-darwin-install_only.tar.gz +67077e6fa918e4f4fd60ba169820b00be7c390c497bf9bc9cab2c255ea8e6f3e 20240107/cpython-3.11.7+20240107-x86_64-pc-windows-msvc-shared-install_only.tar.gz +4a51ce60007a6facf64e5495f4cf322e311ba9f39a8cd3f3e4c026eae488e140 20240107/cpython-3.11.7+20240107-x86_64-unknown-linux-gnu-install_only.tar.gz +f93f8375ca6ac0a35d58ff007043cbd3a88d9609113f1cb59cf7c8d215f064af 20240107/cpython-3.12.1+20240107-aarch64-apple-darwin-install_only.tar.gz +236533ef20e665007a111c2f36efb59c87ae195ad7dca223b6dc03fb07064f0b 20240107/cpython-3.12.1+20240107-aarch64-unknown-linux-gnu-install_only.tar.gz +78051f0d1411ee62bc2af5edfccf6e8400ac4ef82887a2affc19a7ace6a05267 20240107/cpython-3.12.1+20240107-ppc64le-unknown-linux-gnu-install_only.tar.gz +60631211c701f8d2c56e5dd7b154e68868128a019b9db1d53a264f56c0d4aee2 20240107/cpython-3.12.1+20240107-s390x-unknown-linux-gnu-install_only.tar.gz +eca96158c1568dedd9a0b3425375637a83764d1fa74446438293089a8bfac1f8 20240107/cpython-3.12.1+20240107-x86_64-apple-darwin-install_only.tar.gz +fd5a9e0f41959d0341246d3643f2b8794f638adc0cec8dd5e1b6465198eae08a 20240107/cpython-3.12.1+20240107-x86_64-pc-windows-msvc-shared-install_only.tar.gz +74e330b8212ca22fd4d9a2003b9eec14892155566738febc8e5e572f267b9472 20240107/cpython-3.12.1+20240107-x86_64-unknown-linux-gnu-install_only.tar.gz +5fdc0f6a5b5a90fd3c528e8b1da8e3aac931ea8690126c2fdb4254c84a3ff04a 20240224/cpython-3.10.13+20240224-aarch64-apple-darwin-install_only.tar.gz +a898a88705611b372297bb8fe4d23cc16b8603ce5f24494c3a8cfa65d83787f9 20240224/cpython-3.10.13+20240224-aarch64-unknown-linux-gnu-install_only.tar.gz +c23706e138a0351fc1e9def2974af7b8206bac7ecbbb98a78f5aa9e7535fee42 20240224/cpython-3.10.13+20240224-ppc64le-unknown-linux-gnu-install_only.tar.gz +09be8fb2cdfbb4a93d555f268f244dbe4d8ff1854b2658e8043aa4ec08aede3e 20240224/cpython-3.10.13+20240224-s390x-unknown-linux-gnu-install_only.tar.gz +6378dfd22f58bb553ddb02be28304d739cd730c1f95c15c74955c923a1bc3d6a 20240224/cpython-3.10.13+20240224-x86_64-apple-darwin-install_only.tar.gz +086f7fe9156b897bb401273db8359017104168ac36f60f3af4e31ac7acd6634e 20240224/cpython-3.10.13+20240224-x86_64-pc-windows-msvc-shared-install_only.tar.gz +d995d032ca702afd2fc3a689c1f84a6c64972ecd82bba76a61d525f08eb0e195 20240224/cpython-3.10.13+20240224-x86_64-unknown-linux-gnu-install_only.tar.gz +389a51139f5abe071a0d70091ca5df3e7a3dfcfcbe3e0ba6ad85fb4c5638421e 20240224/cpython-3.11.8+20240224-aarch64-apple-darwin-install_only.tar.gz +389b9005fb78dd5a6f68df5ea45ab7b30d9a4b3222af96999e94fd20d4ad0c6a 20240224/cpython-3.11.8+20240224-aarch64-unknown-linux-gnu-install_only.tar.gz +eb2b31f8e50309aae493c6a359c32b723a676f07c641f5e8fe4b6aa4dbb50946 20240224/cpython-3.11.8+20240224-ppc64le-unknown-linux-gnu-install_only.tar.gz +844f64f4c16e24965778281da61d1e0e6cd1358a581df1662da814b1eed096b9 20240224/cpython-3.11.8+20240224-s390x-unknown-linux-gnu-install_only.tar.gz +097f467b0c36706bfec13f199a2eaf924e668f70c6e2bd1f1366806962f7e86e 20240224/cpython-3.11.8+20240224-x86_64-apple-darwin-install_only.tar.gz +b618f1f047349770ee1ef11d1b05899840abd53884b820fd25c7dfe2ec1664d4 20240224/cpython-3.11.8+20240224-x86_64-pc-windows-msvc-shared-install_only.tar.gz +94e13d0e5ad417035b80580f3e893a72e094b0900d5d64e7e34ab08e95439987 20240224/cpython-3.11.8+20240224-x86_64-unknown-linux-gnu-install_only.tar.gz +01c064c00013b0175c7858b159989819ead53f4746d40580b5b0b35b6e80fba6 20240224/cpython-3.12.2+20240224-aarch64-apple-darwin-install_only.tar.gz +e52550379e7c4ac27a87de832d172658bc04150e4e27d4e858e6d8cbb96fd709 20240224/cpython-3.12.2+20240224-aarch64-unknown-linux-gnu-install_only.tar.gz +74bc02c4bbbd26245c37b29b9e12d0a9c1b7ab93477fed8b651c988b6a9a6251 20240224/cpython-3.12.2+20240224-ppc64le-unknown-linux-gnu-install_only.tar.gz +ecd6b0285e5eef94deb784b588b4b425a15a43ae671bf206556659dc141a9825 20240224/cpython-3.12.2+20240224-s390x-unknown-linux-gnu-install_only.tar.gz +a53a6670a202c96fec0b8c55ccc780ea3af5307eb89268d5b41a9775b109c094 20240224/cpython-3.12.2+20240224-x86_64-apple-darwin-install_only.tar.gz +1e5655a6ccb1a64a78460e4e3ee21036c70246800f176a6c91043a3fe3654a3b 20240224/cpython-3.12.2+20240224-x86_64-pc-windows-msvc-shared-install_only.tar.gz +57a37b57f8243caa4cdac016176189573ad7620f0b6da5941c5e40660f9468ab 20240224/cpython-3.12.2+20240224-x86_64-unknown-linux-gnu-install_only.tar.gz +ccc40e5af329ef2af81350db2a88bbd6c17b56676e82d62048c15d548401519e 20240415/cpython-3.12.3+20240415-aarch64-apple-darwin-install_only.tar.gz +ec8126de97945e629cca9aedc80a29c4ae2992c9d69f2655e27ae73906ba187d 20240415/cpython-3.12.3+20240415-aarch64-unknown-linux-gnu-install_only.tar.gz +c5dcf08b8077e617d949bda23027c49712f583120b3ed744f9b143da1d580572 20240415/cpython-3.12.3+20240415-ppc64le-unknown-linux-gnu-install_only.tar.gz +872fc321363b8cdd826fd2cb1adfd1ceb813bc1281f9d410c1c2c4e177e8df86 20240415/cpython-3.12.3+20240415-s390x-unknown-linux-gnu-install_only.tar.gz +c37a22fca8f57d4471e3708de6d13097668c5f160067f264bb2b18f524c890c8 20240415/cpython-3.12.3+20240415-x86_64-apple-darwin-install_only.tar.gz +f7cfa4ad072feb4578c8afca5ba9a54ad591d665a441dd0d63aa366edbe19279 20240415/cpython-3.12.3+20240415-x86_64-pc-windows-msvc-shared-install_only.tar.gz +a73ba777b5d55ca89edef709e6b8521e3f3d4289581f174c8699adfb608d09d6 20240415/cpython-3.12.3+20240415-x86_64-unknown-linux-gnu-install_only.tar.gz +164d89f0df2feb689981864ecc1dffb19e6aa3696c8880166de555494fe92607 20240726/cpython-3.10.14+20240726-aarch64-apple-darwin-install_only.tar.gz +39bcd46b4d70e40da177c55259be16d5c2be7a3f7f93f1e3bde47e71b4833f29 20240726/cpython-3.10.14+20240726-aarch64-unknown-linux-gnu-install_only.tar.gz +549d38b9ef59cba9ab2990025255231bfa1cb32b4bc5eac321667640fdee19d1 20240726/cpython-3.10.14+20240726-ppc64le-unknown-linux-gnu-install_only.tar.gz +de4bc878a8666c734f983db971610980870148f333bda8b0c34abfaeae88d7ec 20240726/cpython-3.10.14+20240726-s390x-unknown-linux-gnu-install_only.tar.gz +1a1455838cd1e8ed0da14a152a2d559a2fd3a6047ba7013e841db4a35a228c1d 20240726/cpython-3.10.14+20240726-x86_64-apple-darwin-install_only.tar.gz +7f68821a8b5445267eca480660364ebd06ec84632b336770c6e39de07ac0f6c3 20240726/cpython-3.10.14+20240726-x86_64-pc-windows-msvc-shared-install_only.tar.gz +32b34cd13d9d745b3db3f3b8398ab2c07de74544829915dbebd8dce39bdc405e 20240726/cpython-3.10.14+20240726-x86_64-unknown-linux-gnu-install_only.tar.gz +cbdac9462bab9671c8e84650e425d3f43b775752a930a2ef954a0d457d5c00c3 20240726/cpython-3.11.9+20240726-aarch64-apple-darwin-install_only.tar.gz +4d17cf988abe24449d649aad3ef974091ab76807904d41839907061925b4c9e3 20240726/cpython-3.11.9+20240726-aarch64-unknown-linux-gnu-install_only.tar.gz +fc4f3c9ef9bfac2ed0282126ff376e544697ad04a5408d6429d46899d7d3bf21 20240726/cpython-3.11.9+20240726-ppc64le-unknown-linux-gnu-install_only.tar.gz +e69b66e53e926460df044f44846eef3fea642f630e829719e1a4112fc370dc56 20240726/cpython-3.11.9+20240726-s390x-unknown-linux-gnu-install_only.tar.gz +dc3174666a30f4c38d04e79a80c3159b4b3aa69597c4676701c8386696811611 20240726/cpython-3.11.9+20240726-x86_64-apple-darwin-install_only.tar.gz +f694be48bdfec1dace6d69a19906b6083f4dd7c7c61f1138ba520e433e5598f8 20240726/cpython-3.11.9+20240726-x86_64-pc-windows-msvc-shared-install_only.tar.gz +f6e955dc9ddfcad74e77abe6f439dac48ebca14b101ed7c85a5bf3206ed2c53d 20240726/cpython-3.11.9+20240726-x86_64-unknown-linux-gnu-install_only.tar.gz +1801025e825c04b3907e4ef6220a13607bc0397628c9485897073110ef7fde15 20240726/cpython-3.12.4+20240726-aarch64-apple-darwin-install_only.tar.gz +a098b18b7e9fea0c66867b76c0124fce9465765017572b2e7b522154c87c78d7 20240726/cpython-3.12.4+20240726-aarch64-unknown-linux-gnu-install_only.tar.gz +04011c4c5b7fe34b0b895edf4ad8748e410686c1d69aaee11d6688d481023bcb 20240726/cpython-3.12.4+20240726-ppc64le-unknown-linux-gnu-install_only.tar.gz +8f8f3e29cf0c2facdbcfee70660939fda7667ac24fee8656d3388fc72f3acc7c 20240726/cpython-3.12.4+20240726-s390x-unknown-linux-gnu-install_only.tar.gz +4c325838c1b0ed13698506fcd515be25c73dcbe195f8522cf98f9148a97601ed 20240726/cpython-3.12.4+20240726-x86_64-apple-darwin-install_only.tar.gz +74309b0f322716409883d38c621743ea7fa0376eb00927b8ee1e1671d3aff450 20240726/cpython-3.12.4+20240726-x86_64-pc-windows-msvc-shared-install_only.tar.gz +e133dd6fc6a2d0033e2658637cc22e9c95f9d7073b80115037ee1f16417a54ac 20240726/cpython-3.12.4+20240726-x86_64-unknown-linux-gnu-install_only.tar.gz +f64776f455a44c24d50f947c813738cfb7b9ac43732c44891bc831fa7940a33c 20241016/cpython-3.10.15+20241016-aarch64-apple-darwin-install_only.tar.gz +eb58581f85fde83d1f3e8e1f8c6f5a15c7ae4fdbe3b1d1083931f9167fdd8dbc 20241016/cpython-3.10.15+20241016-aarch64-unknown-linux-gnu-install_only.tar.gz +0c45af4e7525e2db59901606db32b2896ac1e9830c6f95551402207f537c2ce4 20241016/cpython-3.10.15+20241016-ppc64le-unknown-linux-gnu-install_only.tar.gz +de205896b070e6f5259ac0f2b3379eead875ea84e6a6ef533b89886fcbb46a4c 20241016/cpython-3.10.15+20241016-s390x-unknown-linux-gnu-install_only.tar.gz +90b46dfb1abd98d45663c7a2a8c45d3047a59391d8586d71b459cec7b75f662b 20241016/cpython-3.10.15+20241016-x86_64-apple-darwin-install_only.tar.gz +e48952619796c66ec9719867b87be97edca791c2ef7fbf87d42c417c3331609e 20241016/cpython-3.10.15+20241016-x86_64-pc-windows-msvc-shared-install_only.tar.gz +3db2171e03c1a7acdc599fba583c1b92306d3788b375c9323077367af1e9d9de 20241016/cpython-3.10.15+20241016-x86_64-unknown-linux-gnu-install_only.tar.gz +ed519c47d9620eb916a6f95ec2875396e7b1a9ab993ee40b2f31b837733f318c 20241016/cpython-3.10.15+20241016-x86_64-unknown-linux-musl-install_only.tar.gz +5a69382da99c4620690643517ca1f1f53772331b347e75f536088c42a4cf6620 20241016/cpython-3.11.10+20241016-aarch64-apple-darwin-install_only.tar.gz +803e49259280af0f5466d32829cd9d65a302b0226e424b3f0b261f9daf6aee8f 20241016/cpython-3.11.10+20241016-aarch64-unknown-linux-gnu-install_only.tar.gz +92b666d103902001322f42badbd68da92adc5cebb826af9c1c906c33166e2f34 20241016/cpython-3.11.10+20241016-ppc64le-unknown-linux-gnu-install_only.tar.gz +6d584317651c1ad4a857cb32d1999707e8bb3046fcb2f156d80381814fa19fde 20241016/cpython-3.11.10+20241016-s390x-unknown-linux-gnu-install_only.tar.gz +1e23ffe5bc473e1323ab8f51464da62d77399afb423babf67f8e13c82b69c674 20241016/cpython-3.11.10+20241016-x86_64-apple-darwin-install_only.tar.gz +647b66ff4552e70aec3bf634dd470891b4a2b291e8e8715b3bdb162f577d4c55 20241016/cpython-3.11.10+20241016-x86_64-pc-windows-msvc-shared-install_only.tar.gz +8b50a442b04724a24c1eebb65a36a0c0e833d35374dbdf9c9470d8a97b164cd9 20241016/cpython-3.11.10+20241016-x86_64-unknown-linux-gnu-install_only.tar.gz +d36fc77a8dd76155a7530f6235999a693b9e7c48aa11afeb5610a091cae5aa6f 20241016/cpython-3.11.10+20241016-x86_64-unknown-linux-musl-install_only.tar.gz +4c18852bf9c1a11b56f21bcf0df1946f7e98ee43e9e4c0c5374b2b3765cf9508 20241016/cpython-3.12.7+20241016-aarch64-apple-darwin-install_only.tar.gz +bba3c6be6153f715f2941da34f3a6a69c2d0035c9c5396bc5bb68c6d2bd1065a 20241016/cpython-3.12.7+20241016-aarch64-unknown-linux-gnu-install_only.tar.gz +0a1d1d92e33a969bd2f40a80af53c97b6c0cc1060d384ceff50ff801593bf9d6 20241016/cpython-3.12.7+20241016-ppc64le-unknown-linux-gnu-install_only.tar.gz +935676a0c960b552f95e9ac2e1e385de5de4b34038ff65ffdc688838f1189c17 20241016/cpython-3.12.7+20241016-s390x-unknown-linux-gnu-install_only.tar.gz +60c5271e7edc3c2ab47440b7abf4ed50fbc693880b474f74f05768f5b657045a 20241016/cpython-3.12.7+20241016-x86_64-apple-darwin-install_only.tar.gz +f05531bff16fa77b53be0776587b97b466070e768e6d5920894de988bdcd547a 20241016/cpython-3.12.7+20241016-x86_64-pc-windows-msvc-shared-install_only.tar.gz +43576f7db1033dd57b900307f09c2e86f371152ac8a2607133afa51cbfc36064 20241016/cpython-3.12.7+20241016-x86_64-unknown-linux-gnu-install_only.tar.gz +5ed4a4078db3cbac563af66403aaa156cd6e48831d90382a1820db2b120627b5 20241016/cpython-3.12.7+20241016-x86_64-unknown-linux-musl-install_only.tar.gz +efc2e71c0e05bc5bedb7a846e05f28dd26491b1744ded35ed82f8b49ccfa684b 20241016/cpython-3.13.0+20241016-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +31397953849d275aa2506580f3fa1cb5a85b6a3d392e495f8030e8b6412f5556 20241016/cpython-3.13.0+20241016-aarch64-apple-darwin-install_only.tar.gz +59b50df9826475d24bb7eff781fa3949112b5e9c92adb29e96a09cdf1216d5bd 20241016/cpython-3.13.0+20241016-aarch64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +e8378c0162b2e0e4cc1f62b29443a3305d116d09583304dbb0149fecaff6347b 20241016/cpython-3.13.0+20241016-aarch64-unknown-linux-gnu-install_only.tar.gz +1217efa5f4ce67fcc9f7eb64165b1bd0912b2a21bc25c1a7e2cb174a21a5df7e 20241016/cpython-3.13.0+20241016-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +fc4b7f27c4e84c78f3c8e6c7f8e4023e4638d11f1b36b6b5ce457b1926cebb53 20241016/cpython-3.13.0+20241016-ppc64le-unknown-linux-gnu-install_only.tar.gz +6c3e1e4f19d2b018b65a7e3ef4cd4225c5b9adfbc490218628466e636d5c4b8c 20241016/cpython-3.13.0+20241016-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +66b19e6a07717f6cfcd3a8ca953f0a2eaa232291142f3d26a8d17c979ec0f467 20241016/cpython-3.13.0+20241016-s390x-unknown-linux-gnu-install_only.tar.gz +2e07dfea62fe2215738551a179c87dbed1cc79d1b3654f4d7559889a6d5ce4eb 20241016/cpython-3.13.0+20241016-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +cff1b7e7cd26f2d47acac1ad6590e27d29829776f77e8afa067e9419f2f6ce77 20241016/cpython-3.13.0+20241016-x86_64-apple-darwin-install_only.tar.gz +bfd89f9acf866463bc4baf01733da5e767d13f5d0112175a4f57ba91f1541310 20241016/cpython-3.13.0+20241016-x86_64-pc-windows-msvc-shared-freethreaded+pgo-full.tar.zst +b25926e8ce4164cf103bacc4f4d154894ea53e07dd3fdd5ebb16fb1a82a7b1a0 20241016/cpython-3.13.0+20241016-x86_64-pc-windows-msvc-shared-install_only.tar.gz +a73adeda301ad843cce05f31a2d3e76222b656984535a7b87696a24a098b216c 20241016/cpython-3.13.0+20241016-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +2c8cb15c6a2caadaa98af51df6fe78a8155b8471cb3dd7b9836038e0d3657fb4 20241016/cpython-3.13.0+20241016-x86_64-unknown-linux-gnu-install_only.tar.gz +2f61ee3b628a56aceea63b46c7afe2df3e22a61da706606b0c8efda57f953cf4 20241016/cpython-3.13.0+20241016-x86_64-unknown-linux-musl-install_only.tar.gz +08f05618bdcf8064a7960b25d9ba92155447c9b08e0cf2f46a981e4c6a1bb5a5 20241205/cpython-3.13.1+20241205-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +88b88b609129c12f4b3841845aca13230f61e97ba97bd0fb28ee64b0e442a34f 20241205/cpython-3.13.1+20241205-aarch64-apple-darwin-install_only.tar.gz +9f2fcb809f9ba6c7c014a8803073a88786701a98971135bce684355062e4bb35 20241205/cpython-3.13.1+20241205-aarch64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +fdfa86c2746d2ae700042c461846e6c37f70c249925b58de8cd02eb8d1423d4e 20241205/cpython-3.13.1+20241205-aarch64-unknown-linux-gnu-install_only.tar.gz +15ceea78dff78ca8ccaac8d9c54b808af30daaa126f1f561e920a6896e098634 20241205/cpython-3.13.1+20241205-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +27b20b3237c55430ca1304e687d021f88373f906249f9cd272c5ff2803d5e5c3 20241205/cpython-3.13.1+20241205-ppc64le-unknown-linux-gnu-install_only.tar.gz +ed3c6118d1d12603309c930e93421ac7a30a69045ffd43006f63ecf71d72c317 20241205/cpython-3.13.1+20241205-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +7d0187e20cb5e36c689eec27e4d3de56d8b7f1c50dc5523550fc47377801521f 20241205/cpython-3.13.1+20241205-s390x-unknown-linux-gnu-install_only.tar.gz +dc780fecd215d2cc9e573abf1e13a175fcfa8f6efd100ef888494a248a16cda8 20241205/cpython-3.13.1+20241205-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +47eef6efb8664e2d1d23a7cdaf56262d784f8ace48f3bfca1b183e95a49888d6 20241205/cpython-3.13.1+20241205-x86_64-apple-darwin-install_only.tar.gz +7537b2ab361c0eabc0eabfca9ffd9862d7f5f6576eda13b97e98aceb5eea4fd3 20241205/cpython-3.13.1+20241205-x86_64-pc-windows-msvc-shared-freethreaded+pgo-full.tar.zst +f51f0493a5f979ff0b8d8c598a8d74f2a4d86a190c2729c85e0af65c36a9cbbe 20241205/cpython-3.13.1+20241205-x86_64-pc-windows-msvc-shared-install_only.tar.gz +9ec1b81213f849d91f5ebe6a16196e85cd6ff7c05ca823ce0ab7ba5b0e9fee84 20241205/cpython-3.13.1+20241205-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +242b2727df6c1e00de6a9f0f0dcb4562e168d27f428c785b0eb41a6aeb34d69a 20241205/cpython-3.13.1+20241205-x86_64-unknown-linux-gnu-install_only.tar.gz +76b30c6373b9c0aa2ba610e07da02f384aa210ac79643da38c66d3e6171c6ef5 20241205/cpython-3.13.1+20241205-x86_64-unknown-linux-musl-install_only.tar.gz +e3c4aa607717b23903ca2650d5c3ee24f89b97543e2db2b0f463bddc7a9e92f3 20241206/cpython-3.12.8+20241206-aarch64-apple-darwin-install_only.tar.gz +ce674b55442b732973afb2932c281bb1ded4ad7e22bcf9b07071165770758c7e 20241206/cpython-3.12.8+20241206-aarch64-unknown-linux-gnu-install_only.tar.gz +b7214790b273de9ed0532420054b72ba1393d62d2fc844ec55ade193771bd90c 20241206/cpython-3.12.8+20241206-ppc64le-unknown-linux-gnu-install_only.tar.gz +73102f5dbd7d1e7e9c2f2c80aedf2893d99a7fa407f6674ec8b2f57ba07daee5 20241206/cpython-3.12.8+20241206-s390x-unknown-linux-gnu-install_only.tar.gz +3ba35c706577d755e8e52a4c161a042464577c0e695e2a605362fa469e26de10 20241206/cpython-3.12.8+20241206-x86_64-apple-darwin-install_only.tar.gz +767b4be3ddf6b99e5ade519789c1615c191d8cf99d5aff4685cc18b48931f1e6 20241206/cpython-3.12.8+20241206-x86_64-pc-windows-msvc-shared-install_only.tar.gz +b9d6ee5ddac1198e72d53112698773fc8bb597de095592eb849ca794306699ba 20241206/cpython-3.12.8+20241206-x86_64-unknown-linux-gnu-install_only.tar.gz +6f305888703691dd04cfff85284d23ea0b0146ed7c4415e472f1fb72b3f32cdf 20241206/cpython-3.12.8+20241206-x86_64-unknown-linux-musl-install_only.tar.gz +e99f8457d9c79592c036489c5cfa78df76e4762d170665e499833e045d82608f 20250317/cpython-3.10.16+20250317-aarch64-apple-darwin-install_only.tar.gz +76d0f04d2444e77200fdc70d1c57480e29cca78cb7420d713bc1c523709c198d 20250317/cpython-3.10.16+20250317-aarch64-unknown-linux-gnu-install_only.tar.gz +39c9b3486de984fe1d72d90278229c70d6b08bcf69cd55796881b2d75077b603 20250317/cpython-3.10.16+20250317-ppc64le-unknown-linux-gnu-install_only.tar.gz +ebe949ada9293581c17d9bcdaa8f645f67d95f73eac65def760a71ef9dd6600d 20250317/cpython-3.10.16+20250317-riscv64-unknown-linux-gnu-install_only.tar.gz +9b2fc0b7f1c75b48e799b6fa14f7e24f5c61f2db82e3c65d13ed25e08f7f0857 20250317/cpython-3.10.16+20250317-s390x-unknown-linux-gnu-install_only.tar.gz +e03e62dbe95afa2f56b7344ff3bd061b180a0b690ff77f9a1d7e6601935e05ca 20250317/cpython-3.10.16+20250317-x86_64-apple-darwin-install_only.tar.gz +c7e0eb0ff5b36758b7a8cacd42eb223c056b9c4d36eded9bf5b9fe0c0b9aeb08 20250317/cpython-3.10.16+20250317-x86_64-pc-windows-msvc-install_only.tar.gz +b350c7e63956ca8edb856b91316328e0fd003a840cbd63d08253af43b2c63643 20250317/cpython-3.10.16+20250317-x86_64-unknown-linux-gnu-install_only.tar.gz +6ed64923ee4fbea4c5780f1a5a66651d239191ac10bd23420db4f5e4e0bf79c4 20250317/cpython-3.10.16+20250317-x86_64-unknown-linux-musl-install_only.tar.gz +7c7fd9809da0382a601a79287b5d62d61ce0b15f5a5ee836233727a516e85381 20250317/cpython-3.12.9+20250317-aarch64-apple-darwin-install_only.tar.gz +00c6bf9acef21ac741fea24dc449d0149834d30e9113429e50a95cce4b00bb80 20250317/cpython-3.12.9+20250317-aarch64-unknown-linux-gnu-install_only.tar.gz +25d77599dfd5849f17391d92da0da99079e4e94f19a881f763f5cc62530ef7e1 20250317/cpython-3.12.9+20250317-ppc64le-unknown-linux-gnu-install_only.tar.gz +e97ab0fdf443b302c56a52b4fd08f513bf3be66aa47263f0f9df3c6e60e05f2e 20250317/cpython-3.12.9+20250317-riscv64-unknown-linux-gnu-install_only.tar.gz +7492d079ffa8425c8f6c58e43b237c37e3fb7b31e2e14635927bb4d3397ba21e 20250317/cpython-3.12.9+20250317-s390x-unknown-linux-gnu-install_only.tar.gz +1ee1b1bb9fbce5c145c4bec9a3c98d7a4fa22543e09a7c1d932bc8599283c2dc 20250317/cpython-3.12.9+20250317-x86_64-apple-darwin-install_only.tar.gz +d15361fd202dd74ae9c3eece1abdab7655f1eba90bf6255cad1d7c53d463ed4d 20250317/cpython-3.12.9+20250317-x86_64-pc-windows-msvc-install_only.tar.gz +ef382fb88cbb41a3b0801690bd716b8a1aec07a6c6471010bcc6bd14cd575226 20250317/cpython-3.12.9+20250317-x86_64-unknown-linux-gnu-install_only.tar.gz +94e3837da1adf9964aab2d6047b33f70167de3096d1f9a2d1fa9340b1bbf537d 20250317/cpython-3.12.9+20250317-x86_64-unknown-linux-musl-install_only.tar.gz +c98c9c977e6fa05c3813bd49f3553904d89d60fed27e2e36468da7afa1d6d5e2 20250317/cpython-3.13.2+20250317-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +faa44274a331eb39786362818b21b3a4e74514e8805000b20b0e55c590cecb94 20250317/cpython-3.13.2+20250317-aarch64-apple-darwin-install_only.tar.gz +b8635e59e3143fd17f19a3dfe8ccc246ee6587c87da359bd1bcab35eefbb5f19 20250317/cpython-3.13.2+20250317-aarch64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +9c67260446fee6ea706dad577a0b32936c63f449c25d66e4383d5846b2ab2e36 20250317/cpython-3.13.2+20250317-aarch64-unknown-linux-gnu-install_only.tar.gz +6ae8fa44cb2edf4ab49cff1820b53c40c10349c0f39e11b8cd76ce7f3e7e1def 20250317/cpython-3.13.2+20250317-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +345b53d2f86c9dbd7f1320657cb227ff9a42ef63ff21f129abbbc8c82a375147 20250317/cpython-3.13.2+20250317-ppc64le-unknown-linux-gnu-install_only.tar.gz +2af1b8850c52801fb6189e7a17a51e0c93d9e46ddefcca72247b76329c97d02a 20250317/cpython-3.13.2+20250317-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +172d22b2330737f3a028ea538ffe497c39a066a8d3200b22dd4d177a3332ad85 20250317/cpython-3.13.2+20250317-riscv64-unknown-linux-gnu-install_only.tar.gz +c074144cc80c2af32c420b79a9df26e8db405212619990c1fbdd308bd75afe3f 20250317/cpython-3.13.2+20250317-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +ec3b16ea8a97e3138acec72bc5ff35949950c62c8994a8ec8e213fd93f0e806b 20250317/cpython-3.13.2+20250317-s390x-unknown-linux-gnu-install_only.tar.gz +0d73e4348d8d4b5159058609d2303705190405b485dd09ad05d870d7e0f36e0f 20250317/cpython-3.13.2+20250317-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +ee4526e84b5ce5b11141c50060b385320f2773616249a741f90c96d460ce8e8f 20250317/cpython-3.13.2+20250317-x86_64-apple-darwin-install_only.tar.gz +c51b4845fda5421e044067c111192f645234081d704313f74ee77fa013a186ea 20250317/cpython-3.13.2+20250317-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +84d7b52f3558c8e35c670a4fa14080c75e3ec584adfae49fec8b51008b75b21e 20250317/cpython-3.13.2+20250317-x86_64-pc-windows-msvc-install_only.tar.gz +1aea5062614c036904b55c1cc2fb4b500b7f6f7a4cacc263f4888889d355eef8 20250317/cpython-3.13.2+20250317-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +db011f0cd29cab2291584958f4e2eb001b0e6051848d89b38a2dc23c5c54e512 20250317/cpython-3.13.2+20250317-x86_64-unknown-linux-gnu-install_only.tar.gz +00bb2d629f7eacbb5c6b44dc04af26d1f1da64cee3425b0d8eb5135a93830296 20250317/cpython-3.13.2+20250317-x86_64-unknown-linux-musl-install_only.tar.gz +278dccade56b4bbeecb9a613b77012cf5c1433a5e9b8ef99230d5e61f31d9e02 20250610/cpython-3.13.4+20250610-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +c2ce6601b2668c7bd1f799986af5ddfbff36e88795741864aba6e578cb02ed7f 20250610/cpython-3.13.4+20250610-aarch64-apple-darwin-install_only.tar.gz +b1c1bd6ab9ef95b464d92a6a911cef1a8d9f0b0f6a192f694ef18ed15d882edf 20250610/cpython-3.13.4+20250610-aarch64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +3c2596ece08ffe17e11bc1f27aeb4ce1195d2490a83d695d36ef4933d5c5ca53 20250610/cpython-3.13.4+20250610-aarch64-unknown-linux-gnu-install_only.tar.gz +ed66ae213a62b286b9b7338b816ccd2815f5248b7a28a185dc8159fe004149ae 20250610/cpython-3.13.4+20250610-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +b3cc13ee177b8db1d3e9b2eac413484e3c6a356f97d91dc59de8d3fd8cf79d6b 20250610/cpython-3.13.4+20250610-ppc64le-unknown-linux-gnu-install_only.tar.gz +913264545215236660e4178bc3e5b57a20a444a8deb5c11680c95afc960b4016 20250610/cpython-3.13.4+20250610-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +d1b989e57a9ce29f6c945eeffe0e9750c222fdd09e99d2f8d6b0d8532a523053 20250610/cpython-3.13.4+20250610-riscv64-unknown-linux-gnu-install_only.tar.gz +7556a38ab5e507c1ec22bc38f9859982bc956cab7f4de05a2faac114feb306db 20250610/cpython-3.13.4+20250610-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +d1d19fb01961ac6476712fdd6c5031f74c83666f6f11aa066207e9a158f7e3d8 20250610/cpython-3.13.4+20250610-s390x-unknown-linux-gnu-install_only.tar.gz +64ab7ac8c88002d9ba20a92f72945bfa350268e944a7922500af75d20330574d 20250610/cpython-3.13.4+20250610-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +79feb6ca68f3921d07af52d9db06cf134e6f36916941ea850ab0bc20f5ff638b 20250610/cpython-3.13.4+20250610-x86_64-apple-darwin-install_only.tar.gz +9457504547edb2e0156bf76b53c7e4941c7f61c0eff9fd5f4d816d3df51c58e3 20250610/cpython-3.13.4+20250610-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +29ac3585cc2dcfd79e3fe380c272d00e9d34351fc456e149403c86d3fea34057 20250610/cpython-3.13.4+20250610-x86_64-pc-windows-msvc-install_only.tar.gz +864df6e6819e8f8e855ce30f34410fdc5867d0616e904daeb9a40e5806e970d7 20250610/cpython-3.13.4+20250610-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +44e5477333ebca298a7a0a316985c6c3533b8645f92a83f7f73c44033832bf32 20250610/cpython-3.13.4+20250610-x86_64-unknown-linux-gnu-install_only.tar.gz +a3afbfa94b9ff4d9fc426b47eb3c8446cada535075b8d51b7bdc9d9ab9911fc2 20250610/cpython-3.13.4+20250610-x86_64-unknown-linux-musl-install_only.tar.gz +a94c02b2d597cd6b075a713fe4e9a909cc97ca6a3b2b2ce86eda21be2062d48e 20250808/cpython-3.10.18+20250808-aarch64-apple-darwin-install_only.tar.gz +ef7de3b715d519e246d98ff7856247f7f7b357068705f09c6f300b7e7b76c701 20250808/cpython-3.10.18+20250808-aarch64-unknown-linux-gnu-install_only.tar.gz +f580efed11cc54e1a221c052e8bc88bfbc12844d3ca8949da828351a1232386e 20250808/cpython-3.10.18+20250808-ppc64le-unknown-linux-gnu-install_only.tar.gz +0d7e460e30203a9225b6f417ae972f66415a1cc0e32b37ebc48d195816282669 20250808/cpython-3.10.18+20250808-riscv64-unknown-linux-gnu-install_only.tar.gz +d4ada974daadb08a0184c19232ee3b03b3137aa70609760e1a94aaf7b12989ef 20250808/cpython-3.10.18+20250808-s390x-unknown-linux-gnu-install_only.tar.gz +da96fe2ba841640215788ddb9f151f03629360e37fcb94d4f76e5095b87df0d4 20250808/cpython-3.10.18+20250808-x86_64-apple-darwin-install_only.tar.gz +a648f3c9d136985ccfe57a5507e73d9d0839f7fd09eebd7c247857f2feaecb2a 20250808/cpython-3.10.18+20250808-x86_64-pc-windows-msvc-install_only.tar.gz +0b310a73bb9e7a495dbcad5f685e508ca2e7b36ee8f29301a52285730c425789 20250808/cpython-3.10.18+20250808-x86_64-unknown-linux-gnu-install_only.tar.gz +9cecf6ea2effbe183faebcf7e1160425a4ee17a68e49f2eefe5e1c59c51fa7ee 20250808/cpython-3.10.18+20250808-x86_64-unknown-linux-musl-install_only.tar.gz +d089bfd2c7b98a0942750a195e70d3172beda76d7747097b8afd87028b6e59b6 20250808/cpython-3.11.13+20250808-aarch64-apple-darwin-install_only.tar.gz +a632857c966237e7fd38b44c47c350f6e30d8ec54dcad6c832865ad670f0f22f 20250808/cpython-3.11.13+20250808-aarch64-pc-windows-msvc-install_only.tar.gz +bc57105f8a16acd57b71d926143c7f6ecf61729b40c8b4656f1b98bebd47c710 20250808/cpython-3.11.13+20250808-aarch64-unknown-linux-gnu-install_only.tar.gz +16a0165b0744940702b8fff80b8bf973ac914f78cb6fca28d389583f675e84de 20250808/cpython-3.11.13+20250808-ppc64le-unknown-linux-gnu-install_only.tar.gz +d8e62306be8f41c46bcd62ca68f91a1467f47adff632a35ff413dc1043ed56e8 20250808/cpython-3.11.13+20250808-riscv64-unknown-linux-gnu-install_only.tar.gz +4e302a4514a73baefdd9b327062bdafeb4115a799deec91c185f6ab45a857241 20250808/cpython-3.11.13+20250808-s390x-unknown-linux-gnu-install_only.tar.gz +d946d618f8bba8308b67e460a30612a71e2ccc309f85f6628aaae24e2b816981 20250808/cpython-3.11.13+20250808-x86_64-apple-darwin-install_only.tar.gz +ed963aee33d29ad8abfbb5fe63e42f57a2638a4a11a88e11d8bb66e61f20a6e5 20250808/cpython-3.11.13+20250808-x86_64-pc-windows-msvc-install_only.tar.gz +3ad988c702cbb017fef1208d47dea4138a2e85fd0f7f01ec5e1e335e597131b9 20250808/cpython-3.11.13+20250808-x86_64-unknown-linux-gnu-install_only.tar.gz +3a5810f0696f844289aa06d5c3a1efeab66eee999c25196b7d1954192a2c2100 20250808/cpython-3.11.13+20250808-x86_64-unknown-linux-musl-install_only.tar.gz +8792c4a84c364ab975feca0c27d3157a5435b7baab325a346ae56b223893b661 20250808/cpython-3.12.11+20250808-aarch64-apple-darwin-install_only.tar.gz +00bf7d7e8bcf5d1e9c4dfca0247d8e035147777cd57ee9d4c64dedca86b0a464 20250808/cpython-3.12.11+20250808-aarch64-pc-windows-msvc-install_only.tar.gz +4d7ba5314fab02130d6538f074961ffbf61310cade9180e59026074f9a8939cb 20250808/cpython-3.12.11+20250808-aarch64-unknown-linux-gnu-install_only.tar.gz +2c862eb40a81549d9c11e6bf5a7f07c3406310b14e6a4d16dcdf1c4763ef7090 20250808/cpython-3.12.11+20250808-ppc64le-unknown-linux-gnu-install_only.tar.gz +0bb729b95fabd49c7b495f7c44a9086e3970ea57daf66365741574bd36a17e81 20250808/cpython-3.12.11+20250808-riscv64-unknown-linux-gnu-install_only.tar.gz +99e465882d217d24ac90e99fac8f32e6a644d0340ac05ee510fb5cdf53f0cfb8 20250808/cpython-3.12.11+20250808-s390x-unknown-linux-gnu-install_only.tar.gz +e0c932709dafb05f00e528a7560ef8ee559ac82b75faca60dd1245bca1c1553f 20250808/cpython-3.12.11+20250808-x86_64-apple-darwin-install_only.tar.gz +81214ef71964a40ec269a79067ca490d45298c350583bc3af0e5781451a05c3c 20250808/cpython-3.12.11+20250808-x86_64-pc-windows-msvc-install_only.tar.gz +63d78840bf209af8da8f24e335d910f88387b892ca9187be571d481c071751bb 20250808/cpython-3.12.11+20250808-x86_64-unknown-linux-gnu-install_only.tar.gz +d633d070780590aa03ac5575cd9d7b9e17682d80f14b400313c009c387cf706b 20250808/cpython-3.12.11+20250808-x86_64-unknown-linux-musl-install_only.tar.gz +f2143304012e021a603bf1807bf3e4ce163832e43ab9a9829e53cb136497f207 20250808/cpython-3.13.6+20250808-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +8a1efa6af4e80f08e2c97dda822a3d6c24d6c98e518242f802c6a43ae8401488 20250808/cpython-3.13.6+20250808-aarch64-apple-darwin-install_only.tar.gz +552cfabcc3b103f4b1c4036d2592d5f0373c9554a2c4d2b6631b04ef7e592067 20250808/cpython-3.13.6+20250808-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +8e1617bd407ec1a874499daab26ae95080d1e0267ae616d34490137a28705827 20250808/cpython-3.13.6+20250808-aarch64-pc-windows-msvc-install_only.tar.gz +d84a7d64c284be387386b9f5da273f6d05486eb6bd8f9e86e2575cb59604cb22 20250808/cpython-3.13.6+20250808-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +11fa0591ae2211c08a42ae54944260e36ddf88a1d5604ea0c49e2477be4e5388 20250808/cpython-3.13.6+20250808-aarch64-unknown-linux-gnu-install_only.tar.gz +e76fcaf1bf80a615520dbe7f85ca0bb557fad96d132d836b0ac721e7cc1e2a37 20250808/cpython-3.13.6+20250808-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +8dcf34ae1a685fe1893b52917ae04f23328edadc4acae28499d43850c2bdd26c 20250808/cpython-3.13.6+20250808-ppc64le-unknown-linux-gnu-install_only.tar.gz +24e08a39ba4fc77753e61541e52eed39cc871f4a92a80a3c5dd495056bd8eff9 20250808/cpython-3.13.6+20250808-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +f8ed75aa6cc2011a046be00b629c3c8295267f34280324feaff34c73e7afce39 20250808/cpython-3.13.6+20250808-riscv64-unknown-linux-gnu-install_only.tar.gz +1609b223fd38a4a7a4d20e7173d7d9390fe2258f7dd9a15dc9ef0fa49613735d 20250808/cpython-3.13.6+20250808-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +7707ee5d19a78bc64ef8a66751ec7f97b64ea06714c7b1b52e8b321c2923ead8 20250808/cpython-3.13.6+20250808-s390x-unknown-linux-gnu-install_only.tar.gz +4360a1278dd0a96b526d108c8fd23498a9d2028dd7791e510fd51ff5ea3f462a 20250808/cpython-3.13.6+20250808-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +27badce7201321a8363219e438a6205165e5b4884012b1046532203df2ec9379 20250808/cpython-3.13.6+20250808-x86_64-apple-darwin-install_only.tar.gz +4e727cdbe4057b16a170f887c0fa4227a825ac59bcda84ae946c77cc932af78c 20250808/cpython-3.13.6+20250808-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +af5cc733c33b9aa9f1d74c81a59351e9b27215486d8b6cdbc06d97646a58c953 20250808/cpython-3.13.6+20250808-x86_64-pc-windows-msvc-install_only.tar.gz +e48c13c59cc3c01b79f63c8bccec27d2db6e97f64213b8731e2077b6ed8ed52c 20250808/cpython-3.13.6+20250808-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +f844e8c8b6847628b472f7e97d8893a4e93acd5382a902b465776063668c4d64 20250808/cpython-3.13.6+20250808-x86_64-unknown-linux-gnu-install_only.tar.gz +70076dea0ff65b3c05aae1a97b4a556bf613cc73db30309e59134f9d318f4f7b 20250808/cpython-3.13.6+20250808-x86_64-unknown-linux-musl-install_only.tar.gz +43bda24c2fc073bc308bf631203b917a72640d59b59fdad4ba14503d84727012 20251031/cpython-3.10.19+20251031-aarch64-apple-darwin-install_only.tar.gz +f77a8a8aa77f3f943126fa9215a25309da4bf20398fc8f4b4eec54b5fc7570ef 20251031/cpython-3.10.19+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz +1c55d160fc4c3b93528cd6aaa2bb4ca6018a99e5a45919d33dc761a43a69f860 20251031/cpython-3.10.19+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz +21134d35721cdad4c881f35d0957cc19df9a45d194afb38a099faded3c1cfb4d 20251031/cpython-3.10.19+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz +df0db070f1eb73ab4e371eea32213ddb3500737ea5560a6f0ffd65c82af64ddc 20251031/cpython-3.10.19+20251031-s390x-unknown-linux-gnu-install_only.tar.gz +76c12e633c09c2a790f8a958a55df4495527e0718d1875310c836e757c0c7b55 20251031/cpython-3.10.19+20251031-x86_64-apple-darwin-install_only.tar.gz +cfa08a4caf2df1b43551b843c052d6a8814e2ea0c97268b021f0423646c244c3 20251031/cpython-3.10.19+20251031-x86_64-pc-windows-msvc-install_only.tar.gz +fb1caac917d7b6497bb6f5950da5f1e48d05c43a498948dd97f85760c4382d9f 20251031/cpython-3.10.19+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz +ba85013ed5ac7733fc6840168cc33ed19e9959b363dc80227d54f8fd9c92c0f4 20251031/cpython-3.10.19+20251031-x86_64-unknown-linux-musl-install_only.tar.gz +6de5572b33c65af1c9b7caf00ec593fb04cffb7e14fa393a98261bb9bc464713 20251031/cpython-3.11.14+20251031-aarch64-apple-darwin-install_only.tar.gz +38d0d1466561e15965e8d2c20f5e5be649598f55c761ecab553d087fbd217337 20251031/cpython-3.11.14+20251031-aarch64-pc-windows-msvc-install_only.tar.gz +510edb027527413c4249256194cb8ad2590b52dd93f7123b4cb341aff5d05894 20251031/cpython-3.11.14+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz +4e0bc6a818e0c6a9d7d3ebe1a95591fd84440520577aa837facc96a4b7a80e35 20251031/cpython-3.11.14+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz +16519e69297144f81b2421333bc9e0b6466cf3c84749b216b695cfb4c9deb32f 20251031/cpython-3.11.14+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz +5f9c1b203cdf34c8bff1aef69b63bbf11309bd16ca6e429d8c3651eaa2b3d080 20251031/cpython-3.11.14+20251031-s390x-unknown-linux-gnu-install_only.tar.gz +4891cbf34e8652b7bd1054b9502395e4b7e048e2e517c040fbf6c8297cb954d6 20251031/cpython-3.11.14+20251031-x86_64-apple-darwin-install_only.tar.gz +5223b83ed9e2aa5e9e17d2ebcf767956e998876339b9cde1980a47e9d4655fb6 20251031/cpython-3.11.14+20251031-x86_64-pc-windows-msvc-install_only.tar.gz +60f0bd473d861cc45d3401d9914e47ccb9fa037f88a91879ed517a62042b8477 20251031/cpython-3.11.14+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz +25e82d1e85b90a8ab724ee633a1811b1921797f5c25ee69c6595052371b91a87 20251031/cpython-3.11.14+20251031-x86_64-unknown-linux-musl-install_only.tar.gz +5e110cb821d2eb8246065d3b46faa655180c976c4e17250f7883c634a629bc63 20251031/cpython-3.12.12+20251031-aarch64-apple-darwin-install_only.tar.gz +b190fed7c2b0f6e1010f554a0d1fd191c0754c4c0718e69d9d795ae559613780 20251031/cpython-3.12.12+20251031-aarch64-pc-windows-msvc-install_only.tar.gz +81b644d166e0bfb918615af8a2363f8fcf26eccdcc60a5334b6a62c088470bac 20251031/cpython-3.12.12+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz +024f5e5678c9768d45cc24d37a8e9d265aae86c4a4602352dee3d7deba367052 20251031/cpython-3.12.12+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz +b13c57fc372c131e667a99b9680f41c0b4da571cf99ed412103c2fe9ad5ed1fb 20251031/cpython-3.12.12+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz +2bf05bdd56cdf5ea4fd9f2faf151ea4211be96a0d1f4230b85f5dcae620d6400 20251031/cpython-3.12.12+20251031-s390x-unknown-linux-gnu-install_only.tar.gz +687052a046d33be49dc95dd671816709067cf6176ed36c93ea61b1fe0b883b0f 20251031/cpython-3.12.12+20251031-x86_64-apple-darwin-install_only.tar.gz +cff398b3f520c442a1b085dd347126c10c1b03f01ccc0decd8c897a687e893f1 20251031/cpython-3.12.12+20251031-x86_64-pc-windows-msvc-install_only.tar.gz +80c3882f14e15cef8260ef5257d198e8f4371ca265887431d939e0d561de3253 20251031/cpython-3.12.12+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz +0a461330b9b89f2ea3088dde10d7a3f96aa65897b7c5ce2404fa3b5c4b8daa14 20251031/cpython-3.12.12+20251031-x86_64-unknown-linux-musl-install_only.tar.gz +eae1272a72ccce601590a10a9ca2a58199b5fcdf022aa603a527e3e2a04de9bc 20251031/cpython-3.13.9+20251031-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +1f3568d17383426d52350c2ef7c93c1a5a043198b860cb05e5d19b35f9c25cef 20251031/cpython-3.13.9+20251031-aarch64-apple-darwin-install_only.tar.gz +743ff69935ef28834621647dab30f032dfcd80315732917531eea333210941c7 20251031/cpython-3.13.9+20251031-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +20db43873d3c4c2175d866806545e4ad4ec6bb72ca95e60082a4df6c24567e8c 20251031/cpython-3.13.9+20251031-aarch64-pc-windows-msvc-install_only.tar.gz +a6e72f9de5d9b46cf6968d6a492f2401a919f9b959f8da2d87f43484b80169ee 20251031/cpython-3.13.9+20251031-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +0a56d11b0fb1662e67f892b9d5d1717aef06f24dbb8362bc25b8f784e620d44e 20251031/cpython-3.13.9+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz +0ed5c65437f875c58ba1bee2b8d261d18698d3d0347a2e66f8902fce022a2cda 20251031/cpython-3.13.9+20251031-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +99492123902bd5e9a6b1a30135061e93a2e6a11d25107a741d5a756e91054448 20251031/cpython-3.13.9+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz +584e481d9b5225ffaf02f158fb26d2818207e65fc3c6dc21a6d500277f739220 20251031/cpython-3.13.9+20251031-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +b3dce3e4ef508773521e1ee1be989fff6118f8fd1fbbd0491d7ff7dfbc98ef06 20251031/cpython-3.13.9+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz +7fa7fb912ca989ceac026a332d56a2c7d6d16ab0e94d89e690de5aade26103e2 20251031/cpython-3.13.9+20251031-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +f10e34aaa856c1b8a69c2ea4a9a6723d520443d1a957bf66dc55491334ca0c1e 20251031/cpython-3.13.9+20251031-s390x-unknown-linux-gnu-install_only.tar.gz +e2bf5fa6a3ef443ade362e08b0a19bbc172f7bfe34dabe933ccaad31d53af5da 20251031/cpython-3.13.9+20251031-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +48c0f3ca5d31e90658ef99138dc21865bb62f388ab97a1ce72cac176da194ab0 20251031/cpython-3.13.9+20251031-x86_64-apple-darwin-install_only.tar.gz +318a9a1e43dd52054327de3bccc0c5b7afde7b7f2a398ccb4d38e03d28b05386 20251031/cpython-3.13.9+20251031-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +874593f641f31ea101440c70f81768c35d4d7d6df111fde63094db67465ef787 20251031/cpython-3.13.9+20251031-x86_64-pc-windows-msvc-install_only.tar.gz +dcc29b069d0588fbd4ea29c6df840c8d1207d2a3bce8cd5cd57d1b85373b6048 20251031/cpython-3.13.9+20251031-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +6f05b91ee8c7e6dd0f9c60b95bb29130e2d623961de6578b643e80ddd83f96b6 20251031/cpython-3.13.9+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz +ad987197034185e628715da504a50613af213dc21ba6d5ccaeab3db2c464aa6c 20251031/cpython-3.13.9+20251031-x86_64-unknown-linux-musl-install_only.tar.gz +d9c7b430b25bd3837dbb03f945dbe6b7bc526c5940ca96f5db7cdc42f6b2b801 20251031/cpython-3.14.0+20251031-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +b4bcd3c6c24cab32ae99e1b05c89312b783b4d69431d702e5012fe1fdcad4087 20251031/cpython-3.14.0+20251031-aarch64-apple-darwin-install_only.tar.gz +40266e60f655e49cd1d5303295255909a4b593b08b88be6e6a55b2c9fe6ed13d 20251031/cpython-3.14.0+20251031-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +599a8b7e12439cd95a201dbdfe95cf363146b1ff91f379555dafd86b170caab9 20251031/cpython-3.14.0+20251031-aarch64-pc-windows-msvc-install_only.tar.gz +f383ef50d1da6ca511212e5ae601923b56636b87351fd5fc847e0ea0a19fa9b3 20251031/cpython-3.14.0+20251031-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +128a9cbfb9645d5237ec01704d9d1d2ac5f084464cc43c37a4cd96aa9c3b1ad5 20251031/cpython-3.14.0+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz +cb0e4ff781b856a47f0f461ceb41c78c7eeff65effd0957857ec4702ef1e1bd3 20251031/cpython-3.14.0+20251031-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +e16ca51f018e99a609faf953bd3a3aea31f45ee84262d1a517fb3abd98f1f4af 20251031/cpython-3.14.0+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz +929223470d11a55cd75f880ac3bd4969e42407e2cdf08d4e7e38ba721cf4abec 20251031/cpython-3.14.0+20251031-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +fca340d8fb7a05cd90e216ce601b25d492ed8c1a3b6a6d77703e0f15ab3711a7 20251031/cpython-3.14.0+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz +613fb1f7b249f798b52af957d181305244e936c8e5c94c84688fcdf93fe14253 20251031/cpython-3.14.0+20251031-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +c5803644970eee931bb0581b3b64511d1a8612f67bc98951a7f7ab5581a9ed04 20251031/cpython-3.14.0+20251031-s390x-unknown-linux-gnu-install_only.tar.gz +b3196f6b57bbb3dc2ee07f348f1d51117ffa376979eceafbf50c15f0f7980bf8 20251031/cpython-3.14.0+20251031-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +4e71a3ce973be377ef18637826648bb936e2f9490f64a9e4f33a49bcc431d344 20251031/cpython-3.14.0+20251031-x86_64-apple-darwin-install_only.tar.gz +b81de5fc9e783ea6dfcf1098c28a278c874999c71afbb0309f6a8b4276c769d0 20251031/cpython-3.14.0+20251031-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +39acfcb3857d83eab054a3de11756ffc16b3d49c31393b9800dd2704d1f07fdf 20251031/cpython-3.14.0+20251031-x86_64-pc-windows-msvc-install_only.tar.gz +f4acbef0fbfaf7ab31ac63986da1d93dfa1c5cb797de1dcdc1a988aa18670120 20251031/cpython-3.14.0+20251031-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +3dec1ab70758a3467ac3313bbcdabf7a9b3016db5c072c4537e3cf0a9e6290f6 20251031/cpython-3.14.0+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz +d0a2a6d3b1bb00dce2105377fda8aa79675d187f8d6d7010a42f651af25018dc 20251031/cpython-3.14.0+20251031-x86_64-unknown-linux-musl-install_only.tar.gz +12f1b16be4017181ad67904caf9e59e525b9b5d62f49105017d837e27b832959 20251031/cpython-3.15.0a1+20251031-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +3acf7aa3559b746498b18929456c5cacb84bae4e09249834cbc818970d71de87 20251031/cpython-3.15.0a1+20251031-aarch64-apple-darwin-install_only.tar.gz +54ca78dae455ece6fefbd7f5f287cc55d5ce197caf51921f6d871d15069d9489 20251031/cpython-3.15.0a1+20251031-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +1508bcd7195008479ed156aad3afbb3a3793097ed530690f0304a8107f0e53e8 20251031/cpython-3.15.0a1+20251031-aarch64-pc-windows-msvc-install_only.tar.gz +981fe8dfc6e7e1d0ffefa945a18d5c4c759bbe21722acf3a5cc7e62f16aa5f3c 20251031/cpython-3.15.0a1+20251031-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +d55c2aeece827e6bec83fd18515ee281d9ea0efaa3e2d20130db8f1c7cbb71c6 20251031/cpython-3.15.0a1+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz +088400dec25139f38eeecb48f090ff2ce06a96a1dd79fa8f1dfec1cd1786f5ef 20251031/cpython-3.15.0a1+20251031-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +c28beda791c499b16f06256339522f0002a3e9acba003e6b8374755d7be1def2 20251031/cpython-3.15.0a1+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz +938061a0a31a06672526885de36037ddefd8c4acdb09424691b7000a8c8f8d01 20251031/cpython-3.15.0a1+20251031-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +36619f576b8154e4b56643c5c4a85c352f152df2989c4e602cbbe9c2b7ded870 20251031/cpython-3.15.0a1+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz +2003e7e40bb44b3db7bca81087bfb738fe6af40e5db61cda8e23b59bf55d409e 20251031/cpython-3.15.0a1+20251031-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +5ea47be2a3a563ddd87ff510dae26b7aa7f3855ca00c5f1056ff8114c067c4e4 20251031/cpython-3.15.0a1+20251031-s390x-unknown-linux-gnu-install_only.tar.gz +64fc29e6c7a2f02a18645d968f1b3fc1d00d12a5ef3fcbb0d077fa8c62c08904 20251031/cpython-3.15.0a1+20251031-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +0ab19d3ac25f99da438b088751e5ec2421f9f6aa4292fd2dc0f8e49eb3e16bdf 20251031/cpython-3.15.0a1+20251031-x86_64-apple-darwin-install_only.tar.gz +34abc5603e1b4131f753d29b7deac865b9277912b851cbed5a149cf3e6745d3d 20251031/cpython-3.15.0a1+20251031-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +5f5d6bec2b381cfc771c49972d2a6f7b7e7ab6a1651d8fb6ef3983f3571722b3 20251031/cpython-3.15.0a1+20251031-x86_64-pc-windows-msvc-install_only.tar.gz +0e0272186d9f5169394dbc4d4d72a3f4a5762a04c2e5ac2ab1e23aa41fc8538a 20251031/cpython-3.15.0a1+20251031-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +1f356288c2b2713619cb7a4e453d33bf8882f812af2987e21e01e7ae382fefba 20251031/cpython-3.15.0a1+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz +caf5311f333eef082dd69a669ca65aceba09a08fc1e78aad602ad649106f294c 20251031/cpython-3.15.0a1+20251031-x86_64-unknown-linux-musl-install_only.tar.gz +87275619c2706affa4d1090d2ca3dad354b6d69f8b85dbfafe38785870751b9a 20251031/cpython-3.9.25+20251031-aarch64-apple-darwin-install_only.tar.gz +6112d46355857680b81849764a6cf9f38cc4cd0d1cf29d432bc12fe5aeedf9d0 20251031/cpython-3.9.25+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz +828364b6f54fa45ac2dc91f8e45d5b74306372af374a9ef16eeb2ea81253ed3f 20251031/cpython-3.9.25+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz +17467e0158e5ad04453c447d6773c23b044172276441e22e23058fd3ea053e27 20251031/cpython-3.9.25+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz +3e9539f83e67faa813fd06171199b2d33c89821dfa9a33bf6e27ad67f1b6932d 20251031/cpython-3.9.25+20251031-s390x-unknown-linux-gnu-install_only.tar.gz +ace63cfe27a9487c4d72e1cb518be01c1d985271da0b2158e813801f7d3e5503 20251031/cpython-3.9.25+20251031-x86_64-apple-darwin-install_only.tar.gz +4fb1b416482ce94d73cfa140317a670c596c830671d137b07c26afe8c461768a 20251031/cpython-3.9.25+20251031-x86_64-pc-windows-msvc-install_only.tar.gz +42834f61eb6df43432c3dd6ab9ca3fdf8c06d10a404ebdb53d6902e6b9570b08 20251031/cpython-3.9.25+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz +76593e8c889e81e82db5fe117fe15b69466f85100ab2ec0e4035aa86242b4e93 20251031/cpython-3.9.25+20251031-x86_64-unknown-linux-musl-install_only.tar.gz +3c9fdd76447c1549a0d3bc2a70c63f1daec997ab034206ac0260a03237166dbb 20251202/cpython-3.13.10+20251202-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +37afe4e77ab62ac50f197b1cb1f3bc02c82735c6be893da0996afcde5dc41048 20251202/cpython-3.13.10+20251202-aarch64-apple-darwin-install_only.tar.gz +cdb7141327bdc244715b25752593e2c9eeb3cc2764f37dfe81cfbc92db9d6d57 20251202/cpython-3.13.10+20251202-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +9060d644bd32ac0e0af970d0b21e207e6ff416b7c4dc26ffc4f9b043fb45b463 20251202/cpython-3.13.10+20251202-aarch64-pc-windows-msvc-install_only.tar.gz +6d277221fa4b172e00b29c7158ca9661917bc8db9a0084b1a0ff5c3a0ba8b648 20251202/cpython-3.13.10+20251202-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +c68280591cda1c9515a04809fa6926020177e8e5892300206e0496ea1d10290e 20251202/cpython-3.13.10+20251202-aarch64-unknown-linux-gnu-install_only.tar.gz +d265d8d1c51e25ed70279540223589f79cf99ad00b50d28b6150c2658c973885 20251202/cpython-3.13.10+20251202-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +1507e5528bd88131dc742a2941176aceea1838bc09860c21f179285b7865133b 20251202/cpython-3.13.10+20251202-ppc64le-unknown-linux-gnu-install_only.tar.gz +ec411b4a2d167c3be0a9aeb3905e045d62c8e3c3db0caeade5d47d5f60b98dd0 20251202/cpython-3.13.10+20251202-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +70169e916860b2e5b34c37c302d699eb2b8f24f28090968881942a37aeb7ed08 20251202/cpython-3.13.10+20251202-riscv64-unknown-linux-gnu-install_only.tar.gz +4fc6443948bf5b729481ea02cc5c68e80cd0da42631f6936587a2b8fd45bc62c 20251202/cpython-3.13.10+20251202-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +c5448863b64aacae62f3a213a6e6cf94ec63f96ee4d518491cd62fd3c81d952f 20251202/cpython-3.13.10+20251202-s390x-unknown-linux-gnu-install_only.tar.gz +6ce608684df0f90350c7a1742e9685a7782d9b26ec99d1bd9d55c8cf9a405040 20251202/cpython-3.13.10+20251202-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +a02761a4f189f71c0512e88df7ca2843696d61da659e47f8a5c8a9bd2c0d16f4 20251202/cpython-3.13.10+20251202-x86_64-apple-darwin-install_only.tar.gz +6a8b0372ded655e0d55318089fbce3122a446e69bcd120c79aaadfe9b017299c 20251202/cpython-3.13.10+20251202-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +8b00014c7c35f9ad4cb1c565f067500bacc4125c8bc30e4389ee0be9fd6ffa3d 20251202/cpython-3.13.10+20251202-x86_64-pc-windows-msvc-install_only.tar.gz +e39127fbe8d2ae7d86099f18b4da0918f9b60ce73ed491774d6dcfaa42b5c9ae 20251202/cpython-3.13.10+20251202-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +0cac1495fff920219904b1d573aaec0df54d549c226cb45f5c60cb6d2c72727a 20251202/cpython-3.13.10+20251202-x86_64-unknown-linux-gnu-install_only.tar.gz +04108190972ac98e13098abd972ec3f4f8b0880f83c0bb68249ce1a6164fa041 20251202/cpython-3.13.10+20251202-x86_64-unknown-linux-musl-install_only.tar.gz +61f38e947449cf00f32f0838e813358f6bf61025d0797531e5b8b8b175c617f0 20251202/cpython-3.14.1+20251202-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +cdf1ba0789f529fa34bb5b5619c5da9757ac1067d6b8dd0ee8b78e50078fc561 20251202/cpython-3.14.1+20251202-aarch64-apple-darwin-install_only.tar.gz +ddb10b645de2b1f6f2832a80b115a9cd34a4a760249983027efe46618a8efc48 20251202/cpython-3.14.1+20251202-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +19129cf8b4d68c4e64c25bae43bca139d871267b59cf7f02b9dcf25f0bf59497 20251202/cpython-3.14.1+20251202-aarch64-pc-windows-msvc-install_only.tar.gz +1a88a1fe21eb443d280999464b1a397605a7ca950d8ab73813ca6868835439a2 20251202/cpython-3.14.1+20251202-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +5dde7dba0b8ef34c0d5cb8a721254b1e11028bfc09ff06664879c245fe8df73f 20251202/cpython-3.14.1+20251202-aarch64-unknown-linux-gnu-install_only.tar.gz +7207b736ed2569f307649ffd4b615a5346631bc244730b8702babee377cef528 20251202/cpython-3.14.1+20251202-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +d2774701d53e2ac06f8c8c8e52dfa4ff346890de9b417c9a7664195443a4c766 20251202/cpython-3.14.1+20251202-ppc64le-unknown-linux-gnu-install_only.tar.gz +d1356ccd279920edc31bf0350674d966beb9522f9503846ed7855dbb109ccc14 20251202/cpython-3.14.1+20251202-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +af840506efbcd5026d9140c0a0230e45e46bb1f339a65c10a22875930b2c0159 20251202/cpython-3.14.1+20251202-riscv64-unknown-linux-gnu-install_only.tar.gz +477758eabc06dbc7e5e5d16e97c4672478acd409f420dd2e1b84d3452c0668d1 20251202/cpython-3.14.1+20251202-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +43f8f79bf4c66689d2019f193671d1df3e5e5dbb293382036285e8ce55fc55bb 20251202/cpython-3.14.1+20251202-s390x-unknown-linux-gnu-install_only.tar.gz +c2cb2a9b44285fbc13c3c9b7eea813db6ed8d94909406b059db7afd39b32e786 20251202/cpython-3.14.1+20251202-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +f25ce050e1d370f9c05c9623b769ffa4b269a6ae17e611b435fd2b8b09972a88 20251202/cpython-3.14.1+20251202-x86_64-apple-darwin-install_only.tar.gz +8ef7048315cac6d26bdbef18512a87b1a24fffa21cec86e32f9a9425f2af9bf6 20251202/cpython-3.14.1+20251202-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +cb478a5a37eb93ce4d3c27ae64d211d6a5a42475ae53f666a8d1570e71fcf409 20251202/cpython-3.14.1+20251202-x86_64-pc-windows-msvc-install_only.tar.gz +c5d5b89aab7de683e465e36de2477a131435076badda775ef6e9ea21109c1c32 20251202/cpython-3.14.1+20251202-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +a72f313bad49846e5e9671af2be7476033a877c80831cf47f431400ccb520090 20251202/cpython-3.14.1+20251202-x86_64-unknown-linux-gnu-install_only.tar.gz +15d50b15713097c38c67b1a06a0498ad102377f9b3999e98e4eefd6bf91bd82d 20251202/cpython-3.14.1+20251202-x86_64-unknown-linux-musl-install_only.tar.gz +4213058b7fcd875596c12b58cd46a399358b0a87ecde4b349cbdd00cf87ed79a 20251209/cpython-3.13.11+20251209-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +295a9f7bc899ea1cc08baf60bbf511bdd1e4a29b2dd7e5f59b48f18bfa6bf585 20251209/cpython-3.13.11+20251209-aarch64-apple-darwin-install_only.tar.gz +6daf6d092c7294cfe68c4c7bf2698ac134235489c874b3bf796c7972b9dbba30 20251209/cpython-3.13.11+20251209-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +ba646d0c3b7dd7bdfb770d9b2ebd6cd2df02a37fda90c9c79a7cf59c7df6f165 20251209/cpython-3.13.11+20251209-aarch64-pc-windows-msvc-install_only.tar.gz +290ca3bd0007db9e551f90b08dfcb6c1b2d62c33b2fc3e9a43e77d385d94f569 20251209/cpython-3.13.11+20251209-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +ea1e678e6e82301bb32bf3917732125949b6e46d541504465972024a3f165343 20251209/cpython-3.13.11+20251209-aarch64-unknown-linux-gnu-install_only.tar.gz +09d4b50f8abb443f7e3af858c920aa61c2430b0954df465e861caa7078e55e69 20251209/cpython-3.13.11+20251209-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +7660e53aad9d35ee256913c6d98427f81f078699962035c5fa8b5c3138695109 20251209/cpython-3.13.11+20251209-ppc64le-unknown-linux-gnu-install_only.tar.gz +5406f2a7cacafbd2aac3ce2de066a0929aab55423824276c36e04cb83babc36c 20251209/cpython-3.13.11+20251209-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +763fa1548e6a432e9402916e690c74ea30f26dcd2e131893dd506f72b87c27c9 20251209/cpython-3.13.11+20251209-riscv64-unknown-linux-gnu-install_only.tar.gz +3984b67c4292892eaccdd1c094c7ec788884c4c9b3534ab6995f6be96d5ed51d 20251209/cpython-3.13.11+20251209-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +ffb6af51fbfabfc6fbc4e7379bdec70c2f51e972b1d2f45c053493b9da3a1bbe 20251209/cpython-3.13.11+20251209-s390x-unknown-linux-gnu-install_only.tar.gz +d6f489464045d6895ae68b0a04a9e16477e74fe3185a75f3a9a0af8ccd25eade 20251209/cpython-3.13.11+20251209-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +dac4a0a0a9b71f6b02a8b0886547fa22814474239bffb948e3e77185406ea136 20251209/cpython-3.13.11+20251209-x86_64-apple-darwin-install_only.tar.gz +bb9a29a7ba8f179273b79971da6aaa7be592d78c606a63f99eff3e4c12fb0fae 20251209/cpython-3.13.11+20251209-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +87822417007045a28a7eccc47fe67b8c61265b99b10dbbfa24d231a3622b1c27 20251209/cpython-3.13.11+20251209-x86_64-pc-windows-msvc-install_only.tar.gz +33f89c957d986d525529b8a980103735776f4d20cf52f55960a057c760188ac3 20251209/cpython-3.13.11+20251209-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +1ffa06d714a44aea14c0c54c30656413e5955a6c92074b4b3cb4351dcc28b63b 20251209/cpython-3.13.11+20251209-x86_64-unknown-linux-gnu-install_only.tar.gz +969fe24017380b987c4e3ce15e9edf82a4618c1e61672b2cc9b021a1c98eae78 20251209/cpython-3.13.11+20251209-x86_64-unknown-linux-musl-install_only.tar.gz +d6d17b8ef28326552cdeb2a7541c8a0cb711b378df9b93ebdb461dca065edfea 20251209/cpython-3.14.2+20251209-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +2f74bd26bd16487aca357c879d11f7b16c0521328e5148a1930ab6357bcb89fe 20251209/cpython-3.14.2+20251209-aarch64-apple-darwin-install_only.tar.gz +43aac5bb4cdba71fc6775d26f47348d573a0b1210911438be71d7d96f4b18b51 20251209/cpython-3.14.2+20251209-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +0be0d2557d73efa7f6f3f99679f05252d57fe2aad2d81cac3cad410a9b1eacbd 20251209/cpython-3.14.2+20251209-aarch64-pc-windows-msvc-install_only.tar.gz +adfcb90f3a7e1b3fbc6a99f9c8c8dce1f2e26ea72b724bbe4e9fa39e81e2b0db 20251209/cpython-3.14.2+20251209-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +869af31b2963194e8a2ecfadc36027c4c1c86a10f4960baec36dadb41b2acf02 20251209/cpython-3.14.2+20251209-aarch64-unknown-linux-gnu-install_only.tar.gz +2b1ce0c5a5f5e5add7e4f934f5bd35ac41660895a30b3098db7f7303d6952a4f 20251209/cpython-3.14.2+20251209-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +86129976403fb5d64cf576329f94148f28cf6f82834e94df81ff31e9d5f404e0 20251209/cpython-3.14.2+20251209-ppc64le-unknown-linux-gnu-install_only.tar.gz +4efb610fa07a6ee2639d14d78fc3b6ecb47431c14e1e4bda03c7f7dd60a5c1e5 20251209/cpython-3.14.2+20251209-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +318dceecf119ea903aef1fb03a552cc592ecd61c08da891b68f5755e21e13511 20251209/cpython-3.14.2+20251209-riscv64-unknown-linux-gnu-install_only.tar.gz +e62f3bb3e66dac6c459690f9e9cd8cc2f6fe1dcf8bfed452af4c3df24cd7874f 20251209/cpython-3.14.2+20251209-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +53875c849a14194344ead1d9cd1e128cadd42a4b83c35eeb212417909ef05a6a 20251209/cpython-3.14.2+20251209-s390x-unknown-linux-gnu-install_only.tar.gz +1fd76c79f7fc1753e8d2ed2f71406c0b65776c75f3e95ed99ffde8c95af2adc1 20251209/cpython-3.14.2+20251209-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +58fa3e17d13ab956fd11055fb774c98ecfddcdf3b588e5f2369bdbc14ef9d76a 20251209/cpython-3.14.2+20251209-x86_64-apple-darwin-install_only.tar.gz +9927951e3997c186d2813ca1a0f4a8f5a2f771463f7f8ad0752fd3d2be2b74e4 20251209/cpython-3.14.2+20251209-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +0d660bba9f58cb552e7e99e1f96a9c67b41618c9b8d29f9f3515fe2b5ad1966e 20251209/cpython-3.14.2+20251209-x86_64-pc-windows-msvc-install_only.tar.gz +3728872ffd74989a7b4bbf3f0c629ae8fe821cda2bd6544012c1b92b9f5d5a5b 20251209/cpython-3.14.2+20251209-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +121c3249bef497adf601df76a4d89aed6053fc5ec2f8c0ec656b86f0142e8ddd 20251209/cpython-3.14.2+20251209-x86_64-unknown-linux-gnu-install_only.tar.gz +71639cc5d1fb79840467531c5b53ca77170a58edd3f7e2d29330dd736e477469 20251209/cpython-3.14.2+20251209-x86_64-unknown-linux-musl-install_only.tar.gz +5b34488580df13df051a2e84e43cfca2ab28fdd7a61052f35988eb8b481b894a 20251209/cpython-3.15.0a2+20251209-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +5851f3744fbd39e3e323844cf4f68d7763fb25546aa5ffbb71b1b5ab69c56616 20251209/cpython-3.15.0a2+20251209-aarch64-apple-darwin-install_only.tar.gz +3d99152b4e29b947fb1cfc8d035d1d511e50aeed72886ff4a5fd0a3694bd0b51 20251209/cpython-3.15.0a2+20251209-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +39bc2fcac13aeba7d650f76badf63350a81c86167a62174cb092eab7a749f4a5 20251209/cpython-3.15.0a2+20251209-aarch64-pc-windows-msvc-install_only.tar.gz +0c2c83236f6e28c103e2660a82be94b2459ee8cfdd90f5dd82f0d503ca2aec09 20251209/cpython-3.15.0a2+20251209-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +17ba65d669be3052524e03b4d1426c072ef38df2a9065ff4525d1f4d1bc9f82c 20251209/cpython-3.15.0a2+20251209-aarch64-unknown-linux-gnu-install_only.tar.gz +216842df2377fd032f279ded7fd23d7bdbd92d4c1fa7619523bc0dbdef5bd212 20251209/cpython-3.15.0a2+20251209-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +5585bd7c5eefe28b9bf544d902cad9a2f81f33c618f2a1d3c006cbfcdec77abc 20251209/cpython-3.15.0a2+20251209-ppc64le-unknown-linux-gnu-install_only.tar.gz +2a8b56f318d2e21b01b54909554c53d81871b9bb05d23ea7808dde9acec4dc7e 20251209/cpython-3.15.0a2+20251209-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +bb7252edaffd422bd1c044a4764dfcf83a5d7159942f445abbef524e54ea79a0 20251209/cpython-3.15.0a2+20251209-riscv64-unknown-linux-gnu-install_only.tar.gz +06c4ca3983aad20723f68786e3663ab49fee1bf09326f341649205ed79d34fc6 20251209/cpython-3.15.0a2+20251209-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +03a90ffa9f92d4cf4caeefb9d15f0b39c05c1e60ade6688f32165f957db4f8f3 20251209/cpython-3.15.0a2+20251209-s390x-unknown-linux-gnu-install_only.tar.gz +4d8102b70ea9fe726ee3ae9ad9e9bc4cbe0b6ed18f7989c81aef81de578f0163 20251209/cpython-3.15.0a2+20251209-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +cee576de4919cd422dbc31eb85d3c145ee82acec84f651daaf32dc669b5149c9 20251209/cpython-3.15.0a2+20251209-x86_64-apple-darwin-install_only.tar.gz +6ff71bac78d650ce621fe6db49f06290e48bcceb61f69cccc7728584f70b6346 20251209/cpython-3.15.0a2+20251209-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +e538475ee249eacf63bfdae0e70af73e9c47360e6dd3d6825e7a35107e177de5 20251209/cpython-3.15.0a2+20251209-x86_64-pc-windows-msvc-install_only.tar.gz +70f552e213734c0e260a57603bee504dd7ed0e78a10558b591e724ea8730fef5 20251209/cpython-3.15.0a2+20251209-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +58addaabfab2de422180d32543fb3878ffc984c8a2e4005ff658a5cd83b31fc7 20251209/cpython-3.15.0a2+20251209-x86_64-unknown-linux-gnu-install_only.tar.gz +dcf844400dc2e7f5f3604e994532e4d49db45f4deefe9afdf6809ca1bc6532ee 20251209/cpython-3.15.0a2+20251209-x86_64-unknown-linux-musl-install_only.tar.gz +e7cf7bc717082bb38f5ca75988ecd8e5dbc1b0535192129371e30235d29d67b5 20260325/cpython-3.13.12+20260325-aarch64-apple-darwin-freethreaded-install_only.tar.gz +688da81bcaa6ed91792397c7d5433b13a4f02f021f940637c3972639bc516dca 20260325/cpython-3.13.12+20260325-aarch64-apple-darwin-install_only.tar.gz +54187be504ea5be2f8ed455e9377112bb04f34c9259eae263779e56b403e3e3f 20260325/cpython-3.13.12+20260325-aarch64-pc-windows-msvc-freethreaded-install_only.tar.gz +d2c8b00044cd2e4c5fc7e697e63d5e481ed44b87c2def0beb42991d59f65d930 20260325/cpython-3.13.12+20260325-aarch64-pc-windows-msvc-install_only.tar.gz +9794866e9a464f349055d791ea8f14dfa7f339ecac5aa9b1084bb2ce388fc598 20260325/cpython-3.13.12+20260325-aarch64-unknown-linux-gnu-freethreaded-install_only.tar.gz +31c6e61eed48ca4e156d0e473025a792338641109e8277a63518ded438390c96 20260325/cpython-3.13.12+20260325-aarch64-unknown-linux-gnu-install_only.tar.gz +1a8a4a97f33740a1cb9fa480321818cdc610c79c9137e511e76dc53635615494 20260325/cpython-3.13.12+20260325-ppc64le-unknown-linux-gnu-freethreaded-install_only.tar.gz +654939bc40d5f76f08eb17335bb19e9efa11eb48a0818eda2293a3f7c3570ae7 20260325/cpython-3.13.12+20260325-ppc64le-unknown-linux-gnu-install_only.tar.gz +178d20e568c25abcca9b1dbedf77e904cc3f10a79d22e31f87ddabd2d28f87dc 20260325/cpython-3.13.12+20260325-riscv64-unknown-linux-gnu-freethreaded-install_only.tar.gz +fc7e1fb553c47b831ed7fa529575145207f000f967513f7b9ea809cce006ed79 20260325/cpython-3.13.12+20260325-riscv64-unknown-linux-gnu-install_only.tar.gz +d23c93ea7502420c71e4acf02999c72ab80797d51843b1b6a315ca7bac3cb780 20260325/cpython-3.13.12+20260325-s390x-unknown-linux-gnu-freethreaded-install_only.tar.gz +7d7919358e88fcc672b061be8c2316c3a604c7074200515d7104166ed611f7f9 20260325/cpython-3.13.12+20260325-s390x-unknown-linux-gnu-install_only.tar.gz +6aff211689e30889cfe90b0b2a76b6f5a7b9e6e0bb28d6a66fd5ba35d36dc78a 20260325/cpython-3.13.12+20260325-x86_64-apple-darwin-freethreaded-install_only.tar.gz +7411e47939783708381017a90944a69641ac84d43f74fb6e2d52576c599a2717 20260325/cpython-3.13.12+20260325-x86_64-apple-darwin-install_only.tar.gz +088754e90ff22962a4ab6f7cb6bdabe5d9e7618266595df2cf7b211766e15132 20260325/cpython-3.13.12+20260325-x86_64-pc-windows-msvc-freethreaded-install_only.tar.gz +5b4093f92d9bffcb0d92aea050f3d77d5a4fc8e918b31cea000ee4b3ca751f1d 20260325/cpython-3.13.12+20260325-x86_64-pc-windows-msvc-install_only.tar.gz +6070796c894ef0a25b5a944c8c0327e155df534302e1612a5ddd57d177ddadf7 20260325/cpython-3.13.12+20260325-x86_64-unknown-linux-gnu-freethreaded-install_only.tar.gz +ebb1051ca2822b9803f46a5f10b6d51d153189ef1b1f1e142f733c0cbeaf86eb 20260325/cpython-3.13.12+20260325-x86_64-unknown-linux-gnu-install_only.tar.gz +b2e9400731c7f18069ec2804ba87a404385fe440f93b7dcb59004b9f56651202 20260325/cpython-3.13.12+20260325-x86_64-unknown-linux-musl-install_only.tar.gz +21f297bc1e0503fa077364417e2213c60951d94fd65d837ae6d9d9201ae27483 20260325/cpython-3.14.3+20260325-aarch64-apple-darwin-freethreaded-install_only.tar.gz +80c996c23aab828134821f078a8a77a6f33f3f2c14000f071718c540e20c64d4 20260325/cpython-3.14.3+20260325-aarch64-apple-darwin-install_only.tar.gz +d0e355df7362d12542108f78b3f8085b21e6824420769117c262ac86569bb2a7 20260325/cpython-3.14.3+20260325-aarch64-pc-windows-msvc-freethreaded-install_only.tar.gz +b35fe7c2fe169574f382cef125e95cbd904ddcb98fc337167356371b6d2e8c60 20260325/cpython-3.14.3+20260325-aarch64-pc-windows-msvc-install_only.tar.gz +112cf42bdf4d04f69ff4f9bf18c8ce45f494bac1645310bfdeff6f2ffb30dd9a 20260325/cpython-3.14.3+20260325-aarch64-unknown-linux-gnu-freethreaded-install_only.tar.gz +6faf5478f910741c477830f5fd842011208af0f9678faf77106c9421b325bfc1 20260325/cpython-3.14.3+20260325-aarch64-unknown-linux-gnu-install_only.tar.gz +9d7e5ba8020fd942a89a57179d9015eb0237c2d95cdbf8378639723663f11706 20260325/cpython-3.14.3+20260325-ppc64le-unknown-linux-gnu-freethreaded-install_only.tar.gz +5eafe32e12f33f98c40de920482b013170dcf97d8c7f5dc780271ccf4cded76a 20260325/cpython-3.14.3+20260325-ppc64le-unknown-linux-gnu-install_only.tar.gz +32955ad52ec7931e76f4509134a2ba5a6ba6ea0cd55e05217c1ccca3967c4a5c 20260325/cpython-3.14.3+20260325-riscv64-unknown-linux-gnu-freethreaded-install_only.tar.gz +481d3faef258964e57b7102c63de12b2bb388c7ed07cfe456f33e63b4e061202 20260325/cpython-3.14.3+20260325-riscv64-unknown-linux-gnu-install_only.tar.gz +7a1d36a1567cd747411c9c2bc7e2b5c1ac277ea7c734f74b158b94101fd5ea43 20260325/cpython-3.14.3+20260325-s390x-unknown-linux-gnu-freethreaded-install_only.tar.gz +d706eae2f4d963187b7c866603aed75d7eb3ea59590b06fb34f5fd7d0fe8e432 20260325/cpython-3.14.3+20260325-s390x-unknown-linux-gnu-install_only.tar.gz +3788781d0f9704f91ab5f7ad2a040d26b0f9b6aba0a2535db21755aebb69e620 20260325/cpython-3.14.3+20260325-x86_64-apple-darwin-freethreaded-install_only.tar.gz +847a49fea36c066f8df7a57cd8c4c02d17667e25d30b7930e8f8ba15e72d7efc 20260325/cpython-3.14.3+20260325-x86_64-apple-darwin-install_only.tar.gz +99dd7e425b3dac23e03f37787d77ee0af531e96b1c748275185342bc6642eb6b 20260325/cpython-3.14.3+20260325-x86_64-pc-windows-msvc-freethreaded-install_only.tar.gz +8b4e1329c4901ce2c0f1c20ac5d2ffa62fc13f12e26b5d1e5a1000f910f980d4 20260325/cpython-3.14.3+20260325-x86_64-pc-windows-msvc-install_only.tar.gz +20d3bcd7f175e09fa08f4cb3039e5f90fe7e4ce2476534e83f5aa21eb0d7cee9 20260325/cpython-3.14.3+20260325-x86_64-unknown-linux-gnu-freethreaded-install_only.tar.gz +18270c5a7b1a572599df5e68b497ba5254811dac43ba6f542245807d821fcb44 20260325/cpython-3.14.3+20260325-x86_64-unknown-linux-gnu-install_only.tar.gz +726a28734d2878a637b0d16ce07ce24c7d6ca1043d8e6f4a23b1b0a3478eedb9 20260325/cpython-3.14.3+20260325-x86_64-unknown-linux-musl-install_only.tar.gz +f76cc83c7db16cfc8794bf6e44d834152b57d8bab4e04e823cbc59ed23ec22f8 20260414/cpython-3.10.20+20260414-aarch64-apple-darwin-install_only.tar.gz +64932c8e8bbdf9d6b66ee85934f6f8ad1d18218b51a87ea06cefd3b84554a3e4 20260414/cpython-3.10.20+20260414-aarch64-unknown-linux-gnu-install_only.tar.gz +76b48eb26ef274045772186e63431419294c41baf6d5a372b722d4c9e711082e 20260414/cpython-3.10.20+20260414-ppc64le-unknown-linux-gnu-install_only.tar.gz +76e1ec72717d17493976fc176ec661f02412666d4f19e50908d8e4303c0511d5 20260414/cpython-3.10.20+20260414-riscv64-unknown-linux-gnu-install_only.tar.gz +2edf241199d11a3ef79a312737c1bcdb86908352c585ca14b667539080630e85 20260414/cpython-3.10.20+20260414-s390x-unknown-linux-gnu-install_only.tar.gz +95a2d794b8981723095190fa94b574ceb4272bb49d83b9e418bb90341e304d09 20260414/cpython-3.10.20+20260414-x86_64-apple-darwin-install_only.tar.gz +0d828683d30185ab9f1110ad2194ef384cef0533b8e0da7e03ce837548841788 20260414/cpython-3.10.20+20260414-x86_64-pc-windows-msvc-install_only.tar.gz +303047011b2c9f58504a930fc974d84547477cf69a3f2962f25552e2395c13af 20260414/cpython-3.10.20+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +84eb198d318f8b1b8bf59eef5d30d742e13afd97c213fa229578f8fdab0c406f 20260414/cpython-3.10.20+20260414-x86_64-unknown-linux-musl-install_only.tar.gz +a57ffd435652092d16b30e783f9826c55e9c64b0f0a72cbae0a9f39e663137fb 20260414/cpython-3.11.15+20260414-aarch64-apple-darwin-install_only.tar.gz +a882abe4876985c9dc3d433420548506fb0cc9bb9d9fe336a2d3aaf28922aa45 20260414/cpython-3.11.15+20260414-aarch64-pc-windows-msvc-install_only.tar.gz +77836944ae15b74e0b25bdc68a4703a340f2ccb684effc0f45fbd7910e1a1f39 20260414/cpython-3.11.15+20260414-aarch64-unknown-linux-gnu-install_only.tar.gz +30a2107f000dbe304820627cbe2cc257027c20f3241d96e6c7df796b69ac2062 20260414/cpython-3.11.15+20260414-ppc64le-unknown-linux-gnu-install_only.tar.gz +373b98fbf2d04099139a2f6be57593714382ed790be7e7419e358830c23ddd0f 20260414/cpython-3.11.15+20260414-riscv64-unknown-linux-gnu-install_only.tar.gz +7838efa839158c80568de35ac78d438f564f4c32272a2fe7d9e14a9b351d1a62 20260414/cpython-3.11.15+20260414-s390x-unknown-linux-gnu-install_only.tar.gz +317055d80e553764feeaef432d833dd8385c14b83465a8b3fa7c2b7819cba681 20260414/cpython-3.11.15+20260414-x86_64-apple-darwin-install_only.tar.gz +8e69ecf1d9fc194e029aafa608d483bf24ccaa8f56d456d7009f20462d62ad23 20260414/cpython-3.11.15+20260414-x86_64-pc-windows-msvc-install_only.tar.gz +8b14030dd3af9ea7f7c51b4c90feb04afd8a8f45435727e67b875270bd08f3bc 20260414/cpython-3.11.15+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +ca92d3a68a39fa330498b09714733f347bead7313ba9d9b7fbed837aa4ba7796 20260414/cpython-3.11.15+20260414-x86_64-unknown-linux-musl-install_only.tar.gz +8966b2bcd9fa03ba22c080ad15a86bc12e41a00122b16f4b3740e302261124d9 20260414/cpython-3.12.13+20260414-aarch64-apple-darwin-install_only.tar.gz +f55326c894fde76fc0faffe95d2bce60be533c88a8c44c1b88bbbc17bf6a5cd5 20260414/cpython-3.12.13+20260414-aarch64-pc-windows-msvc-install_only.tar.gz +355d981eafb9b2870af79ddc106ced7266b6f6d2101d8fbcb05620fa386642b9 20260414/cpython-3.12.13+20260414-aarch64-unknown-linux-gnu-install_only.tar.gz +4aef4cffe73c4a65ea486f14d684a9ad3f831a354174d163bb531b5baa70fc49 20260414/cpython-3.12.13+20260414-ppc64le-unknown-linux-gnu-install_only.tar.gz +c2629d69324155132343913f064be93509bd162531e08a292e50c3973ec8b5db 20260414/cpython-3.12.13+20260414-riscv64-unknown-linux-gnu-install_only.tar.gz +e5baafd64180f45165d2751b25d1bcc89254eefc7926f3ab341fc61b541d7606 20260414/cpython-3.12.13+20260414-s390x-unknown-linux-gnu-install_only.tar.gz +801b03fbe004181d55a02ebd8b4e04d74973e70d716062aebe3b3cf32e9be297 20260414/cpython-3.12.13+20260414-x86_64-apple-darwin-install_only.tar.gz +c5a9e011e284c49c48106ca177342f3e3f64e95b4c6652d4a382cc7c9bb1cc46 20260414/cpython-3.12.13+20260414-x86_64-pc-windows-msvc-install_only.tar.gz +cdcf8724d46e4857f8db5ee9f4252dc2f5da34f7940294ec6b312389dd3f41e0 20260414/cpython-3.12.13+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +d10e971238c130fdf25e577c6538a3effa5589d5fcf53665e3c711edd6a6ff2f 20260414/cpython-3.12.13+20260414-x86_64-unknown-linux-musl-install_only.tar.gz +2662b1c3f6d5ed4d02d877c07f9384acc0d18b9046d54cd2853dad3ca172784f 20260414/cpython-3.13.13+20260414-aarch64-apple-darwin-freethreaded-install_only.tar.gz +c652dad552122cd2e76968ec41c803f8222038169b11310dba0c85928265f5c1 20260414/cpython-3.13.13+20260414-aarch64-apple-darwin-install_only.tar.gz +c6c1aae3809ef585271f6f1bb3643a2c6e0c82b811b93284c6218b31f0b931d7 20260414/cpython-3.13.13+20260414-aarch64-pc-windows-msvc-freethreaded-install_only.tar.gz +586ba71c75f341e1d111399b7f719ae784dc11e8672e93e017388f28684226d0 20260414/cpython-3.13.13+20260414-aarch64-pc-windows-msvc-install_only.tar.gz +46ac7e9476b938ef19f71029a77d28ed1e201335dd0aa0237fcfed2e5ce0ee61 20260414/cpython-3.13.13+20260414-aarch64-unknown-linux-gnu-freethreaded-install_only.tar.gz +6a65f68043d7fadcd580415493d2929d1fd686013f9ae44ddbd3a81307ab256d 20260414/cpython-3.13.13+20260414-aarch64-unknown-linux-gnu-install_only.tar.gz +abe26a6cab523a5d00d75f1353cbad9c5dc04262dcb0dc4a2b47d02384e2a7d7 20260414/cpython-3.13.13+20260414-ppc64le-unknown-linux-gnu-freethreaded-install_only.tar.gz +aef73894107300264222b19e357baf5bad616b1c4bf5daa5c3b97cfee8f5ed7b 20260414/cpython-3.13.13+20260414-ppc64le-unknown-linux-gnu-install_only.tar.gz +eea71fc3625fcc2408171b17fb97e0c6286ed60ed225ca7fd6e2fc5d9cc21dce 20260414/cpython-3.13.13+20260414-riscv64-unknown-linux-gnu-freethreaded-install_only.tar.gz +f47c09f8e7f2fb0bc4afe52422705af4016c8d3ec1cf004b67bb56a86caa62cb 20260414/cpython-3.13.13+20260414-riscv64-unknown-linux-gnu-install_only.tar.gz +dd8b6161c4af3c2f5f29b3535decdcf146ce90d7a062687c9e5229b4151198b0 20260414/cpython-3.13.13+20260414-s390x-unknown-linux-gnu-freethreaded-install_only.tar.gz +4d205af9654e1f33cefd23ff798af470e565f3ac0eba18d2f98f18a2abd07166 20260414/cpython-3.13.13+20260414-s390x-unknown-linux-gnu-install_only.tar.gz +27edbaad8f0c1a8814647d24df3f87eb13c89bbc2cb90e2fc23d8fa48dd64b15 20260414/cpython-3.13.13+20260414-x86_64-apple-darwin-freethreaded-install_only.tar.gz +540337412d2c4220e99280f741dbf45c1e3da3a39edaaab20c6ba1d53e1692ef 20260414/cpython-3.13.13+20260414-x86_64-apple-darwin-install_only.tar.gz +002c07103bfbe1b889f41eb1b9fade81651a21aed35a3512e2a916c5d7903cfe 20260414/cpython-3.13.13+20260414-x86_64-pc-windows-msvc-freethreaded-install_only.tar.gz +ee0cb26453d6e025d36502d765c1639c34830355e46ab3ad31c0360bc4cd9b79 20260414/cpython-3.13.13+20260414-x86_64-pc-windows-msvc-install_only.tar.gz +a183ec7a10c38ab8c3f19968614f1e69ec697199e94525583662dfbc22b70d9a 20260414/cpython-3.13.13+20260414-x86_64-unknown-linux-gnu-freethreaded-install_only.tar.gz +e5ec3b2c5693215d153c434ac018e75511b2c4f96d2bce30468a477cb3a89d5e 20260414/cpython-3.13.13+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +24ac6bf80dd2991c8be348f777c96c6eb69b71e78d8fa28c09beb3ddca015a47 20260414/cpython-3.13.13+20260414-x86_64-unknown-linux-musl-install_only.tar.gz +a4bfd77675740a0362c137b094f3cd9995775e8e6c0a7874a095dd055fd1ea99 20260414/cpython-3.14.4+20260414-aarch64-apple-darwin-freethreaded-install_only.tar.gz +8b7865e511b17093e090449bf71eb52933c17d45ad5257ddeacaffbb2c7239df 20260414/cpython-3.14.4+20260414-aarch64-apple-darwin-install_only.tar.gz +0458cb9885c30df690cdf304a16ec335cbc7344792ef0e8a904614b24a61316d 20260414/cpython-3.14.4+20260414-aarch64-pc-windows-msvc-freethreaded-install_only.tar.gz +82613380d582d806e562d7701496c34c87753ab13c37aa0afe2039003651f389 20260414/cpython-3.14.4+20260414-aarch64-pc-windows-msvc-install_only.tar.gz +6d84fb153ccb5cb650652aadc490d99881a8d9b68cf273d44cb553e8cd087734 20260414/cpython-3.14.4+20260414-aarch64-unknown-linux-gnu-freethreaded-install_only.tar.gz +5c8db1c21023316adad827a46d917bbbd6a85ae4e39bc3a58febda712c2f963d 20260414/cpython-3.14.4+20260414-aarch64-unknown-linux-gnu-install_only.tar.gz +b5e025e340d0faa1772ef234e320401b0aa5cf6c9d16ed63a8c44be7c531bc58 20260414/cpython-3.14.4+20260414-ppc64le-unknown-linux-gnu-freethreaded-install_only.tar.gz +055977a09de092744bbb22db64144e6afef8592eaac5e2bce4cca33f2592281a 20260414/cpython-3.14.4+20260414-ppc64le-unknown-linux-gnu-install_only.tar.gz +4373553133eb4712bc10f720da29e091a23153f587fdb2c38f1fb105e70db53a 20260414/cpython-3.14.4+20260414-riscv64-unknown-linux-gnu-freethreaded-install_only.tar.gz +e959df167c502fb0bbcacc31a997e25c6b0ff6b5e496321b691955aa702d0c09 20260414/cpython-3.14.4+20260414-riscv64-unknown-linux-gnu-install_only.tar.gz +a6797ad05c7d7f74a2cea28bf012f9199f4d6c1ed6d09f7adfeb9b3c538c6258 20260414/cpython-3.14.4+20260414-s390x-unknown-linux-gnu-freethreaded-install_only.tar.gz +35f70ad05b2c4045889ee0c3d93f61b012654c1d91e10e671f0e5b4d4a6c6637 20260414/cpython-3.14.4+20260414-s390x-unknown-linux-gnu-install_only.tar.gz +1c366767d203b722efbd5b3796d16a08436e8a328afd31e551289efba9bf56d1 20260414/cpython-3.14.4+20260414-x86_64-apple-darwin-freethreaded-install_only.tar.gz +9ecb2b942e6698c04af10a63a3d73c0b2e8d8e11ce44933fbffe8651bef4577d 20260414/cpython-3.14.4+20260414-x86_64-apple-darwin-install_only.tar.gz +5ccaecdb899431f393209647182def14b36d7398bd59be4fa73dd79b48b3f290 20260414/cpython-3.14.4+20260414-x86_64-pc-windows-msvc-freethreaded-install_only.tar.gz +9647bb46d3c236e34c1c11bbb7113444d9711811f0d11c39956168807a955b1a 20260414/cpython-3.14.4+20260414-x86_64-pc-windows-msvc-install_only.tar.gz +c1a845a79da56265dc49628bc3b9e20d34f04674fd2d637ee40cbe259d2b1b95 20260414/cpython-3.14.4+20260414-x86_64-unknown-linux-gnu-freethreaded-install_only.tar.gz +e17275eaf95ceb5877aa6816e209b7733f41fee401d39c3921b88fb73fc4a4ba 20260414/cpython-3.14.4+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +12687a989a2384665577e1ef9864f33d4c074a1e69b38a8bac8d656531aefa3e 20260414/cpython-3.14.4+20260414-x86_64-unknown-linux-musl-install_only.tar.gz +5791a69a73b76b908f5bdf96da1928de8db696ab198f4ced04b77b22fe712ce0 20260414/cpython-3.15.0a8+20260414-aarch64-apple-darwin-freethreaded-install_only.tar.gz +780d46b3da0e58e15c620d9e7dfd29b54c8359c195f625858f85df9c2c7ecc32 20260414/cpython-3.15.0a8+20260414-aarch64-apple-darwin-install_only.tar.gz +95ddfe7dd52185f7e5d55524eafb48e54d1eab0b0cf013966f144a411f3ddd0f 20260414/cpython-3.15.0a8+20260414-aarch64-pc-windows-msvc-freethreaded-install_only.tar.gz +10fb470e900e65df4e37f8deaf1726397c914861ffc37b43ae3743a7eee88377 20260414/cpython-3.15.0a8+20260414-aarch64-pc-windows-msvc-install_only.tar.gz +b72908bce86036a0a1ba98ca9917ea0b99dc1e6c5d715d3d463c4f330880c09b 20260414/cpython-3.15.0a8+20260414-aarch64-unknown-linux-gnu-freethreaded-install_only.tar.gz +8f6dda4d8ff44976f1aa6a94674a09a503dc50b015297e1b62c8cdc591c90f4f 20260414/cpython-3.15.0a8+20260414-aarch64-unknown-linux-gnu-install_only.tar.gz +b3c8210674140a4c5beefa2d4afd752979222638a0fb68de672c60300b4a6642 20260414/cpython-3.15.0a8+20260414-ppc64le-unknown-linux-gnu-freethreaded-install_only.tar.gz +09f076c63fadbf675143674aa3b23229482b9a44840b8b1808a216def2a9af15 20260414/cpython-3.15.0a8+20260414-ppc64le-unknown-linux-gnu-install_only.tar.gz +1a4984207974563c6aea7dc934579d058dbac7436642081113e86011114b9fdf 20260414/cpython-3.15.0a8+20260414-riscv64-unknown-linux-gnu-freethreaded-install_only.tar.gz +9d41ce752e8b731872f0f5c9c48199e63c789d24ce3ae9e91d6c8008f36e7c51 20260414/cpython-3.15.0a8+20260414-riscv64-unknown-linux-gnu-install_only.tar.gz +f525a6244d73450e0c0a7ba125b5934894ab25ee171f7099c239d4eb7ce2f5f2 20260414/cpython-3.15.0a8+20260414-s390x-unknown-linux-gnu-freethreaded-install_only.tar.gz +1de2593c40cce2d8ea883f8c8580223bfa1478cbd9d0191ba3640aed083c2202 20260414/cpython-3.15.0a8+20260414-s390x-unknown-linux-gnu-install_only.tar.gz +3dcee23c21e4a3518947e988e115c1d824f07540f4326d93d4ea2028918e0193 20260414/cpython-3.15.0a8+20260414-x86_64-apple-darwin-freethreaded-install_only.tar.gz +a7744d34148969a2ec010da6f0a46ddeceda7c02e5cdfa2b4e1811487381491a 20260414/cpython-3.15.0a8+20260414-x86_64-apple-darwin-install_only.tar.gz +6e69670347e3a6ac1d0cd89b9506d825bd2f2690cc51ead5dec61aec6857d08d 20260414/cpython-3.15.0a8+20260414-x86_64-pc-windows-msvc-freethreaded-install_only.tar.gz +3ded476f676fdf260d56a5e49aa083d5ffd218fc3390e4480ed42bee1acfb3fb 20260414/cpython-3.15.0a8+20260414-x86_64-pc-windows-msvc-install_only.tar.gz +2d06d97e230b7f74de0fe4f661918a0ee827b08127b9372e0890e167de52a8c6 20260414/cpython-3.15.0a8+20260414-x86_64-unknown-linux-gnu-freethreaded-install_only.tar.gz +c93f4b15287ac48d7e3a475b245cb59cc51079382747e3e6213d6406c158969d 20260414/cpython-3.15.0a8+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +0568e953f837f09689eb4dd1af0043ba5e2ebae0c6395b8b9f8344a53b1f1da5 20260414/cpython-3.15.0a8+20260414-x86_64-unknown-linux-musl-freethreaded-install_only.tar.gz +9fbd6f243a424d4ae973e72aa0075122a7cfe05ac8f6cfde986e7b00d0dbc0bf 20260414/cpython-3.15.0a8+20260414-x86_64-unknown-linux-musl-install_only.tar.gz +""" diff --git a/python/private/sentinel.bzl b/python/private/sentinel_impl.bzl similarity index 100% rename from python/private/sentinel.bzl rename to python/private/sentinel_impl.bzl diff --git a/python/private/site_init_template.py b/python/private/site_init_template.py index a87a0d2a8f..12be98eb57 100644 --- a/python/private/site_init_template.py +++ b/python/private/site_init_template.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """site initialization logic for Bazel-built py_binary targets.""" + import os import os.path import sys @@ -26,6 +27,8 @@ _SELF_RUNFILES_RELATIVE_PATH = "%site_init_runfiles_path%" # Runfiles-relative path to the coverage tool entry point, if any. _COVERAGE_TOOL = "%coverage_tool%" +# True if the runfiles root should be added to sys.path +_ADD_RUNFILES_ROOT_TO_SYS_PATH = "%add_runfiles_root_to_sys_path%" == "1" def _is_verbose(): @@ -95,7 +98,15 @@ def _get_windows_path_with_unc_prefix(path): # Related doc: https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later import platform - if platform.win32_ver()[1] >= "10.0.14393": + win32_version = None + # Windows 2022 with Python 3.12.8 gives flakey errors, so try a couple times. + for _ in range(3): + try: + win32_version = platform.win32_ver()[1] + break + except (ValueError, KeyError): + pass + if win32_version and win32_version >= "10.0.14393": return path # import sysconfig only now to maintain python 2.6 compatibility @@ -125,31 +136,35 @@ def _search_path(name): def _setup_sys_path(): - """Perform Bazel/binary specific sys.path setup. - - NOTE: We do not add _RUNFILES_ROOT to sys.path for two reasons: - 1. Under workspace, it makes every external repository importable. If a Bazel - repository matches a Python import name, they conflict. - 2. Under bzlmod, the repo names in the runfiles directory aren't importable - Python names, so there's no point in adding the runfiles root to sys.path. - """ + """Perform Bazel/binary specific sys.path setup.""" + _print_verbose("site init: initial sys.path:\n", "\n".join(sys.path)) seen = set(sys.path) - python_path_entries = [] - def _maybe_add_path(path): + def _maybe_add_path(path, reason): if path in seen: return path = _get_windows_path_with_unc_prefix(path) if _is_windows(): path = path.replace("/", os.sep) - _print_verbose("append sys.path:", path) + _print_verbose("append sys.path:", reason, ":", path) sys.path.append(path) seen.add(path) + # Adding the runfiles root to sys.path is a legacy behavior that will be + # removed. We don't want to add it to sys.path for two reasons: + # 1. Under workspace, it makes every external repository importable. If a Bazel + # repository matches a Python import name, they conflict. + # 2. Under bzlmod, the repo names in the runfiles directory aren't importable + # Python names, so there's no point in adding the runfiles root to sys.path. + # For temporary compatibility with the original system_python bootstrap + # behavior, it is conditionally added for that boostrap mode. + if _ADD_RUNFILES_ROOT_TO_SYS_PATH: + _maybe_add_path(_RUNFILES_ROOT, "runfiles-root") + for rel_path in _IMPORTS_STR.split(":"): abs_path = os.path.join(_RUNFILES_ROOT, rel_path) - _maybe_add_path(abs_path) + _maybe_add_path(abs_path, "imports-strs") if _IMPORT_ALL: repo_dirs = sorted( @@ -157,9 +172,9 @@ def _maybe_add_path(path): ) for d in repo_dirs: if os.path.isdir(d): - _maybe_add_path(d) + _maybe_add_path(d, "import-all") else: - _maybe_add_path(os.path.join(_RUNFILES_ROOT, _WORKSPACE_NAME)) + _maybe_add_path(os.path.join(_RUNFILES_ROOT, _WORKSPACE_NAME), "workspace-root") # COVERAGE_DIR is set if coverage is enabled and instrumentation is configured # for something, though it could be another program executing this one or @@ -191,7 +206,7 @@ def _maybe_add_path(path): # it with the directory of the program it starts. Our actual sys.path[0] is # the runfiles directory, which must not be replaced. # CoverageScript.do_execute() undoes this sys.path[0] setting. - _maybe_add_path(coverage_dir) + _maybe_add_path(coverage_dir, "coverage-dir") coverage_setup = True else: _print_verbose_coverage( diff --git a/python/private/stage1_bootstrap_template.sh b/python/private/stage1_bootstrap_template.sh index a984344647..4374ff95b0 100644 --- a/python/private/stage1_bootstrap_template.sh +++ b/python/private/stage1_bootstrap_template.sh @@ -6,14 +6,38 @@ if [[ -n "${RULES_PYTHON_BOOTSTRAP_VERBOSE:-}" ]]; then set -x fi -# runfiles-relative path +# Creates a symlink. If the symlink already exists, it is tolerated to avoid +# race conditions during startup. +function _symlink() { + local target="$1" + local link="$2" + if ln -s "$target" "$link" 2>/dev/null; then + return 0 + fi + # If it failed, maybe it already exists because of a race. + if [[ -L "$link" || -e "$link" ]]; then + return 0 + fi + # If it doesn't exist, maybe we don't have write permission in the directory. + local dir + dir=$(dirname "$link") + if [[ ! -w "$dir" ]]; then + echo >&2 "ERROR: Cannot create symlink $link: Directory $dir is not writable" + else + echo >&2 "ERROR: Failed to create symlink $link -> $target" + fi + return 1 +} + + +# runfiles-root-relative path STAGE2_BOOTSTRAP="%stage2_bootstrap%" -# runfiles-relative path to python interpreter to use. +# runfiles-root-relative path to python interpreter to use. # This is the `bin/python3` path in the binary's venv. PYTHON_BINARY='%python_binary%' # The path that PYTHON_BINARY should symlink to. -# runfiles-relative path, absolute path, or single word. +# runfiles-root-relative path, absolute path, or single word. # Only applicable for zip files or when venv is recreated at runtime. PYTHON_BINARY_ACTUAL="%python_binary_actual%" @@ -85,9 +109,9 @@ else stub_filename="$PWD/$stub_filename" fi while true; do - module_space="${stub_filename}.runfiles" - if [[ -d "$module_space" ]]; then - echo "$module_space" + runfiles_root="${stub_filename}.runfiles" + if [[ -d "$runfiles_root" ]]; then + echo "$runfiles_root" return 0 fi if [[ "$stub_filename" == *.runfiles/* ]]; then @@ -105,8 +129,8 @@ else RUNFILES_DIR=$(find_runfiles_root $0) fi -if [[ -n "$RULES_PYTHON_TESTING_TELL_MODULE_SPACE" ]]; then - export RULES_PYTHON_TESTING_MODULE_SPACE="$RUNFILES_DIR" +if [[ -n "$RULES_PYTHON_TESTING_TELL_RUNFILES_ROOT" ]]; then + export RULES_PYTHON_TESTING_RUNFILES_ROOT="$RUNFILES_DIR" fi function find_python_interpreter() { @@ -153,7 +177,7 @@ if [[ "$IS_ZIPFILE" == "1" ]]; then fi # The bin/ directory may not exist if it is empty. mkdir -p "$(dirname $python_exe)" - ln -s "$symlink_to" "$python_exe" + _symlink "$symlink_to" "$python_exe" elif [[ "$RECREATE_VENV_AT_RUNTIME" == "1" ]]; then if [[ -n "$RULES_PYTHON_EXTRACT_ROOT" ]]; then use_exec=1 @@ -211,7 +235,7 @@ elif [[ "$RECREATE_VENV_AT_RUNTIME" == "1" ]]; then read -r resolved_py_exe read -r resolved_site_packages } < <("$python_exe_actual" -I </dev/null 2>&1; then + ENV_CMD="env" +else + ENV_CMD="/usr/bin/env" +fi + command=( - env + "$ENV_CMD" "${interpreter_env[@]}" "$python_exe" "${interpreter_args[@]}" diff --git a/python/private/stage2_bootstrap_template.py b/python/private/stage2_bootstrap_template.py index 4d98b03846..f445ad2b6a 100644 --- a/python/private/stage2_bootstrap_template.py +++ b/python/private/stage2_bootstrap_template.py @@ -9,23 +9,30 @@ # and is a special case of #7091. # # Python 3.11 introduced an PYTHONSAFEPATH (-P) option that disables this -# behaviour, which we set in the stage 1 bootstrap. +# behaviour, which we set in the stage 1 bootstrap. Isolated mode (-I) also +# disables it, including on older interpreters without safe_path. # So the prepended entry needs to be removed only if the above option is either # unset or not supported by the interpreter. # NOTE: This can be removed when Python 3.10 and below is no longer supported -if not getattr(sys.flags, "safe_path", False): +if ( + not getattr(sys.flags, "safe_path", False) + and not getattr(sys.flags, "isolated", False) + and sys.path +): del sys.path[0] import contextlib import os import re import runpy +import types import uuid +from functools import cache # ===== Template substitutions start ===== # We just put them in one place so its easy to tell which are used. -# Runfiles-relative path to the main Python source file. +# Runfiles-root-relative path to the main Python source file. # Empty if MAIN_MODULE is used MAIN_PATH = "%main%" @@ -41,19 +48,69 @@ # string otherwise. VENV_SITE_PACKAGES = "%venv_rel_site_packages%" +# Whether we should generate coverage data. +# string, 1 or 0 +COVERAGE_INSTRUMENTED = "%coverage_instrumented%" == "1" + +# runfiles-root-relative path to a file with binary-specific build information +# It uses forward slashes, so must be converted for proper usage on Windows. +BUILD_DATA_FILE = "%build_data_file%" + # ===== Template substitutions end ===== +IS_WINDOWS = os.name == "nt" +IS_VERBOSE = bool(os.environ.get("RULES_PYTHON_BOOTSTRAP_VERBOSE")) + +# Windows APIs can be picky about slashes depending on the context, +# so convert to backslashes to avoid any issues. +# Related: some logic checks path strings, which needs uniform separators. +if IS_WINDOWS: + + def norm_slashes(s): + return s.replace("/", "\\") + + MAIN_PATH = norm_slashes(MAIN_PATH) + VENV_ROOT = norm_slashes(VENV_ROOT) + VENV_SITE_PACKAGES = norm_slashes(VENV_SITE_PACKAGES) + BUILD_DATA_FILE = norm_slashes(BUILD_DATA_FILE) + + +class BazelBinaryInfoModule(types.ModuleType): + BUILD_DATA_FILE = BUILD_DATA_FILE + + @cache + def get_build_data(self): + """Returns a string of the raw build data.""" + try: + # Prefer dep via pypi + import runfiles + except ImportError: + from python.runfiles import runfiles + rlocation_path = self.BUILD_DATA_FILE + path = runfiles.Create().Rlocation(rlocation_path) + if IS_WINDOWS: + path = os.path.normpath(path) + try: + # Use utf-8-sig to handle Windows BOM + with open(path, encoding="utf-8-sig") as fp: + return fp.read() + except Exception as exc: + if hasattr(exc, "add_note"): + exc.add_note(f"runfiles lookup path: {rlocation_path}") + exc.add_note(f"exists: {os.path.exists(path)}") + can_read = os.access(path, os.R_OK) + exc.add_note(f"readable: {can_read}") + raise -# Return True if running on Windows -def is_windows(): - return os.name == "nt" + +sys.modules["bazel_binary_info"] = BazelBinaryInfoModule("bazel_binary_info") def get_windows_path_with_unc_prefix(path): path = path.strip() # No need to add prefix for non-Windows platforms. - if not is_windows() or sys.version_info[0] < 3: + if not IS_WINDOWS or sys.version_info[0] < 3: return path # Starting in Windows 10, version 1607(OS build 14393), MAX_PATH limitations have been @@ -79,50 +136,36 @@ def get_windows_path_with_unc_prefix(path): return path # Lets start the unicode fun - if path.startswith(unicode_prefix): + if path.startswith(unicode_prefix): # noqa: F821 return path # os.path.abspath returns a normalized absolute path - return unicode_prefix + os.path.abspath(path) - - -def search_path(name): - """Finds a file in a given search path.""" - search_path = os.getenv("PATH", os.defpath).split(os.pathsep) - for directory in search_path: - if directory: - path = os.path.join(directory, name) - if os.path.isfile(path) and os.access(path, os.X_OK): - return path - return None - - -def is_verbose(): - return bool(os.environ.get("RULES_PYTHON_BOOTSTRAP_VERBOSE")) + return unicode_prefix + os.path.abspath(path) # noqa: F821 def print_verbose(*args, mapping=None, values=None): - if is_verbose(): - if mapping is not None: - for key, value in sorted((mapping or {}).items()): - print( - "bootstrap: stage 2:", - *args, - f"{key}={value!r}", - file=sys.stderr, - flush=True, - ) - elif values is not None: - for i, v in enumerate(values): - print( - "bootstrap: stage 2:", - *args, - f"[{i}] {v!r}", - file=sys.stderr, - flush=True, - ) - else: - print("bootstrap: stage 2:", *args, file=sys.stderr, flush=True) + if not IS_VERBOSE: + return + if mapping is not None: + for key, value in sorted((mapping or {}).items()): + print( + "bootstrap: stage 2:", + *args, + f"{key}={value!r}", + file=sys.stderr, + flush=True, + ) + elif values is not None: + for i, v in enumerate(values): + print( + "bootstrap: stage 2:", + *args, + f"[{i}] {v!r}", + file=sys.stderr, + flush=True, + ) + else: + print("bootstrap: stage 2:", *args, file=sys.stderr, flush=True) def print_verbose_coverage(*args): @@ -133,7 +176,7 @@ def print_verbose_coverage(*args): def is_verbose_coverage(): """Returns True if VERBOSE_COVERAGE is non-empty in the environment.""" - return os.environ.get("VERBOSE_COVERAGE") or is_verbose() + return os.environ.get("VERBOSE_COVERAGE") or IS_VERBOSE def find_runfiles_root(main_rel_path): @@ -155,16 +198,23 @@ def find_runfiles_root(main_rel_path): if runfiles_dir and os.path.exists(os.path.join(runfiles_dir, main_rel_path)): return runfiles_dir + # Clear RUNFILES_DIR & RUNFILES_MANIFEST_FILE since the runfiles dir was + # not found. These can be correctly set for a parent Python process, but + # inherited by the child, and not correct for it. Later bootstrap code + # assumes they're are correct if set. + os.environ.pop("RUNFILES_DIR", None) + os.environ.pop("RUNFILES_MANIFEST_FILE", None) + stub_filename = sys.argv[0] if not os.path.isabs(stub_filename): stub_filename = os.path.join(os.getcwd(), stub_filename) while True: - module_space = stub_filename + (".exe" if is_windows() else "") + ".runfiles" + module_space = stub_filename + (".exe" if IS_WINDOWS else "") + ".runfiles" if os.path.isdir(module_space): return module_space - runfiles_pattern = r"(.*\.runfiles)" + (r"\\" if is_windows() else "/") + ".*" + runfiles_pattern = r"(.*\.runfiles)" + (r"\\" if IS_WINDOWS else "/") + ".*" matchobj = re.match(runfiles_pattern, stub_filename) if matchobj: return matchobj.group(1) @@ -177,6 +227,8 @@ def find_runfiles_root(main_rel_path): else: stub_filename = os.path.join(os.path.dirname(stub_filename), target) + # The `--enable_runfiles=false` flag is likely set, which isn't fully + # supported. raise AssertionError("Cannot find .runfiles directory for %s" % sys.argv[0]) @@ -309,9 +361,8 @@ def _maybe_collect_coverage(enable): print_verbose_coverage("Instrumented Files:\n" + "\n".join(instrumented_files)) print_verbose_coverage("Sources:\n" + "\n".join(unique_dirs)) - import uuid - import coverage + from coverage.exceptions import NoDataError coverage_dir = os.environ["COVERAGE_DIR"] unique_id = uuid.uuid4() @@ -319,11 +370,17 @@ def _maybe_collect_coverage(enable): # We need for coveragepy to use relative paths. This can only be configured # using an rc file. rcfile_name = os.path.join(coverage_dir, ".coveragerc_{}".format(unique_id)) + disable_warnings = ( + "disable_warnings = module-not-imported, no-data-collected" + if COVERAGE_INSTRUMENTED + else "" + ) print_verbose_coverage("coveragerc file:", rcfile_name) with open(rcfile_name, "w") as rcfile: rcfile.write( f"""[run] relative_files = True +{disable_warnings} source = \t{source} """ @@ -355,17 +412,27 @@ def _maybe_collect_coverage(enable): yield finally: cov.stop() - lcov_path = os.path.join(coverage_dir, "pylcov.dat") + lcov_path = os.path.join(coverage_dir, "pylcov_{}.dat".format(unique_id)) print_verbose_coverage("generating lcov from:", lcov_path) - cov.lcov_report( - outfile=lcov_path, - # Ignore errors because sometimes instrumented files aren't - # readable afterwards. e.g. if they come from /dev/fd or if - # they were transient code-under-test in /tmp - ignore_errors=True, - ) - if os.path.isfile(lcov_path): - unresolve_symlinks(lcov_path) + try: + cov.lcov_report( + outfile=lcov_path, + # Ignore errors because sometimes instrumented files aren't + # readable afterwards. e.g. if they come from /dev/fd or if + # they were transient code-under-test in /tmp + ignore_errors=True, + ) + except NoDataError: + # coverage.py raises NoDataError if no instrumented Python code ran + # (e.g. tests not running Python, or subprocess-only binaries). + # Skip the report to avoid failing otherwise passing tests. + # See https://github.com/bazel-contrib/rules_python/issues/2762. + print_verbose_coverage( + "no coverage data collected; skipping lcov report:", lcov_path + ) + else: + if os.path.isfile(lcov_path): + unresolve_symlinks(lcov_path) finally: try: os.unlink(rcfile_name) @@ -377,12 +444,32 @@ def _maybe_collect_coverage(enable): def _add_site_packages(site_packages): + if sys.prefix != sys.base_prefix: + venv_root = sys.prefix + os.sep + saw_venv_site_packages = False + else: + venv_root = None + saw_venv_site_packages = True first_global_offset = len(sys.path) for i, p in enumerate(sys.path): + # Handle the Windows venv case: when a temporary directory is created + # for the venv, we want the build-time venv in runfiles to come after + # the venv site-packages directory. + if venv_root: + is_venv_path = (p + os.sep).startswith(venv_root) + if is_venv_path: + if p.endswith("site-packages"): + saw_venv_site_packages = True + is_after_venv_site_packages = False + else: + is_after_venv_site_packages = saw_venv_site_packages + else: + is_after_venv_site_packages = True + # We assume the first *-packages is the runtime's. # *-packages is matched because Debian may use dist-packages # instead of site-packages. - if p.endswith("-packages"): + if p.endswith("-packages") and is_after_venv_site_packages: first_global_offset = i break prev_len = len(sys.path) @@ -409,7 +496,7 @@ def main(): # runfiles root if MAIN_PATH: main_rel_path = MAIN_PATH - if is_windows(): + if IS_WINDOWS: main_rel_path = main_rel_path.replace("/", os.sep) runfiles_root = find_runfiles_root(main_rel_path) @@ -457,8 +544,10 @@ def main(): # means only other generated files are importable (not source files). # # To replicate this behavior, we add main's directory within the runfiles - # when safe path isn't enabled. - if not getattr(sys.flags, "safe_path", False): + # when safe path or isolated mode isn't enabled. + if not getattr(sys.flags, "safe_path", False) and not getattr( + sys.flags, "isolated", False + ): prepend_path_entries = [ os.path.join(runfiles_root, os.path.dirname(main_rel_path)) ] diff --git a/python/private/stamp.bzl b/python/private/stamp_impl.bzl similarity index 96% rename from python/private/stamp.bzl rename to python/private/stamp_impl.bzl index 6bc0cd9d23..77e3da1f72 100644 --- a/python/private/stamp.bzl +++ b/python/private/stamp_impl.bzl @@ -18,6 +18,8 @@ This module can be removed likely after the following PRs ar addressed: - https://github.com/bazelbuild/bazel/issues/11164 """ +load(":visibility.bzl", "NOT_ACTUALLY_PUBLIC") + StampSettingInfo = provider( doc = "Information about the `--stamp` command line flag", fields = { @@ -50,7 +52,7 @@ Stamped binaries are not rebuilt unless their dependencies change. }, ) -def stamp_build_setting(name, visibility = ["//visibility:public"]): +def stamp_build_setting(name, visibility = NOT_ACTUALLY_PUBLIC): native.config_setting( name = "stamp_detect", values = {"stamp": "1"}, diff --git a/python/private/text_util.bzl b/python/private/text_util.bzl index 28979d8981..eedf66009d 100644 --- a/python/private/text_util.bzl +++ b/python/private/text_util.bzl @@ -76,13 +76,14 @@ def _render_select(selects, *, no_match_error = None, key_repr = repr, value_rep return "{}({})".format(name, args) -def _render_list(items, *, hanging_indent = ""): +def _render_list(items, *, hanging_indent = "", value_repr = repr): """Convert a list to formatted text. Args: items: list of items. hanging_indent: str, indent to apply to second and following lines of the formatted text. + value_repr: callable, function to represent each item. Returns: The list pretty formatted as a string. @@ -91,12 +92,12 @@ def _render_list(items, *, hanging_indent = ""): return "[]" if len(items) == 1: - return "[{}]".format(repr(items[0])) + return "[{}]".format(value_repr(items[0])) text = "\n".join([ "[", _indent("\n".join([ - "{},".format(repr(item)) + "{},".format(value_repr(item)) for item in items ])), "]", @@ -106,6 +107,18 @@ def _render_list(items, *, hanging_indent = ""): return text def _render_str(value): + """Render a string value. + + If value is None, it is automatically rendered as the Starlark literal `None`. + + Args: + value: str or None. + + Returns: + The value represented as Starlark source text. + """ + if value == None: + return "None" return repr(value) def _render_string_list_dict(value): @@ -157,9 +170,38 @@ def _left_pad_zero(index, length): fail("index must be non-negative") return ("0" * length + str(index))[-length:] +def _render_dict_dict(d): + """Render a dict[str, dict] value without recursive function calls.""" + if not d: + return "{}" + + lines = ["{"] + for k, v in d.items(): + if not v: + v_str = "{}" + else: + inner_lines = ["{"] + for ik, iv in v.items(): + inner_lines.append(_indent("{}: {},".format(repr(ik), repr(iv)))) + inner_lines.append("}") + v_str = "\n".join(inner_lines) + + # We need to correctly indent the multi-line string v_str + # but _indent acts on every line except the first if not carefully handled. + # It's easier to just do: + lines.append(_indent("{}: {},".format(repr(k), v_str))) + lines.append("}") + return "\n".join(lines) + +def _render_struct(value): + """Render a struct value.""" + fields = {k: repr(getattr(value, k)) for k in sorted(dir(value)) if k not in ["to_json", "to_proto"]} + return _render_call("struct", **fields) + render = struct( alias = _render_alias, dict = _render_dict, + dict_dict = _render_dict_dict, call = _render_call, hanging_indent = _hanging_indent, indent = _indent, @@ -168,6 +210,7 @@ render = struct( list = _render_list, select = _render_select, str = _render_str, + struct = _render_struct, toolchain_prefix = _toolchain_prefix, tuple = _render_tuple, string_list_dict = _render_string_list_dict, diff --git a/python/private/toolchain_types.bzl b/python/private/toolchain_types.bzl index ef81bf3bd4..5b5cce90ee 100644 --- a/python/private/toolchain_types.bzl +++ b/python/private/toolchain_types.bzl @@ -21,3 +21,4 @@ implementation of the toolchain. TARGET_TOOLCHAIN_TYPE = Label("//python:toolchain_type") EXEC_TOOLS_TOOLCHAIN_TYPE = Label("//python:exec_tools_toolchain_type") PY_CC_TOOLCHAIN_TYPE = Label("//python/cc:toolchain_type") +LAUNCHER_MAKER_TOOLCHAIN_TYPE = Label("@bazel_tools//tools/launcher:launcher_maker_toolchain_type") diff --git a/python/private/toolchains_repo.bzl b/python/private/toolchains_repo.bzl index 93bbb52108..1338e9c569 100644 --- a/python/private/toolchains_repo.bzl +++ b/python/private/toolchains_repo.bzl @@ -214,7 +214,7 @@ def python_toolchain_build_file_content( user_repository_name = "{}_{}".format(user_repository_name, platform), python_version = python_version, set_python_version_constraint = set_python_version_constraint, - target_settings = [], + target_settings = meta.target_settings, )) return "\n\n".join(entries) @@ -592,7 +592,7 @@ def _get_host_impl_repo_name(*, rctx, logger, python_version, os_name, cpu_name, os_name.upper(), cpu_name.upper(), ) - preference = repo_utils.getenv(rctx, env_var) + preference = rctx.getenv(env_var) if preference == None: logger.info("Consider using '{}' to select from one of the platforms: {}".format( env_var, diff --git a/python/private/tools/sort_manifest.py b/python/private/tools/sort_manifest.py new file mode 100755 index 0000000000..41bf4181e5 --- /dev/null +++ b/python/private/tools/sort_manifest.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 + +"""Sorts python-build-standalone manifest files by filename.""" + +import argparse +import sys +from pathlib import Path + + +def sort_manifest(manifest_path: Path) -> bool: + """Sorts a manifest file in place by filename. Returns True if modified.""" + # Read using pathlib.Path + lines = manifest_path.read_text(encoding="utf-8").splitlines(keepends=True) + + if not lines: + return False + + first_entry_idx = -1 + last_entry_idx = -1 + for idx, line in enumerate(lines): + stripped = line.strip() + if stripped and not stripped.startswith("#"): + if first_entry_idx == -1: + first_entry_idx = idx + last_entry_idx = idx + + if first_entry_idx == -1: + return False + + # Extract top-level comments (comments at the top followed by a blank newline) + top_level_comments = [] + pre_entry_lines = lines[:first_entry_idx] + + last_blank_idx = -1 + for idx, line in enumerate(pre_entry_lines): + if not line.strip(): + last_blank_idx = idx + + if last_blank_idx != -1: + top_level_comments = pre_entry_lines[: last_blank_idx + 1] + remaining_pre = pre_entry_lines[last_blank_idx + 1 :] + else: + remaining_pre = pre_entry_lines + + # Extract bottom-level comments + bottom_level_comments = lines[last_entry_idx + 1 :] + + # Group middle lines into actual catalog entries with their attached comments/blank lines + middle_lines = remaining_pre + lines[first_entry_idx : last_entry_idx + 1] + + entries = [] + current_attached = [] + + for line in middle_lines: + stripped = line.strip() + if stripped and not stripped.startswith("#"): + parts = [p for p in stripped.split(" ") if p] + if len(parts) == 2: + sha256, filename = parts[0], parts[1] + normalized_line = f"{sha256} {filename}\n" + else: + filename = parts[0] if parts else "" + normalized_line = line + + block = current_attached + [normalized_line] + entries.append((filename, block)) + current_attached = [] + else: + current_attached.append(line) + + if current_attached: + bottom_level_comments = current_attached + bottom_level_comments + + # Sort entries lexicographically by filename + entries.sort(key=lambda e: e[0]) + + new_lines = top_level_comments + for _, block in entries: + new_lines.extend(block) + new_lines.extend(bottom_level_comments) + + if new_lines == lines: + return False + + manifest_path.write_text("".join(new_lines), encoding="utf-8") + return True + + +def main(): + parser = argparse.ArgumentParser(description="Sort manifest files by filename.") + parser.add_argument( + "manifests", + nargs="*", + type=Path, + help="Path to manifest files to sort.", + ) + args = parser.parse_args() + + manifests = args.manifests + if not manifests: + repo_root = Path(__file__).resolve().parent.parent.parent.parent + default_manifest = repo_root / "python" / "private" / "runtimes_manifest.txt" + if default_manifest.exists(): + manifests = [default_manifest] + else: + print("No manifests provided.", file=sys.stderr) + sys.exit(1) + + changed = False + for m in manifests: + if m.exists(): + if sort_manifest(m): + print(f"Sorted {m}") + changed = True + else: + print(f"Warning: Manifest not found: {m}", file=sys.stderr) + + if changed: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/python/private/tools/sync_runtimes_manifest_workspace.py b/python/private/tools/sync_runtimes_manifest_workspace.py new file mode 100755 index 0000000000..450358542a --- /dev/null +++ b/python/private/tools/sync_runtimes_manifest_workspace.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 + +"""Synchronizes runtimes_manifest_workspace.bzl with runtimes_manifest.txt.""" + +import argparse +import sys +from pathlib import Path + + +def sync_workspace_manifest(txt_path: Path, bzl_path: Path) -> bool: + with open(txt_path, "r", encoding="utf-8") as f: + txt_content = f.read() + header = '''"""Manifest of runtimes for workspace mode builds. + +This is the workspace equivalent of runtimes_manifest.txt. It's a bzl file +to simplify loading of the data under workspace mode, which doesn't +support parsing a runtimes_manifest.txt file. + +NOTE: This file is automatically generated by sync_runtimes_manifest_workspace.py. +Do not edit directly! +""" + +MANIFEST_TEXT = """ +''' + + new_content = header + txt_content + '"""\n' + + if bzl_path.exists(): + with open(bzl_path, "r", encoding="utf-8") as f: + old_content = f.read() + else: + old_content = "" + + if new_content != old_content: + with open(bzl_path, "w", encoding="utf-8", newline="\n") as f: + f.write(new_content) + return True + + return False + + +def main(): + parser = argparse.ArgumentParser( + description="Sync runtimes workspace bzl file with runtimes manifest text file." + ) + parser.add_argument( + "txt_path", + nargs="?", + type=Path, + help="Path to runtimes_manifest.txt", + ) + parser.add_argument( + "bzl_path", + nargs="?", + type=Path, + help="Path to runtimes_manifest_workspace.bzl", + ) + args = parser.parse_args() + + txt_path = args.txt_path + bzl_path = args.bzl_path + + if not txt_path or not bzl_path: + repo_root = Path(__file__).resolve().parent.parent.parent.parent + txt_path = repo_root / "python" / "private" / "runtimes_manifest.txt" + bzl_path = repo_root / "python" / "private" / "runtimes_manifest_workspace.bzl" + + if not txt_path.exists(): + print(f"Error: Manifest not found: {txt_path}", file=sys.stderr) + sys.exit(1) + + if sync_workspace_manifest(txt_path, bzl_path): + print(f"Updated {bzl_path}") + if not args.bzl_path: + # Exit 1 for pre-commit mode (in-place modification) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/python/private/transition_labels.bzl b/python/private/transition_labels.bzl index b2cf6d7d88..d337044cb5 100644 --- a/python/private/transition_labels.bzl +++ b/python/private/transition_labels.bzl @@ -10,12 +10,10 @@ load(":common_labels.bzl", "labels") _BASE_TRANSITION_LABELS = [ labels.ADD_SRCS_TO_RUNFILES, labels.BOOTSTRAP_IMPL, + labels.DEBUGGER, labels.EXEC_TOOLS_TOOLCHAIN, + "//command_line_option:extra_toolchains", labels.PIP_ENV_MARKER_CONFIG, - labels.PIP_WHL_MUSLC_VERSION, - labels.PIP_WHL, - labels.PIP_WHL_GLIBC_VERSION, - labels.PIP_WHL_OSX_ARCH, labels.PIP_WHL_OSX_VERSION, labels.PRECOMPILE, labels.PRECOMPILE_SOURCE_RETENTION, @@ -23,6 +21,7 @@ _BASE_TRANSITION_LABELS = [ labels.PYTHON_VERSION, labels.PY_FREETHREADED, labels.PY_LINUX_LIBC, + labels.VENV, labels.VENVS_SITE_PACKAGES, labels.VENVS_USE_DECLARE_SYMLINK, ] diff --git a/python/private/uncachable_version_file.bzl b/python/private/uncachable_version_file.bzl new file mode 100644 index 0000000000..0f58df8043 --- /dev/null +++ b/python/private/uncachable_version_file.bzl @@ -0,0 +1,40 @@ +"""Implementation of uncachable_version_file.""" + +load(":py_internal.bzl", "py_internal") + +def _uncachable_version_file_impl(ctx): + version_file = ctx.actions.declare_file("uncachable_version_file.txt") + py_internal.copy_without_caching( + ctx = ctx, + # NOTE: ctx.version_file is undocumented; see + # https://github.com/bazelbuild/bazel/issues/9363 + # NOTE: Even though the version file changes every build (it contains + # the build timestamp), it is ignored when computing what inputs + # changed. See https://bazel.build/docs/user-manual#workspace-status + read_from = ctx.version_file, + write_to = version_file, + ) + return [DefaultInfo( + files = depset([version_file]), + )] + +uncachable_version_file = rule( + doc = """ +Creates a copy of `ctx.version_file`, except it isn't ignored by +Bazel's change-detecting logic. In fact, it's the opposite: +caching is disabled for the action generating this file, so any +actions depending on this file will always re-run. +""", + implementation = _uncachable_version_file_impl, +) + +def define_uncachable_version_file(name, visibility = None): + native.alias( + name = name, + actual = select({ + ":stamp_detect": ":uncachable_version_file_impl", + "//conditions:default": ":sentinel", + }), + visibility = visibility, + ) + uncachable_version_file(name = "uncachable_version_file_impl") diff --git a/python/private/util.bzl b/python/private/util.bzl index 4d2da57760..31f317fedf 100644 --- a/python/private/util.bzl +++ b/python/private/util.bzl @@ -15,7 +15,7 @@ """Functionality shared by multiple pieces of code.""" load("@bazel_skylib//lib:types.bzl", "types") -load("@rules_python_internal//:rules_python_config.bzl", "config") +load("//python/private:py_internal.bzl", "py_internal") def copy_propagating_kwargs(from_kwargs, into_kwargs = None): """Copies args that must be compatible between two targets with a dependency relationship. @@ -50,21 +50,6 @@ def copy_propagating_kwargs(from_kwargs, into_kwargs = None): # The implementation of the macros and tagging mechanism follows the example # set by rules_cc and rules_java. -_MIGRATION_TAG = "__PYTHON_RULES_MIGRATION_DO_NOT_USE_WILL_BREAK__" - -def add_migration_tag(attrs): - """Add a special tag to `attrs` to aid migration off native rles. - - Args: - attrs: dict of keyword args. The `tags` key will be modified in-place. - - Returns: - The same `attrs` object, but modified. - """ - if not config.enable_pystar: - add_tag(attrs, _MIGRATION_TAG) - return attrs - def add_tag(attrs, tag): """Adds `tag` to `attrs["tags"]`. @@ -86,36 +71,17 @@ def add_tag(attrs, tag): else: attrs["tags"] = [tag] -# Helper to make the provider definitions not crash under Bazel 5.4: -# Bazel 5.4 doesn't support the `init` arg of `provider()`, so we have to -# not pass that when using Bazel 5.4. But, not passing the `init` arg -# changes the return value from a two-tuple to a single value, which then -# breaks Bazel 6+ code. -# This isn't actually used under Bazel 5.4, so just stub out the values -# to get past the loading phase. -def define_bazel_6_provider(doc, fields, **kwargs): - """Define a provider, or a stub for pre-Bazel 7.""" - if not IS_BAZEL_6_OR_HIGHER: - return provider("Stub, not used", fields = []), None - return provider(doc = doc, fields = fields, **kwargs) - -IS_BAZEL_7_4_OR_HIGHER = hasattr(native, "legacy_globals") - -IS_BAZEL_7_OR_HIGHER = hasattr(native, "starlark_doc_extract") - -# Bazel 5.4 has a bug where every access of testing.ExecutionInfo is a -# different object that isn't equal to any other. This is fixed in bazel 6+. -IS_BAZEL_6_OR_HIGHER = testing.ExecutionInfo == testing.ExecutionInfo - -_marker_rule_to_detect_bazel_6_4_or_higher = rule(implementation = lambda ctx: None) - -# Bazel 6.4 and higher have a bug fix where rule names show up in the str() -# of a rule. See -# https://github.com/bazelbuild/bazel/commit/002490b9a2376f0b2ea4a37102c5e94fc50a65ba -# https://github.com/bazelbuild/bazel/commit/443cbcb641e17f7337ccfdecdfa5e69bc16cae55 -# This technique is done instead of using native.bazel_version because, -# under stardoc, the native.bazel_version attribute is entirely missing, which -# prevents doc generation from being able to correctly generate docs. -IS_BAZEL_6_4_OR_HIGHER = "_marker_rule_to_detect_bazel_6_4_or_higher" in str( - _marker_rule_to_detect_bazel_6_4_or_higher, -) +def is_importable_name(name): + # Requires Bazel 8+ + if hasattr(py_internal, "regex_match"): + # ?U means activates unicode matching (Python allows most unicode + # in module names / identifiers). + # \w matches alphanumeric and underscore. + # NOTE: regex_match has an implicit ^ and $ + return py_internal.regex_match(name, "(?U)\\w+") + else: + # Otherwise, use a rough hueristic that should catch most cases. + return ( + "." not in name and + "-" not in name + ) diff --git a/python/private/venv_runfiles.bzl b/python/private/venv_runfiles.bzl index 291920b848..45d4d24848 100644 --- a/python/private/venv_runfiles.bzl +++ b/python/private/venv_runfiles.bzl @@ -3,8 +3,9 @@ load("@bazel_skylib//lib:paths.bzl", "paths") load( ":common.bzl", - "PYTHON_FILE_EXTENSIONS", + "ExplicitSymlink", "is_file", + "is_windows_platform", "relative_path", "runfiles_root_path", ) @@ -14,6 +15,15 @@ load( "VenvSymlinkEntry", "VenvSymlinkKind", ) +load(":util.bzl", "is_importable_name") + +# List of top-level package names that are known to be namespace +# packages, but cannot be detected as such automatically. +_WELL_KNOWN_NAMESPACE_PACKAGES = [ + # nvidia wheels incorrectly use an empty `__init__.py` file, even + # though multiple distributions install into the directory. + "nvidia", +] def create_venv_app_files(ctx, deps, venv_dir_map): """Creates the tree of app-specific files for a venv for a binary. @@ -30,7 +40,12 @@ def create_venv_app_files(ctx, deps, venv_dir_map): paths within the current ctx's venv (e.g. `_foo.venv/bin`). Returns: - {type}`list[File]` of the files that were created. + {type}`struct` with the following attributes: + * {type}`list[File]` `venv_files` additional files created for + the venv. + * {type}`dict[str, File]` `runfiles_symlinks` map intended for + the `runfiles.symlinks` argument. A map of main-repo + relative paths to File. """ # maps venv-relative path to the runfiles path it should point to @@ -44,17 +59,46 @@ def create_venv_app_files(ctx, deps, venv_dir_map): link_map = build_link_map(ctx, entries) venv_files = [] + runfiles_symlinks = {} + explicit_symlinks = [] + + is_windows = is_windows_platform(ctx) + + ctx_rf_path = paths.join( + ctx.label.repo_name or ctx.workspace_name, + ctx.label.package, + ) + + seen_bin_venv_paths = {} + for kind, kind_map in link_map.items(): base = venv_dir_map[kind] for venv_path, link_to in kind_map.items(): bin_venv_path = paths.join(base, venv_path) + if bin_venv_path in seen_bin_venv_paths: + continue + seen_bin_venv_paths[bin_venv_path] = True + if is_file(link_to): - if link_to.is_directory: - venv_link = ctx.actions.declare_directory(bin_venv_path) - else: - venv_link = ctx.actions.declare_file(bin_venv_path) - ctx.actions.symlink(output = venv_link, target_file = link_to) - else: + # use paths.join to handle ctx.label.package = "" + # runfile_prefix should be prepended as we use runfiles.root_symlinks + runfile_prefix = ctx.label.repo_name or ctx.workspace_name + symlink_from = paths.join(runfile_prefix, ctx.label.package, bin_venv_path) + + runfiles_symlinks[symlink_from] = link_to + + # On Windows, we need to explicitly create the symlink in the venv + # because the bootstrap script won't otherwise know about it. + if is_windows: + rf_path = paths.join(ctx_rf_path, bin_venv_path) + _, _, venv_path = bin_venv_path.partition(".venv/") + explicit_symlinks.append(ExplicitSymlink( + runfiles_path = rf_path, + venv_path = venv_path, + link_to_path = runfiles_root_path(ctx, link_to.short_path), + files = depset([link_to]), + )) + elif not is_windows: venv_link = ctx.actions.declare_symlink(bin_venv_path) venv_link_rf_path = runfiles_root_path(ctx, venv_link.short_path) rel_path = relative_path( @@ -64,24 +108,39 @@ def create_venv_app_files(ctx, deps, venv_dir_map): to = link_to, ) ctx.actions.symlink(output = venv_link, target_path = rel_path) - venv_files.append(venv_link) - - return venv_files + venv_files.append(venv_link) + else: + rf_path = paths.join(ctx_rf_path, bin_venv_path) + _, _, venv_path = bin_venv_path.partition(".venv/") + explicit_symlinks.append(ExplicitSymlink( + runfiles_path = rf_path, + venv_path = venv_path, + link_to_path = link_to, + files = depset(), + )) + + return struct( + venv_files = venv_files, + runfiles_symlinks = runfiles_symlinks, + explicit_symlinks = depset(explicit_symlinks), + ) # Visible for testing -def build_link_map(ctx, entries): +def build_link_map(ctx, entries, return_conflicts = False): """Compute the mapping of venv paths to their backing objects. - Args: ctx: {type}`ctx` current ctx. entries: {type}`list[VenvSymlinkEntry]` the entries that describe the venv-relative + return_conflicts: {type}`bool`. Only present for testing. If True, + also return a list of the groups that had overlapping paths and had + to be resolved and merged. Returns: {type}`dict[str, dict[str, str|File]]` Mappings of venv paths to their backing files. The first key is a `VenvSymlinkKind` value. - The inner dict keys are venv paths relative to the kind's diretory. The + The inner dict keys are venv paths relative to the kind's directory. The inner dict values are strings or Files to link to. """ @@ -106,6 +165,7 @@ def build_link_map(ctx, entries): # final paths to keep, grouped by kind keep_link_map = {} # dict[str kind, dict[path, str|File]] + conflicts = [] if return_conflicts else None for kind, entries in entries_by_kind.items(): # dict[str kind-relative path, str|File link_to] keep_kind_link_map = {} @@ -116,14 +176,22 @@ def build_link_map(ctx, entries): # If there's just one group, we can symlink to the directory if len(group) == 1: entry = group[0] - keep_kind_link_map[entry.venv_path] = entry.link_to_path + if entry.link_to_file: + keep_kind_link_map[entry.venv_path] = entry.link_to_file + else: + keep_kind_link_map[entry.venv_path] = entry.link_to_path else: + if return_conflicts: + conflicts.append(group) + # Merge a group of overlapping prefixes _merge_venv_path_group(ctx, group, keep_kind_link_map) keep_link_map[kind] = keep_kind_link_map - - return keep_link_map + if return_conflicts: + return keep_link_map, conflicts + else: + return keep_link_map def _group_venv_path_entries(entries): """Group entries by VenvSymlinkEntry.venv_path overlap. @@ -138,14 +206,18 @@ def _group_venv_path_entries(entries): """ # Sort so order is top-down, ensuring grouping by short common prefix - entries = sorted(entries, key = lambda e: e.venv_path) + # Split it into path components so `foo foo-bar foo/bar` sorts as + # `foo foo/bar foo-bar` + entries = sorted(entries, key = lambda e: tuple(e.venv_path.split("/"))) groups = [] current_group = None current_group_prefix = None for entry in entries: - prefix = entry.venv_path - anchored_prefix = prefix + "/" + # NOTE: When a file is being directly linked, the anchored prefix can look + # odd, e.g. "foo/__init__.py/". This is OK; it's just used to prevent + # incorrect prefix substring matching. + anchored_prefix = entry.venv_path + "/" if (current_group_prefix == None or not anchored_prefix.startswith(current_group_prefix)): current_group_prefix = anchored_prefix @@ -170,30 +242,81 @@ def _merge_venv_path_group(ctx, group, keep_map): # TODO: Compute the minimum number of entries to create. This can't avoid # flattening the files depset, but can lower the number of materialized # files significantly. Usually overlaps are limited to a small number - # of directories. + # of directories. Note that, when doing so, shared libraries need to + # be symlinked directly, not the directory containing them, due to + # dynamic linker symlink resolution semantics on Linux. for entry in group: - prefix = entry.venv_path + root_venv_path = entry.venv_path + anchored_link_to_path = entry.link_to_path + "/" for file in entry.files.to_list(): - # Compute the file-specific venv path. i.e. the relative - # path of the file under entry.venv_path, joined with - # entry.venv_path rf_root_path = runfiles_root_path(ctx, file.short_path) - if not rf_root_path.startswith(entry.link_to_path): - # This generally shouldn't occur in practice, but just - # in case, skip them, for lack of a better option. - continue - venv_path = "{}/{}".format( - prefix, - rf_root_path.removeprefix(entry.link_to_path + "/"), - ) - # For lack of a better option, first added wins. We happen to - # go in top-down prefix order, so the highest level namespace - # package typically wins. - if venv_path not in keep_map: - keep_map[venv_path] = file + # It's a file (or directory) being directly linked and + # must be directly linked. + if rf_root_path == entry.link_to_path: + # For lack of a better option, first added wins. + if entry.venv_path not in keep_map: + keep_map[entry.venv_path] = file + + # Skip anything remaining: anything left is either + # the same path (first set wins), a suffix (violates + # preconditions and can't link anyways), or not under + # the prefix (violates preconditions). + break + else: + # Compute the file-specific venv path. i.e. the relative + # path of the file under entry.venv_path, joined with + # entry.venv_path + head, match, rel_venv_path = rf_root_path.partition(anchored_link_to_path) + if not match or head: + # If link_to_path didn't match, then obviously skip. + # If head is non-empty, it means link_to_path wasn't + # found at the start + # This shouldn't occur in practice, but guard against it + # just in case + continue + + venv_path = paths.join(root_venv_path, rel_venv_path) + + # For lack of a better option, first added wins. + if venv_path not in keep_map: + keep_map[venv_path] = file + +def _get_file_venv_path(ctx, f, site_packages_root): + """Computes a file's venv_path if it's under the site_packages_root. + + Args: + ctx: The current ctx. + f: The file to compute the venv_path for. + site_packages_root: The site packages root path; repo-relative + path. -def get_venv_symlinks(ctx, files, package, version_str, site_packages_root): + Returns: + A tuple `(venv_path, rf_root_path)` if the file is under + `site_packages_root` or data/, bin/, include/ otherwise `(None, None)`. + """ + rf_root_path = runfiles_root_path(ctx, f.short_path) + _, _, repo_rel_path = rf_root_path.partition("/") + + # Check for wheel data/bin/include folders first + for prefix in ["data/", "bin/", "include/"]: + if repo_rel_path.startswith(prefix): + return (repo_rel_path, rf_root_path) + + head, found_sp_root, venv_path = repo_rel_path.partition(site_packages_root) + if head or not found_sp_root: + # If head is set, then the path didn't start with site_packages_root + # if found_sp_root is empty, then it means it wasn't found at all. + return (None, None) + return (venv_path, rf_root_path) + +def get_venv_symlinks( + ctx, + files, + package, + version_str, + site_packages_root, + namespace_package_files = []): """Compute the VenvSymlinkEntry objects for a library. Args: @@ -205,6 +328,9 @@ def get_venv_symlinks(ctx, files, package, version_str, site_packages_root): version_str: {type}`str` the distribution's version. site_packages_root: {type}`str` prefix under which files are considered to be part of the installed files. + namespace_package_files: {type}`list[File]` a list of files + that are pkgutil-style namespace packages and cannot be + directly linked. Returns: {type}`list[VenvSymlinkEntry]` the entries that describe how @@ -220,99 +346,205 @@ def get_venv_symlinks(ctx, files, package, version_str, site_packages_root): # Append slash to prevent incorrect prefix-string matches site_packages_root += "/" - # We have to build a list of (runfiles path, site-packages path) pairs of the files to - # create in the consuming binary's venv site-packages directory. To minimize the number of - # files to create, we just return the paths to the directories containing the code of - # interest. - # - # However, namespace packages complicate matters: multiple distributions install in the - # same directory in site-packages. This works out because they don't overlap in their - # files. Typically, they install to different directories within the namespace package - # directory. We also need to ensure that we can handle a case where the main package (e.g. - # airflow) has directories only containing data files and then namespace packages coming - # along and being next to it. - # - # Lastly we have to assume python modules just being `.py` files (e.g. typing-extensions) - # is just a single Python file. - - dir_symlinks = {} # dirname -> runfile path - venv_symlinks = [] - - # Sort so order is top-down all_files = sorted(files, key = lambda f: f.short_path) - for src in all_files: - path = _repo_relative_short_path(src.short_path) - if not path.startswith(site_packages_root): + cannot_be_linked_directly = {} + for dirname in [ + # The venv directories that bin, include, and data get put into are + # shared across wheels, so we cannot link them directly + "bin", + "include", + "data", + # The data scheme is overlaid on the venv root, so the files under it + # could, in theory, get installed into e.g. bin/ or similar. Explicitly + # mark them as non-directly linkable to avoid issues. + "data/bin", + "data/include", + "data/lib", + "data/Scripts", + "data/Include", + "data/Lib", + ]: + cannot_be_linked_directly[dirname] = True + + # dict[str venv-relative dirname, bool is_namespace_package] + namespace_package_dirs = { + ns: True + for ns in _WELL_KNOWN_NAMESPACE_PACKAGES + } + + # venv paths that cannot be directly linked. Dict acting as set. + cannot_be_linked_directly.update({ + dirname: True + for dirname in namespace_package_dirs.keys() + }) + for f in namespace_package_files: + venv_path, _ = _get_file_venv_path(ctx, f, site_packages_root) + if venv_path == None: continue - path = path.removeprefix(site_packages_root) - dir_name, _, filename = path.rpartition("/") + ns_dir = paths.dirname(venv_path) + namespace_package_dirs[ns_dir] = True + cannot_be_linked_directly[ns_dir] = True + + # dict[str path, VenvSymlinkEntry] + # Where path is the venv path (i.e. relative to site_packages_prefix) + venv_symlinks = {} + + # List of (File, str venv_path) tuples + files_left_to_link = [] - if dir_name in dir_symlinks: - # we already have this dir, this allows us to short-circuit since most of the - # ctx.files.data might share the same directories as ctx.files.srcs + # We want to minimize the number of files symlinked. Ideally, only the + # top-level directories are symlinked. Unfortunately, shared libraries + # complicate matters: if a shared library's directory is linked, then the + # dynamic linker computes the wrong search path. + # + # To fix, we have to directly link shared libraries. This then means that + # all the parent directories of the shared library can't be linked + # directly. + for src in all_files: + venv_path, rf_root_path = _get_file_venv_path(ctx, src, site_packages_root) + if venv_path == None: continue - runfiles_dir_name, _, _ = runfiles_root_path(ctx, src.short_path).partition("/") - if dir_name: - # This can be either: - # * a directory with libs (e.g. numpy.libs, created by auditwheel) - # * a directory with `__init__.py` file that potentially also needs to be - # symlinked. - # * `.dist-info` directory - # - # This could be also regular files, that just need to be symlinked, so we will - # add the directory here. - dir_symlinks[dir_name] = runfiles_dir_name - elif src.extension in PYTHON_FILE_EXTENSIONS: - # This would be files that do not have directories and we just need to add - # direct symlinks to them as is, we only allow Python files in here - entry = VenvSymlinkEntry( + filename = paths.basename(venv_path) + if _is_linker_loaded_library(filename): + venv_symlinks[venv_path] = VenvSymlinkEntry( kind = VenvSymlinkKind.LIB, - link_to_path = paths.join(runfiles_dir_name, site_packages_root, filename), + link_to_path = rf_root_path, + link_to_file = src, package = package, version = version_str, - venv_path = filename, files = depset([src]), + venv_path = venv_path, ) - venv_symlinks.append(entry) - - # Sort so that we encounter `foo` before `foo/bar`. This ensures we - # see the top-most explicit package first. - dirnames = sorted(dir_symlinks.keys()) - first_level_explicit_packages = [] - for d in dirnames: - is_sub_package = False - for existing in first_level_explicit_packages: - # Suffix with / to prevent foo matching foobar - if d.startswith(existing + "/"): - is_sub_package = True - break - if not is_sub_package: - first_level_explicit_packages.append(d) - - for dirname in first_level_explicit_packages: - prefix = dir_symlinks[dirname] - link_to_path = paths.join(prefix, site_packages_root, dirname) - entry = VenvSymlinkEntry( - kind = VenvSymlinkKind.LIB, + parent = paths.dirname(venv_path) + for _ in range(len(venv_path) + 1): # Iterate enough times to traverse up + if not parent: + break + if cannot_be_linked_directly.get(parent, False): + # Already seen + break + cannot_be_linked_directly[parent] = True + parent = paths.dirname(parent) + else: + files_left_to_link.append((src, venv_path)) + + top_level_dirname, _, tail = venv_path.partition("/") + if ( + # If it's already not directly linkable, nothing to do + not cannot_be_linked_directly.get(top_level_dirname, False) and + # If its already known to be non-implicit namespace, then skip + namespace_package_dirs.get(top_level_dirname, True) and + # It must be an importable name to be an implicit namespace package + is_importable_name(top_level_dirname) + ): + namespace_package_dirs.setdefault(top_level_dirname, True) + + # Looking for `__init__.` isn't 100% correct, as it'll match e.g. + # `__init__.pyi`, but it's close enough. + if "/" not in tail and tail.startswith("__init__."): + namespace_package_dirs[top_level_dirname] = False + + # We treat namespace packages as a hint that other distributions may + # install into the same directory. As such, we avoid linking them directly + # to avoid conflict merging later. + for dirname, is_namespace_package in namespace_package_dirs.items(): + if is_namespace_package: + # If it's already in cannot_be_linked_directly due to pkgutil_namespace_packages + # then we should not unset it. + if not cannot_be_linked_directly.get(dirname, False): + cannot_be_linked_directly[dirname] = True + + # At this point, venv_symlinks has entries for the shared libraries + # and cannot_be_linked_directly has the directories that cannot be + # directly linked. Next, we loop over the remaining files and group + # them into the highest level directory that can be linked. + + # dict[str venv_path, list[File]] + optimized_groups = {} + + for src, venv_path in files_left_to_link: + parent = paths.dirname(venv_path) + if not parent: + # File in root, must be linked directly + optimized_groups.setdefault(venv_path, []) + optimized_groups[venv_path].append(src) + continue + + if parent in cannot_be_linked_directly: + # File in a directory that cannot be directly linked, + # so link the file directly + optimized_groups.setdefault(venv_path, []) + optimized_groups[venv_path].append(src) + else: + # This path can be grouped. Find the highest-level directory to link. + venv_path = parent + next_parent = paths.dirname(parent) + for _ in range(len(venv_path) + 1): # Iterate enough times + if next_parent: + if next_parent not in cannot_be_linked_directly: + venv_path = next_parent + next_parent = paths.dirname(next_parent) + else: + break + else: + break + + optimized_groups.setdefault(venv_path, []) + optimized_groups[venv_path].append(src) + + # Finally, for each group, we create the VenvSymlinkEntry objects + for venv_path, files in optimized_groups.items(): + if venv_path.startswith("data/"): + out_venv_path = venv_path[len("data/"):] + kind = VenvSymlinkKind.DATA + prefix = "" + elif venv_path.startswith("include/"): + out_venv_path = venv_path[len("include/"):] + kind = VenvSymlinkKind.INCLUDE + prefix = "" + elif venv_path.startswith("bin/"): + out_venv_path = venv_path[len("bin/"):] + kind = VenvSymlinkKind.BIN + prefix = "" + else: + out_venv_path = venv_path + kind = VenvSymlinkKind.LIB + prefix = site_packages_root + + link_to_path = ( + _get_label_runfiles_repo(ctx, files[0].owner) + + "/" + + prefix + + venv_path + ) + venv_symlinks[venv_path] = VenvSymlinkEntry( + kind = kind, link_to_path = link_to_path, + link_to_file = files[0] if kind == VenvSymlinkKind.BIN and len(files) == 1 else None, package = package, version = version_str, - venv_path = dirname, - files = depset([ - f - for f in all_files - if runfiles_root_path(ctx, f.short_path).startswith(link_to_path + "/") - ]), + venv_path = out_venv_path, + files = depset(files), ) - venv_symlinks.append(entry) - return venv_symlinks + return venv_symlinks.values() -def _repo_relative_short_path(short_path): - # Convert `../+pypi+foo/some/file.py` to `some/file.py` - if short_path.startswith("../"): - return short_path[3:].partition("/")[2] +def _is_linker_loaded_library(filename): + """Tells if a filename is one that `dlopen()` or the runtime linker handles. + + C libraries: *.so (linux), *.so.* (linux), *.dylib (mac), .dll (windows) + """ + if filename.endswith(".dll"): + return True + if filename.endswith((".so", ".dylib")) or ".so." in filename: + return True + return False + +def _get_label_runfiles_repo(ctx, label): + repo = label.repo_name + if repo: + return repo else: - return short_path + # For files, empty repo means the main repo + return ctx.workspace_name diff --git a/python/private/version.bzl b/python/private/version.bzl index 8b5fef7b2a..b13327841b 100644 --- a/python/private/version.bzl +++ b/python/private/version.bzl @@ -622,7 +622,8 @@ def parse(version_str, strict = False, _fail = fail): # https://peps.python.org/pep-0440/#public-version-identifiers return None - return struct( + # buildifier: disable=uninitialized + self = struct( epoch = _parse_epoch(parts["epoch"], _fail), release = _parse_release(parts["release"]), pre = _parse_pre(parts["pre"]), @@ -631,7 +632,9 @@ def parse(version_str, strict = False, _fail = fail): local = _parse_local(parts["local"], _fail), string = parts["norm"], is_prefix = parts["is_prefix"], + key = lambda *a, **k: _version_key(self, *a, **k), ) + return self def _parse_epoch(value, fail): if not value: @@ -716,12 +719,9 @@ def _version_eeq(left, right): def _version_eq(left, right): """== operator""" - if left.is_prefix and right.is_prefix: - fail("Invalid comparison: both versions cannot be prefix matching") - if left.is_prefix: - return right.string.startswith("{}.".format(left.string)) - if right.is_prefix: - return left.string.startswith("{}.".format(right.string)) + is_prefix = _is_prefix(left, right) + if is_prefix != None: + return is_prefix if left.epoch != right.epoch: return False @@ -740,6 +740,27 @@ def _version_eq(left, right): # local is ignored for == checks ) +def _is_prefix(left, right): + if left.is_prefix and right.is_prefix: + fail("Invalid comparison: both versions cannot be prefix matching") + if left.is_prefix: + return _left_is_prefix(left, right) + if right.is_prefix: + return _left_is_prefix(right, left) + + return None + +def _left_is_prefix(left, right): + if right.string.startswith("{}.".format(left.string)): + return True + + # There is a chance that we are comparing 1.3 ~= 1.3.0 + # + # In that case the logic would be to normalize 1.3 to 1.3.0 and then + # the above would work, but we end up comparing 1.3 with 1.3. above + # and it fails. + return _version_key(left) == _version_key(right) + def _version_compatible(left, right): """~= operator""" if left.is_prefix or right.is_prefix: @@ -751,11 +772,11 @@ def _version_compatible(left, right): right_star = ".".join([str(d) for d in right.release[:-1]]) if right.epoch: - right_star = "{}!{}.".format(right.epoch, right_star) + right_star = "{}!{}.*".format(right.epoch, right_star) else: - right_star = "{}.".format(right_star) + right_star = "{}.*".format(right_star) - return _version_ge(left, right) and left.string.startswith(right_star) + return _version_ge(left, right) and _is_prefix(left, parse(right_star, strict = False)) def _version_ne(left, right): """!= operator""" diff --git a/python/private/which.bzl b/python/private/which.bzl deleted file mode 100644 index b0cbddb0e8..0000000000 --- a/python/private/which.bzl +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright 2023 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. - -"""Wrapper for repository which call""" - -_binary_not_found_msg = "Unable to find the binary '{binary_name}'. Please update your PATH to include '{binary_name}'." - -def which_with_fail(binary_name, rctx): - """Tests to see if a binary exists, and otherwise fails with a message. - - Args: - binary_name: name of the binary to find. - rctx: repository context. - - Returns: - rctx.Path for the binary. - """ - binary = rctx.which(binary_name) - if binary == None: - fail(_binary_not_found_msg.format(binary_name = binary_name)) - return binary diff --git a/python/private/whl_filegroup/BUILD.bazel b/python/private/whl_filegroup/BUILD.bazel index b4246ca080..4d0a096311 100644 --- a/python/private/whl_filegroup/BUILD.bazel +++ b/python/private/whl_filegroup/BUILD.bazel @@ -7,14 +7,14 @@ filegroup( visibility = ["//python/private:__pkg__"], ) -bzl_library( - name = "whl_filegroup_bzl", - srcs = ["whl_filegroup.bzl"], - visibility = ["//:__subpackages__"], -) - py_binary( name = "extract_wheel_files", srcs = ["extract_wheel_files.py"], visibility = ["//visibility:public"], ) + +bzl_library( + name = "whl_filegroup", + srcs = ["whl_filegroup.bzl"], + visibility = ["//python:__subpackages__"], +) diff --git a/python/private/zip_main_template.py b/python/private/zip_main_template.py deleted file mode 100644 index d1489b46aa..0000000000 --- a/python/private/zip_main_template.py +++ /dev/null @@ -1,333 +0,0 @@ -# Template for the __main__.py file inserted into zip files -# -# NOTE: This file is a "stage 1" bootstrap, so it's responsible for locating the -# desired runtime and having it run the stage 2 bootstrap. This means it can't -# assume much about the current runtime and environment. e.g., the current -# runtime may not be the correct one, the zip may not have been extract, the -# runfiles env vars may not be set, etc. -# -# NOTE: This program must retain compatibility with a wide variety of Python -# versions since it is run by an unknown Python interpreter. - -import sys - -# The Python interpreter unconditionally prepends the directory containing this -# script (following symlinks) to the import path. This is the cause of #9239, -# and is a special case of #7091. We therefore explicitly delete that entry. -# TODO(#7091): Remove this hack when no longer necessary. -del sys.path[0] - -import os -import shutil -import subprocess -import tempfile -import zipfile - -# runfiles-relative path -_STAGE2_BOOTSTRAP = "%stage2_bootstrap%" -# runfiles-relative path to venv's bin/python3. Empty if venv not being used. -_PYTHON_BINARY = "%python_binary%" -# runfiles-relative path, absolute path, or single word. The actual Python -# executable to use. -_PYTHON_BINARY_ACTUAL = "%python_binary_actual%" -_WORKSPACE_NAME = "%workspace_name%" - - -def print_verbose(*args, mapping=None, values=None): - if bool(os.environ.get("RULES_PYTHON_BOOTSTRAP_VERBOSE")): - if mapping is not None: - for key, value in sorted((mapping or {}).items()): - print( - "bootstrap: stage 1:", - *args, - f"{key}={value!r}", - file=sys.stderr, - flush=True, - ) - elif values is not None: - for i, v in enumerate(values): - print( - "bootstrap: stage 1:", - *args, - f"[{i}] {v!r}", - file=sys.stderr, - flush=True, - ) - else: - print("bootstrap: stage 1:", *args, file=sys.stderr, flush=True) - - -# Return True if running on Windows -def is_windows(): - return os.name == "nt" - - -def get_windows_path_with_unc_prefix(path): - """Adds UNC prefix after getting a normalized absolute Windows path. - - No-op for non-Windows platforms or if running under python2. - """ - path = path.strip() - - # No need to add prefix for non-Windows platforms. - # And \\?\ doesn't work in python 2 or on mingw - if not is_windows() or sys.version_info[0] < 3: - return path - - # Starting in Windows 10, version 1607(OS build 14393), MAX_PATH limitations have been - # removed from common Win32 file and directory functions. - # Related doc: https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later - import platform - - if platform.win32_ver()[1] >= "10.0.14393": - return path - - # import sysconfig only now to maintain python 2.6 compatibility - import sysconfig - - if sysconfig.get_platform() == "mingw": - return path - - # Lets start the unicode fun - unicode_prefix = "\\\\?\\" - if path.startswith(unicode_prefix): - return path - - # os.path.abspath returns a normalized absolute path - return unicode_prefix + os.path.abspath(path) - - -def has_windows_executable_extension(path): - return path.endswith(".exe") or path.endswith(".com") or path.endswith(".bat") - - -if ( - _PYTHON_BINARY - and is_windows() - and not has_windows_executable_extension(_PYTHON_BINARY) -): - _PYTHON_BINARY = _PYTHON_BINARY + ".exe" - - -def search_path(name): - """Finds a file in a given search path.""" - search_path = os.getenv("PATH", os.defpath).split(os.pathsep) - for directory in search_path: - if directory: - path = os.path.join(directory, name) - if os.path.isfile(path) and os.access(path, os.X_OK): - return path - return None - - -def find_python_binary(module_space): - """Finds the real Python binary if it's not a normal absolute path.""" - if _PYTHON_BINARY: - return find_binary(module_space, _PYTHON_BINARY) - else: - return find_binary(module_space, _PYTHON_BINARY_ACTUAL) - - -def find_binary(module_space, bin_name): - """Finds the real binary if it's not a normal absolute path.""" - if not bin_name: - return None - if bin_name.startswith("//"): - # Case 1: Path is a label. Not supported yet. - raise AssertionError( - "Bazel does not support execution of Python interpreters via labels yet" - ) - elif os.path.isabs(bin_name): - # Case 2: Absolute path. - return bin_name - # Use normpath() to convert slashes to os.sep on Windows. - elif os.sep in os.path.normpath(bin_name): - # Case 3: Path is relative to the repo root. - return os.path.join(module_space, bin_name) - else: - # Case 4: Path has to be looked up in the search path. - return search_path(bin_name) - - -def extract_zip(zip_path, dest_dir): - """Extracts the contents of a zip file, preserving the unix file mode bits. - - These include the permission bits, and in particular, the executable bit. - - Ideally the zipfile module should set these bits, but it doesn't. See: - https://bugs.python.org/issue15795. - - Args: - zip_path: The path to the zip file to extract - dest_dir: The path to the destination directory - """ - zip_path = get_windows_path_with_unc_prefix(zip_path) - dest_dir = get_windows_path_with_unc_prefix(dest_dir) - with zipfile.ZipFile(zip_path) as zf: - for info in zf.infolist(): - zf.extract(info, dest_dir) - # UNC-prefixed paths must be absolute/normalized. See - # https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file#maximum-path-length-limitation - file_path = os.path.abspath(os.path.join(dest_dir, info.filename)) - # The Unix st_mode bits (see "man 7 inode") are stored in the upper 16 - # bits of external_attr. Of those, we set the lower 12 bits, which are the - # file mode bits (since the file type bits can't be set by chmod anyway). - attrs = info.external_attr >> 16 - if attrs != 0: # Rumor has it these can be 0 for zips created on Windows. - os.chmod(file_path, attrs & 0o7777) - - -# Create the runfiles tree by extracting the zip file -def create_module_space(): - temp_dir = tempfile.mkdtemp("", "Bazel.runfiles_") - extract_zip(os.path.dirname(__file__), temp_dir) - # IMPORTANT: Later code does `rm -fr` on dirname(module_space) -- it's - # important that deletion code be in sync with this directory structure - return os.path.join(temp_dir, "runfiles") - - -def execute_file( - python_program, - main_filename, - args, - env, - module_space, - workspace, -): - # type: (str, str, list[str], dict[str, str], str, str|None, str|None) -> ... - """Executes the given Python file using the various environment settings. - - This will not return, and acts much like os.execv, except is much - more restricted, and handles Bazel-related edge cases. - - Args: - python_program: (str) Path to the Python binary to use for execution - main_filename: (str) The Python file to execute - args: (list[str]) Additional args to pass to the Python file - env: (dict[str, str]) A dict of environment variables to set for the execution - module_space: (str) Path to the module space/runfiles tree directory - workspace: (str|None) Name of the workspace to execute in. This is expected to be a - directory under the runfiles tree. - """ - # We want to use os.execv instead of subprocess.call, which causes - # problems with signal passing (making it difficult to kill - # Bazel). However, these conditions force us to run via - # subprocess.call instead: - # - # - On Windows, os.execv doesn't handle arguments with spaces - # correctly, and it actually starts a subprocess just like - # subprocess.call. - # - When running in a workspace or zip file, we need to clean up the - # workspace after the process finishes so control must return here. - try: - subprocess_argv = [python_program, main_filename] + args - print_verbose("subprocess argv:", values=subprocess_argv) - print_verbose("subprocess env:", mapping=env) - print_verbose("subprocess cwd:", workspace) - ret_code = subprocess.call(subprocess_argv, env=env, cwd=workspace) - sys.exit(ret_code) - finally: - # NOTE: dirname() is called because create_module_space() creates a - # sub-directory within a temporary directory, and we want to remove the - # whole temporary directory. - shutil.rmtree(os.path.dirname(module_space), True) - - -def main(): - print_verbose("running zip main bootstrap") - print_verbose("initial argv:", values=sys.argv) - print_verbose("initial environ:", mapping=os.environ) - print_verbose("initial sys.executable", sys.executable) - print_verbose("initial sys.version", sys.version) - - args = sys.argv[1:] - - new_env = {} - - # The main Python source file. - # The magic string percent-main-percent is replaced with the runfiles-relative - # filename of the main file of the Python binary in BazelPythonSemantics.java. - main_rel_path = _STAGE2_BOOTSTRAP - if is_windows(): - main_rel_path = main_rel_path.replace("/", os.sep) - - module_space = create_module_space() - print_verbose("extracted runfiles to:", module_space) - - new_env["RUNFILES_DIR"] = module_space - - # Don't prepend a potentially unsafe path to sys.path - # See: https://docs.python.org/3.11/using/cmdline.html#envvar-PYTHONSAFEPATH - new_env["PYTHONSAFEPATH"] = "1" - - main_filename = os.path.join(module_space, main_rel_path) - main_filename = get_windows_path_with_unc_prefix(main_filename) - assert os.path.exists(main_filename), ( - "Cannot exec() %r: file not found." % main_filename - ) - assert os.access(main_filename, os.R_OK), ( - "Cannot exec() %r: file not readable." % main_filename - ) - - python_program = find_python_binary(module_space) - if python_program is None: - raise AssertionError("Could not find python binary: " + _PYTHON_BINARY) - - # When a venv is used, the `bin/python3` symlink has to be recreated. - if _PYTHON_BINARY: - # The venv bin/python3 interpreter should always be under runfiles, but - # double check. We don't want to accidentally create symlinks elsewhere. - if not python_program.startswith(module_space): - raise AssertionError( - "Program's venv binary not under runfiles: {python_program}" - ) - - if os.path.isabs(_PYTHON_BINARY_ACTUAL): - symlink_to = _PYTHON_BINARY_ACTUAL - elif "/" in _PYTHON_BINARY_ACTUAL: - symlink_to = os.path.join(module_space, _PYTHON_BINARY_ACTUAL) - else: - symlink_to = search_path(_PYTHON_BINARY_ACTUAL) - if not symlink_to: - raise AssertionError( - f"Python interpreter to use not found on PATH: {_PYTHON_BINARY_ACTUAL}" - ) - - # The bin/ directory may not exist if it is empty. - os.makedirs(os.path.dirname(python_program), exist_ok=True) - try: - os.symlink(symlink_to, python_program) - except OSError as e: - raise Exception( - f"Unable to create venv python interpreter symlink: {python_program} -> {symlink_to}" - ) from e - - # Some older Python versions on macOS (namely Python 3.7) may unintentionally - # leave this environment variable set after starting the interpreter, which - # causes problems with Python subprocesses correctly locating sys.executable, - # which subsequently causes failure to launch on Python 3.11 and later. - if "__PYVENV_LAUNCHER__" in os.environ: - del os.environ["__PYVENV_LAUNCHER__"] - - new_env.update((key, val) for key, val in os.environ.items() if key not in new_env) - - workspace = None - # If RUN_UNDER_RUNFILES equals 1, it means we need to - # change directory to the right runfiles directory. - # (So that the data files are accessible) - if os.environ.get("RUN_UNDER_RUNFILES") == "1": - workspace = os.path.join(module_space, _WORKSPACE_NAME) - - sys.stdout.flush() - execute_file( - python_program, - main_filename, - args, - new_env, - module_space, - workspace, - ) - - -if __name__ == "__main__": - main() diff --git a/python/private/zipapp/BUILD.bazel b/python/private/zipapp/BUILD.bazel new file mode 100644 index 0000000000..66f0b4f667 --- /dev/null +++ b/python/private/zipapp/BUILD.bazel @@ -0,0 +1,48 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + +package( + default_visibility = ["//:__subpackages__"], +) + +licenses(["notice"]) + +filegroup( + name = "distribution", + srcs = glob(["**"]), +) + +filegroup( + name = "zip_main_template", + srcs = ["zip_main_template.py"], + visibility = ["//visibility:public"], +) + +filegroup( + name = "zip_shell_template", + srcs = ["zip_shell_template.sh"], + visibility = ["//visibility:public"], +) + +filegroup( + name = "zipapp_stage2_bootstrap_template", + srcs = ["zipapp_stage2_bootstrap_template.py"], + visibility = ["//visibility:public"], +) + +bzl_library( + name = "py_zipapp_rule", + srcs = ["py_zipapp_rule.bzl"], + deps = [ + "//python/private:attributes", + "//python/private:builders", + "//python/private:common", + "//python/private:common_labels", + "//python/private:py_executable_info", + "//python/private:py_internal", + "//python/private:py_runtime_info", + "//python/private:toolchain_types", + "//python/private:transition_labels", + "@bazel_skylib//lib:paths", + "@rules_python_internal//:rules_python_config", + ], +) diff --git a/python/private/zipapp/py_zipapp_rule.bzl b/python/private/zipapp/py_zipapp_rule.bzl new file mode 100644 index 0000000000..4d31c99311 --- /dev/null +++ b/python/private/zipapp/py_zipapp_rule.bzl @@ -0,0 +1,443 @@ +"""Implementation of the zipapp rules.""" + +load("@bazel_skylib//lib:paths.bzl", "paths") +load("@rules_python_internal//:rules_python_config.bzl", rp_config = "config") +load("//python/private:attributes.bzl", "apply_config_settings_attr") +load("//python/private:builders.bzl", "builders") +load( + "//python/private:common.bzl", + "BUILTIN_BUILD_PYTHON_ZIP", + "actions_run", + "create_windows_exe_launcher", + "is_windows_platform", + "maybe_builtin_build_python_zip", + "maybe_create_repo_mapping", + "runfiles_root_path", +) +load("//python/private:common_labels.bzl", "labels") +load("//python/private:py_executable_info.bzl", "PyExecutableInfo") +load("//python/private:py_internal.bzl", "py_internal") +load("//python/private:py_runtime_info.bzl", "PyRuntimeInfo") +load("//python/private:toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE", "LAUNCHER_MAKER_TOOLCHAIN_TYPE") +load("//python/private:transition_labels.bzl", "TRANSITION_LABELS") + +def _is_symlink(f): + if hasattr(f, "is_symlink"): + return str(int(f.is_symlink)) + else: + return "-1" + +def _create_zipapp_main_py(ctx, py_runtime, py_executable, stage2_bootstrap, runfiles, explicit_symlinks): + venv_python_exe = py_executable.venv_python_exe + if venv_python_exe: + venv_python_exe_path = runfiles_root_path(ctx, venv_python_exe.short_path) + else: + venv_python_exe_path = "" + + if py_runtime.interpreter: + python_binary_actual_path = runfiles_root_path(ctx, py_runtime.interpreter.short_path) + else: + python_binary_actual_path = py_runtime.interpreter_path + + zip_main_py = ctx.actions.declare_file(ctx.label.name + ".zip_main.py") + + args = ctx.actions.args() + args.add(py_runtime.zip_main_template, format = "--template=%s") + args.add(zip_main_py, format = "--output=%s") + + args.add( + "%EXTRACT_DIR%=" + paths.join( + (ctx.label.repo_name or "_main"), + ctx.label.package, + ctx.label.name, + ), + format = "--substitution=%s", + ) + args.add("%python_binary%=" + venv_python_exe_path, format = "--substitution=%s") + args.add("%python_binary_actual%=" + python_binary_actual_path, format = "--substitution=%s") + args.add("%stage2_bootstrap%=" + runfiles_root_path(ctx, stage2_bootstrap.short_path), format = "--substitution=%s") + args.add("%workspace_name%=" + ctx.workspace_name, format = "--substitution=%s") + + hash_files_manifest = ctx.actions.args() + hash_files_manifest.use_param_file("--hash_files_manifest=%s", use_always = True) + hash_files_manifest.set_param_file_format("multiline") + + inputs = builders.DepsetBuilder() + inputs.add(py_runtime.zip_main_template) + _build_manifest(ctx, hash_files_manifest, runfiles, explicit_symlinks, inputs) + + actions_run( + ctx, + executable = ctx.attr._zip_main_maker, + arguments = [args, hash_files_manifest], + inputs = inputs.build(), + outputs = [zip_main_py], + mnemonic = "PyZipAppCreateMainPy", + progress_message = "Generating zipapp __main__.py: %{label}", + ) + return zip_main_py + +def _map_zip_empty_filenames(list_paths_cb): + return ["rf-empty|" + path for path in list_paths_cb().to_list()] + +def _map_zip_runfiles(file): + return "rf-file|" + _is_symlink(file) + "|" + file.short_path + "|" + file.path + +def _map_zip_symlinks(entry): + return "rf-symlink|" + _is_symlink(entry.target_file) + "|" + entry.path + "|" + entry.target_file.path + +def _map_zip_root_symlinks(entry): + return "rf-root-symlink|" + _is_symlink(entry.target_file) + "|" + entry.path + "|" + entry.target_file.path + +def _map_explicit_symlinks(entry): + return "symlink|" + entry.runfiles_path + "|" + entry.link_to_path + +def _build_manifest(ctx, manifest, runfiles, explicit_symlinks, inputs): + manifest.add_all( + # NOTE: Accessing runfiles.empty_filenames materializes them. A lambda + # is used to defer that. + [lambda: runfiles.empty_filenames], + map_each = _map_zip_empty_filenames, + allow_closure = True, + ) + + manifest.add_all(runfiles.files, map_each = _map_zip_runfiles) + manifest.add_all(runfiles.symlinks, map_each = _map_zip_symlinks) + manifest.add_all(runfiles.root_symlinks, map_each = _map_zip_root_symlinks) + manifest.add_all(explicit_symlinks, map_each = _map_explicit_symlinks) + + inputs.add(runfiles.files) + inputs.add([entry.target_file for entry in runfiles.symlinks.to_list()]) + inputs.add([entry.target_file for entry in runfiles.root_symlinks.to_list()]) + for entry in explicit_symlinks.to_list(): + inputs.add(entry.files) + + zip_repo_mapping_manifest = maybe_create_repo_mapping( + ctx = ctx, + runfiles = runfiles, + ) + if zip_repo_mapping_manifest: + # NOTE: rf-root-symlink is used to make it show up under the runfiles + # subdirectory within the zip. + manifest.add( + zip_repo_mapping_manifest.path, + format = "rf-root-symlink|0|_repo_mapping|%s", + ) + inputs.add(zip_repo_mapping_manifest) + +def _create_zip(ctx, py_runtime, py_executable, stage2_bootstrap): + output = ctx.actions.declare_file(ctx.label.name + ".zip") + manifest = ctx.actions.args() + manifest.use_param_file("%s", use_always = True) + manifest.set_param_file_format("multiline") + + runfiles = builders.RunfilesBuilder() + + runfiles.add(py_runtime.files) + if py_executable.venv_python_exe: + runfiles.add(py_executable.venv_python_exe) + + if py_executable.venv_interpreter_runfiles: + runfiles.add(py_executable.venv_interpreter_runfiles) + runfiles.add(py_executable.app_runfiles) + runfiles.add(stage2_bootstrap) + + runfiles = runfiles.build(ctx) + + explicit_symlinks = depset(transitive = [ + py_executable.venv_interpreter_symlinks, + py_executable.venv_app_symlinks, + ]) + zip_main = _create_zipapp_main_py( + ctx, + py_runtime, + py_executable, + stage2_bootstrap, + runfiles, + explicit_symlinks, + ) + inputs = builders.DepsetBuilder() + manifest.add("regular|0|__main__.py|{}".format(zip_main.path)) + inputs.add(zip_main) + _build_manifest(ctx, manifest, runfiles, explicit_symlinks, inputs) + + zipper_args = ctx.actions.args() + zipper_args.add(output) + zipper_args.add(ctx.workspace_name, format = "--workspace-name=%s") + zipper_args.add( + str(int(py_internal.get_legacy_external_runfiles(ctx))), + format = "--legacy-external-runfiles=%s", + ) + if ctx.attr.compression: + zipper_args.add(ctx.attr.compression, format = "--compression=%s") + zipper_args.add("--runfiles-dir=runfiles") + + is_windows = is_windows_platform(ctx) + zipper_args.add("\\" if is_windows else "/", format = "--target-platform-pathsep=%s") + + actions_run( + ctx, + executable = ctx.attr._zipper, + arguments = [manifest, zipper_args], + inputs = inputs.build(), + outputs = [output], + mnemonic = "PyZipAppCreateZip", + progress_message = "Reticulating zipapp archive: %{label} into %{output}", + ) + return output + +def _create_shell_bootstrap(ctx, py_runtime, py_executable, stage2_bootstrap): + preamble = ctx.actions.declare_file(ctx.label.name + ".preamble.sh") + + bundled_pyexe_path = "" + external_pyexe_path = "" + if py_runtime.interpreter_path: + external_pyexe_path = py_runtime.interpreter_path + else: + bundled_pyexe_path = runfiles_root_path(ctx, py_runtime.interpreter.short_path) + + substitutions = { + "%BUNDLED_PYEXE_PATH%": bundled_pyexe_path, + "%EXTERNAL_PYEXE_PATH%": external_pyexe_path, + "%EXTRACT_DIR%": paths.join( + (ctx.label.repo_name or "_main"), + ctx.label.package, + ctx.label.name, + ), + "%INTERPRETER_ARGS%": "\n".join([ + '"{}"'.format(v) + for v in py_executable.interpreter_args + ]), + "%STAGE2_BOOTSTRAP%": runfiles_root_path(ctx, stage2_bootstrap.short_path), + } + ctx.actions.expand_template( + template = ctx.file._zip_shell_template, + output = preamble, + substitutions = substitutions, + is_executable = True, + ) + return preamble + +def _create_self_executable_zip(ctx, preamble, zip_file): + pyz = ctx.actions.declare_file(ctx.label.name + ".pyz") + args = ctx.actions.args() + args.add(preamble) + args.add(zip_file) + args.add(pyz) + actions_run( + ctx, + executable = ctx.attr._exe_zip_maker, + arguments = [args], + inputs = depset([preamble, zip_file]), + outputs = [pyz], + mnemonic = "PyZipAppCreateExecutableZip", + progress_message = "Reticulating zipapp executable: %{label} into %{output}", + ) + return pyz + +def _py_zipapp_executable_impl(ctx): + py_executable = ctx.attr.binary[PyExecutableInfo] + py_runtime = ctx.attr.binary[PyRuntimeInfo] + + stage2_bootstrap = py_executable.stage2_bootstrap + + zip_file = _create_zip(ctx, py_runtime, py_executable, stage2_bootstrap) + if ctx.attr.executable: + if is_windows_platform(ctx): + executable = ctx.actions.declare_file(ctx.label.name + ".exe") + + # The zipapp is an opaque zip file, so the Bazel Python launcher doesn't + # know how to look inside it to find the Python interpreter. This means + # we can only use system paths or programs on PATH to bootstrap. + if py_runtime.interpreter_path: + bootstrap_python_path = py_runtime.interpreter_path + else: + # A special value the Bazel Python launcher recognized to skip + # lookup in the runfiles and uses `python.exe` from PATH. + bootstrap_python_path = "python" + + create_windows_exe_launcher( + ctx, + output = executable, + # The path to a python to use to invoke e.g. `python.exe foo.zip` + python_binary_path = bootstrap_python_path, + # Tell the launcher to invoke `python_binary_path` on itself + # after removing its file extension and appending `.zip`. + use_zip_file = True, + ) + default_outputs = [executable, zip_file] + else: + preamble = _create_shell_bootstrap(ctx, py_runtime, py_executable, stage2_bootstrap) + executable = _create_self_executable_zip(ctx, preamble, zip_file) + default_outputs = [executable] + else: + # Bazel requires executable=True rules to have an executable given, so give + # a fake one to satisfy that. + default_outputs = [zip_file] + executable = ctx.actions.declare_file(ctx.label.name + "-not-executable") + ctx.actions.write(executable, "echo 'ERROR: Non executable zip file'; exit 1") + + return [ + DefaultInfo( + files = depset(default_outputs), + runfiles = ctx.runfiles(files = default_outputs), + executable = executable, + ), + OutputGroupInfo( + python_zip_file = depset([zip_file]), + ), + ] + +def _transition_zipapp_impl(settings, attr): + settings = apply_config_settings_attr(dict(settings), attr) + + # Force this to false, otherwise the binary is already a zipapp + settings[labels.BUILD_PYTHON_ZIP] = False + maybe_builtin_build_python_zip("false", settings) + return settings + +_zipapp_transition = transition( + implementation = _transition_zipapp_impl, + inputs = TRANSITION_LABELS, + outputs = TRANSITION_LABELS + [ + labels.BUILD_PYTHON_ZIP, + ] + BUILTIN_BUILD_PYTHON_ZIP, +) + +_ATTRS = { + "binary": attr.label( + doc = """ +A `py_binary` or `py_test` (or equivalent) target to package. +""", + providers = [PyExecutableInfo, PyRuntimeInfo], + mandatory = True, + ), + "compression": attr.string( + doc = """ +The compression level to use. + +Typically 0 to 9, with higher numbers being to compress more. +""", + default = "", + ), + "config_settings": attr.label_keyed_string_dict( + doc = """ +Config settings to change for this target. + +The keys are labels for settings, and the values are strings for the new value +to use. Pass `Label` objects or canonical label strings for the keys to ensure +they resolve as expected (canonical labels start with `@@` and can be +obtained by calling `str(Label(...))`). + +Most `@rules_python//python/config_setting` settings can be used here, which +allows, for example, making only a certain `py_binary` use +{obj}`--bootstrap_impl=script`. + +Additional or custom config settings can be registered using the +{obj}`add_transition_setting` API. This allows, for example, forcing a +particular CPU, or defining a custom setting that `select()` uses elsewhere +to pick between `pip.parse` hubs. See the [How to guide on multiple +versions of a library] for a more concrete example. + +:::{note} +These values are transitioned on, so will affect the analysis graph and the +associated memory overhead. The more unique configurations in your overall +build, the more memory and (often unnecessary) re-analysis and re-building +can occur. See +https://bazel.build/extending/config#memory-performance-considerations for +more information about risks and considerations. +::: +""", + ), + "executable": attr.bool( + doc = """ +Whether the output should be an executable zip file. +""", + default = True, + ), + # Required to opt-in to the transition feature. + "_allowlist_function_transition": attr.label( + default = "@bazel_tools//tools/allowlists/function_transition_allowlist", + ), + "_exe_zip_maker": attr.label( + cfg = "exec", + default = "//tools/private/zipapp:exe_zip_maker", + ), + "_launcher": attr.label( + cfg = "target", + # NOTE: This is an executable, but is only used for Windows. It + # can't have executable=True because the backing target is an + # empty target for other platforms. + default = "//tools/launcher:launcher", + ), + "_windows_constraints": attr.label_list( + default = [ + "@platforms//os:windows", + ], + ), + "_zip_main_maker": attr.label( + cfg = "exec", + default = "//tools/private/zipapp:zip_main_maker", + ), + "_zip_shell_template": attr.label( + default = ":zip_shell_template", + allow_single_file = True, + ), + "_zipper": attr.label( + cfg = "exec", + default = "//tools/private/zipapp:zipper", + ), +} | ({ + "_windows_launcher_maker": attr.label( + default = "@bazel_tools//tools/launcher:launcher_maker", + cfg = "exec", + executable = True, + ), +} if not rp_config.bazel_9_or_later else {}) + +_TOOLCHAINS = [EXEC_TOOLS_TOOLCHAIN_TYPE] + ([LAUNCHER_MAKER_TOOLCHAIN_TYPE] if rp_config.bazel_9_or_later else []) + +_COMMON_RULE_DOC = """ + +Output groups: + +* `python_zip_file`: (*deprecated*) The plain, non-self-executable zipapp zipfile. + *This output group is deprecated and retained for compatibility with + the previous implicit zipapp functionality. Set `executable=False` + and use the default output of the target instead.* + +:::{versionadded} 1.9.0 +::: +""".lstrip() + +py_zipapp_binary = rule( + doc = """ +Packages a `py_binary` as a Python zipapp. + +{} +""".format(_COMMON_RULE_DOC), + implementation = _py_zipapp_executable_impl, + attrs = _ATTRS, + # NOTE: While this is marked executable, it is conditionally executable + # based on the `executable` attribute. + executable = True, + toolchains = _TOOLCHAINS, + cfg = _zipapp_transition, +) + +py_zipapp_test = rule( + doc = """ +Packages a `py_test` as a Python zipapp. + +This target is also a valid test target to run. + +{} +""".format(_COMMON_RULE_DOC), + implementation = _py_zipapp_executable_impl, + attrs = _ATTRS, + # NOTE: While this is marked as a test, it is conditionally executable + # based on the `executable` attribute. + test = True, + toolchains = _TOOLCHAINS, + cfg = _zipapp_transition, +) diff --git a/python/private/zipapp/zip_main_template.py b/python/private/zipapp/zip_main_template.py new file mode 100644 index 0000000000..709e08815c --- /dev/null +++ b/python/private/zipapp/zip_main_template.py @@ -0,0 +1,404 @@ +# Template for the __main__.py file inserted into zip files +# +# Generated file from @rules_python//python/private/zipapp:zip_main_template.py +# +# NOTE: This file is a "stage 1" bootstrap, so it's responsible for locating the +# desired runtime and having it run the stage 2 bootstrap. This means it can't +# assume much about the current runtime and environment. e.g., the current +# runtime may not be the correct one, the zip may not have been extracted, the +# runfiles env vars may not be set, etc. +# +# NOTE: This program must retain compatibility with a wide variety of Python +# versions since it is run by an unknown Python interpreter. +# +# NOTE: For a self-executable zip, this file may not be the entry point +# for the program and may be skipped entirely; the self-executable zip +# preamble may jump directly to the stage2 bootstrap. + +import sys + +# The Python interpreter unconditionally prepends the directory containing this +# script (following symlinks) to the import path. This is the cause of #9239, +# and is a special case of #7091. We therefore explicitly delete that entry. +# TODO(#7091): Remove this hack when no longer necessary. +del sys.path[0] + +import os # noqa: E402 +import shutil # noqa: E402 +import stat # noqa: E402 +import subprocess # noqa: E402 +import tempfile # noqa: E402 +import zipfile # noqa: E402 +from os.path import basename, dirname, join, normpath # noqa: E402 + +# runfiles-root-relative path +_STAGE2_BOOTSTRAP = "%stage2_bootstrap%" +# runfiles-root-relative path to venv's bin/python3. Empty if venv not being used. +_PYTHON_BINARY_VENV = "%python_binary%" +# runfiles-root-relative path, absolute path, or single word. The actual Python +# executable to use. +_PYTHON_BINARY_ACTUAL = "%python_binary_actual%" +_WORKSPACE_NAME = "%workspace_name%" +# relative path under EXTRACT_ROOT to extract to. +EXTRACT_DIR = "%EXTRACT_DIR%" +APP_HASH = "%APP_HASH%" + +EXTRACT_ROOT = os.environ.get("RULES_PYTHON_EXTRACT_ROOT") +IS_WINDOWS = os.name == "nt" + + +# Change the paths with Unix-style forward slashes to backslashes for Windows. +# Windows usually transparently rewrites them, but e.g. `\\?\` paths require +# backslashes to be properly understood by Windows APIs. +if IS_WINDOWS: + + def norm_slashes(s): + if not s: + return s + return s.replace("/", "\\") + + _STAGE2_BOOTSTRAP = norm_slashes(_STAGE2_BOOTSTRAP) + _PYTHON_BINARY_VENV = norm_slashes(_PYTHON_BINARY_VENV) + _PYTHON_BINARY_ACTUAL = norm_slashes(_PYTHON_BINARY_ACTUAL) + EXTRACT_DIR = norm_slashes(EXTRACT_DIR) + EXTRACT_ROOT = norm_slashes(EXTRACT_ROOT) + +IS_VERBOSE = bool(os.environ.get("RULES_PYTHON_BOOTSTRAP_VERBOSE")) + + +def print_verbose(*args, mapping=None, values=None): + if not IS_VERBOSE: + return + if mapping is not None: + for key, value in sorted((mapping or {}).items()): + print( + "bootstrap: stage 1:", + *args, + f"{key}={value!r}", + file=sys.stderr, + flush=True, + ) + elif values is not None: + for i, v in enumerate(values): + print( + "bootstrap: stage 1:", + *args, + f"[{i}] {v!r}", + file=sys.stderr, + flush=True, + ) + else: + print("bootstrap: stage 1:", *args, file=sys.stderr, flush=True) + + +def get_windows_path_with_unc_prefix(path): + """Adds UNC prefix after getting a normalized absolute Windows path. + + No-op for non-Windows platforms or if running under python2. + """ + path = path.strip() + + # No need to add prefix for non-Windows platforms. + # And \\?\ doesn't work in python 2 or on mingw + if not IS_WINDOWS or sys.version_info[0] < 3: + return path + + # Starting in Windows 10, version 1607(OS build 14393), MAX_PATH limitations have been + # removed from common Win32 file and directory functions. + # Related doc: https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later + import platform + + if platform.win32_ver()[1] >= "10.0.14393": + return path + + # import sysconfig only now to maintain python 2.6 compatibility + import sysconfig + + if sysconfig.get_platform() == "mingw": + return path + + # Lets start the unicode fun + unicode_prefix = "\\\\?\\" + if path.startswith(unicode_prefix): + return path + + # os.path.abspath returns a normalized absolute path + return unicode_prefix + os.path.abspath(path) + + +def search_path(name): + """Finds a file in a given search path.""" + search_path = os.getenv("PATH", os.defpath).split(os.pathsep) + for directory in search_path: + if directory: + path = join(directory, name) + if os.path.isfile(path) and os.access(path, os.X_OK): + return path + return None + + +def find_binary(runfiles_root, bin_name): + """Finds the real binary if it's not a normal absolute path.""" + if not bin_name: + return None + if bin_name.startswith("//"): + # Case 1: Path is a label. Not supported yet. + raise AssertionError( + "Bazel does not support execution of Python interpreters via labels" + ) + elif os.path.isabs(bin_name): + # Case 2: Absolute path. + return bin_name + # Use normpath() to convert slashes to os.sep on Windows. + elif os.sep in normpath(bin_name): + # Case 3: Path is relative to the repo root. + return join(runfiles_root, bin_name) + else: + # Case 4: Path has to be looked up in the search path. + return search_path(bin_name) + + +def extract_zip(zip_path, dest_dir): + """Extracts the contents of a zip file, preserving the unix file mode bits. + + These include the permission bits, and in particular, the executable bit. + + Ideally the zipfile module should set these bits, but it doesn't. See: + https://bugs.python.org/issue15795. + + Args: + zip_path: The path to the zip file to extract + dest_dir: The path to the destination directory + """ + zip_path = get_windows_path_with_unc_prefix(zip_path) + dest_dir = get_windows_path_with_unc_prefix(dest_dir) + with zipfile.ZipFile(zip_path) as zf: + for info in zf.infolist(): + file_path = os.path.abspath(join(dest_dir, info.filename)) + # If the file exists, it might be a symlink or read-only file from a previous extraction. + # Unlink it first so zipfile.extract doesn't corrupt the symlink target or fail on read-only files. + if os.path.lexists(file_path) and not os.path.isdir(file_path): + try: + os.unlink(file_path) + except OSError: + # On Windows, unlinking a read-only file fails. + os.chmod(file_path, stat.S_IWRITE) + os.unlink(file_path) + + zf.extract(info, dest_dir) + # The Unix st_mode bits (see "man 7 inode") are stored in the upper 16 + # bits of external_attr. + attrs = info.external_attr >> 16 + # Symlink bit in st_mode is 0o120000. + if (attrs & 0o170000) == 0o120000: + with open(file_path, "r") as f: + target = f.read() + os.remove(file_path) + if IS_WINDOWS: + entry_path = normpath(join(dirname(info.filename), target)) + # Zip lookup uses forward slashes, target has backslashes. + entry_path = entry_path.replace("\\", "/") + try: + target_is_directory = zf.getinfo(entry_path).is_dir() + except KeyError: + # Directories aren't stored in zips, so a missing + # target means it points to a directory. + target_is_directory = True + else: + target_is_directory = False + os.symlink(target, file_path, target_is_directory=target_is_directory) + # Of those, we set the lower 12 bits, which are the + # file mode bits (since the file type bits can't be set by chmod anyway). + elif attrs != 0: # Rumor has it these can be 0 for zips created on Windows. + os.chmod(file_path, attrs & 0o7777) + + +# Create the runfiles tree by extracting the zip file +def create_runfiles_root(): + if EXTRACT_ROOT: + # Shorten the path for Windows in case long path support is disabled + if IS_WINDOWS: + hash_dir = APP_HASH[0:32] + extract_dir = basename(EXTRACT_DIR) + extract_root = join(EXTRACT_ROOT, extract_dir, hash_dir) + else: + extract_root = join(EXTRACT_ROOT, EXTRACT_DIR, APP_HASH) + extract_root = get_windows_path_with_unc_prefix(extract_root) + else: + extract_root = tempfile.mkdtemp("", "Bazel.runfiles_") + + extract_zip(dirname(__file__), extract_root) + print_verbose("extracted to:", extract_root) + # IMPORTANT: Later code does `rm -fr` on dirname(runfiles_root) -- it's + # important that deletion code be in sync with this directory structure + return join(extract_root, "runfiles") + + +def execute_file( + python_program, + main_filename, + args, + env, + runfiles_root, + workspace, +): + # type: (str, str, list[str], dict[str, str], str, str|None, str|None) -> ... + """Executes the given Python file using the various environment settings. + + This will not return, and acts much like os.execv, except is much + more restricted, and handles Bazel-related edge cases. + + Args: + python_program: (str) Path to the Python binary to use for execution + main_filename: (str) The Python file to execute + args: (list[str]) Additional args to pass to the Python file + env: (dict[str, str]) A dict of environment variables to set for the execution + runfiles_root: (str) Path to the runfiles tree directory + workspace: (str|None) Name of the workspace to execute in. This is expected to be a + directory under the runfiles tree. + """ + # We want to use os.execv instead of subprocess.call, which causes + # problems with signal passing (making it difficult to kill + # Bazel). However, these conditions force us to run via + # subprocess.call instead: + # + # - On Windows, os.execv doesn't handle arguments with spaces + # correctly, and it actually starts a subprocess just like + # subprocess.call. + # - When running in a zip file, we need to clean up the + # workspace after the process finishes so control must return here. + try: + subprocess_argv = [python_program] + if not EXTRACT_ROOT: + subprocess_argv.append(f"-XRULES_PYTHON_ZIP_DIR={dirname(runfiles_root)}") + subprocess_argv.append(main_filename) + subprocess_argv += args + print_verbose("subprocess env:", mapping=env) + print_verbose("subprocess cwd:", workspace) + print_verbose("subprocess argv:", values=subprocess_argv) + ret_code = subprocess.call(subprocess_argv, env=env, cwd=workspace) + print_verbose("subprocess exit code:", ret_code) + sys.exit(ret_code) + finally: + if not EXTRACT_ROOT: + # NOTE: dirname() is called because create_runfiles_root() creates a + # sub-directory within a temporary directory, and we want to remove the + # whole temporary directory. + extract_root = dirname(runfiles_root) + print_verbose("cleanup: rmtree: ", extract_root) + shutil.rmtree(extract_root, True) + + +def finish_venv_setup(runfiles_root): + python_program = os.path.join(runfiles_root, _PYTHON_BINARY_VENV) + # When a venv is used, the `bin/python3` symlink may need to be created. + # This case occurs when "create venv at runtime" or "resolve python at + # runtime" modes are enabled. + if not os.path.exists(python_program): + # The venv bin/python3 interpreter should always be under runfiles, but + # double check. We don't want to accidentally create symlinks elsewhere + if not python_program.startswith(runfiles_root): + raise AssertionError( + "Program's venv binary not under runfiles: {python_program}" + ) + symlink_to = find_binary(runfiles_root, _PYTHON_BINARY_ACTUAL) + os.makedirs(dirname(python_program), exist_ok=True) + if os.path.lexists(python_program): + os.remove(python_program) + try: + os.symlink(symlink_to, python_program) + except OSError as e: + raise Exception( + f"Unable to create venv python interpreter symlink: {python_program} -> {symlink_to}" + ) from e + venv_root = dirname(dirname(python_program)) + pyvenv_cfg = join(venv_root, "pyvenv.cfg") + if not os.path.exists(pyvenv_cfg): + print_verbose("finish_venv_setup: create pyvenv.cfg:", pyvenv_cfg) + python_home = join(runfiles_root, dirname(_PYTHON_BINARY_ACTUAL)) + print_verbose("finish_venv_setup: pyvenv.cfg home:", python_home) + with open(pyvenv_cfg, "w") as fp: + # Until Windows supports a build-time generated venv using symlinks + # to directories, we have to write the full, absolute, path to PYTHONHOME + # so that support directories (e.g. DLLs, libs) can be found. + fp.write("home = {}\n".format(python_home)) + + return python_program + + +def main(): + print_verbose("running zip main bootstrap") + print_verbose("initial sys.version:", sys.version) + print_verbose("initial sys.executable:", sys.executable) + print_verbose("initial argv:", values=sys.argv) + print_verbose("initial environ:", mapping=os.environ) + print_verbose("stage2_bootstrap:", _STAGE2_BOOTSTRAP) + print_verbose("python_binary_venv:", _PYTHON_BINARY_VENV) + print_verbose("python_binary_actual:", _PYTHON_BINARY_ACTUAL) + print_verbose("workspace_name:", _WORKSPACE_NAME) + + args = sys.argv[1:] + + new_env = {} + + # The main Python source file. + main_rel_path = _STAGE2_BOOTSTRAP + if IS_WINDOWS: + main_rel_path = main_rel_path.replace("/", os.sep) + + runfiles_root = create_runfiles_root() + print_verbose("extracted runfiles to:", runfiles_root) + + new_env["RUNFILES_DIR"] = runfiles_root + + # Don't prepend a potentially unsafe path to sys.path + # See: https://docs.python.org/3.11/using/cmdline.html#envvar-PYTHONSAFEPATH + new_env["PYTHONSAFEPATH"] = "1" + + main_filename = join(runfiles_root, main_rel_path) + main_filename = get_windows_path_with_unc_prefix(main_filename) + assert os.path.exists(main_filename), ( + "Cannot exec() %r: file not found." % main_filename + ) + assert os.access(main_filename, os.R_OK), ( + "Cannot exec() %r: file not readable." % main_filename + ) + + if _PYTHON_BINARY_VENV: + python_program = finish_venv_setup(runfiles_root) + else: + python_program = find_binary(runfiles_root, _PYTHON_BINARY_ACTUAL) + if python_program is None: + raise AssertionError( + "Could not find python binary: " + _PYTHON_BINARY_ACTUAL + ) + + # Some older Python versions on macOS (namely Python 3.7) may unintentionally + # leave this environment variable set after starting the interpreter, which + # causes problems with Python subprocesses correctly locating sys.executable, + # which subsequently causes failure to launch on Python 3.11 and later. + if "__PYVENV_LAUNCHER__" in os.environ: + del os.environ["__PYVENV_LAUNCHER__"] + + new_env.update((key, val) for key, val in os.environ.items() if key not in new_env) + + workspace = None + # If RUN_UNDER_RUNFILES equals 1, it means we need to + # change directory to the right runfiles directory. + # (So that the data files are accessible) + if os.environ.get("RUN_UNDER_RUNFILES") == "1": + workspace = join(runfiles_root, _WORKSPACE_NAME) + + sys.stdout.flush() + execute_file( + python_program, + main_filename, + args, + new_env, + runfiles_root, + workspace, + ) + + +if __name__ == "__main__": + main() diff --git a/python/private/zipapp/zip_shell_template.sh b/python/private/zipapp/zip_shell_template.sh new file mode 100644 index 0000000000..d79331444a --- /dev/null +++ b/python/private/zipapp/zip_shell_template.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash + +set -e + +if [[ -n "${RULES_PYTHON_BOOTSTRAP_VERBOSE:-}" ]]; then + set -x +fi + +# runfiles-root-relative path +BUNDLED_PYEXE_PATH="%BUNDLED_PYEXE_PATH%" +# Absolute path or single word +EXTERNAL_PYEXE_PATH="%EXTERNAL_PYEXE_PATH%" +# runfiles-root-relative path +STAGE2_BOOTSTRAP="%STAGE2_BOOTSTRAP%" +EXTRACT_DIR="%EXTRACT_DIR%" +ZIP_HASH="%ZIP_HASH%" +declare -a INTERPRETER_ARGS_FROM_TARGET=( +%INTERPRETER_ARGS% +) + +declare -a interpreter_env +declare -a interpreter_args +declare -a additional_interpreter_args + +if [[ -z "${PYTHONSAFEPATH+x}" ]]; then + # ${FOO-WORD} expands to WORD if $FOO is undefined, and $FOO otherwise + interpreter_env+=("PYTHONSAFEPATH=${PYTHONSAFEPATH-1}") +fi + + +if [[ -n "${RULES_PYTHON_ADDITIONAL_INTERPRETER_ARGS}" ]]; then + read -a additional_interpreter_args <<< "${RULES_PYTHON_ADDITIONAL_INTERPRETER_ARGS}" + interpreter_args+=("${additional_interpreter_args[@]}") + unset RULES_PYTHON_ADDITIONAL_INTERPRETER_ARGS +fi + + +if [[ -n "$RULES_PYTHON_EXTRACT_ROOT" ]]; then + zip_dir="$RULES_PYTHON_EXTRACT_ROOT/$EXTRACT_DIR/$ZIP_HASH" + if [[ ! -e "$zip_dir/__main__.py" ]]; then + mkdir -p "$zip_dir" + # Unzip emits a warning and exits 1 with the prelude + ( unzip -q -d "$zip_dir" "$0" 2>/dev/null || true ) + fi +else + # NOTE: Macs have an old version of mktemp, so we must use only the + # minimal functionality of it. + zip_dir=$(mktemp -d) + # Unzip emits a warning and exits 1 with the prelude + ( unzip -q -d "$zip_dir" "$0" 2>/dev/null || true ) + if [[ -n "$zip_dir" && -z "${RULES_PYTHON_BOOTSTRAP_VERBOSE:-}" ]]; then + trap 'rm -fr "$zip_dir"' EXIT + fi +fi + +export RUNFILES_DIR="$zip_dir/runfiles" +if [[ ! -d "$RUNFILES_DIR" ]]; then + echo "Runfiles dir not found: zip extraction likely failed" 1>&2 + echo "Run with RULES_PYTHON_BOOTSTRAP_VERBOSE=1 to aid debugging" 1>&2 + exit 1 +fi + +if [[ -n "$BUNDLED_PYEXE_PATH" ]]; then + python_exe="$RUNFILES_DIR/$BUNDLED_PYEXE_PATH" +else + python_exe="$EXTERNAL_PYEXE_PATH" +fi + +command=( + env + "${interpreter_env[@]}" + "$python_exe" + "-XRULES_PYTHON_ZIP_DIR=$zip_dir" + "${interpreter_args[@]}" + "${INTERPRETER_ARGS_FROM_TARGET[@]}" + "$RUNFILES_DIR/$STAGE2_BOOTSTRAP" + "$@" +) + +# NOTE: because exec isn't used, signals don't propagate to the child +# TODO: Use exec and let the program handle cleanup. Without exec, +# signals don't propagate to the child nicely. +# See https://github.com/bazel-contrib/rules_python/issues/2043#issuecomment-2215469971 +# for more information. +"${command[@]}" +# Explicit exit is needed because the implicit next line the zip file this +# template is prepended to. +exit 0 diff --git a/python/private/zipapp/zipapp_stage2_bootstrap_template.py b/python/private/zipapp/zipapp_stage2_bootstrap_template.py new file mode 100644 index 0000000000..9fd71fefb9 --- /dev/null +++ b/python/private/zipapp/zipapp_stage2_bootstrap_template.py @@ -0,0 +1,10 @@ +import runpy +import shutil +import sys + +try: + sys.argv.pop(0) # Remove zipapp_stage2_bootstrap from args + runpy.run_path(sys.argv[0], run_name="__main__") +finally: + if zip_dir := sys._xoptions.get("RULES_PYTHON_ZIP_DIR"): + shutil.rmtree(zip_dir, True) diff --git a/python/py_binary.bzl b/python/py_binary.bzl index 48ea768948..1b59451893 100644 --- a/python/py_binary.bzl +++ b/python/py_binary.bzl @@ -14,13 +14,7 @@ """Public entry point for py_binary.""" -load("@rules_python_internal//:rules_python_config.bzl", "config") -load("//python/private:py_binary_macro.bzl", _starlark_py_binary = "py_binary") -load("//python/private:register_extension_info.bzl", "register_extension_info") -load("//python/private:util.bzl", "add_migration_tag") - -# buildifier: disable=native-python -_py_binary_impl = _starlark_py_binary if config.enable_pystar else native.py_binary +load("//python/private:py_binary_macro.bzl", _py_binary = "py_binary") def py_binary(**attrs): """Creates an executable Python program. @@ -34,6 +28,11 @@ def py_binary(**attrs): * `srcs_version`: cannot be `PY2` or `PY2ONLY` * `tags`: May have special marker values added, if not already present. + :::{versionchanged} 1.9.0 + The `PYTHONBREAKPOINT` environment variable is inherited. Use in combination + with {obj}`--debugger` to customize the debugger available and used. + ::: + Args: **attrs: Rule attributes forwarded onto the underlying {rule}`py_binary`. """ @@ -42,9 +41,4 @@ def py_binary(**attrs): if attrs.get("srcs_version") in ("PY2", "PY2ONLY"): fail("Python 2 is no longer supported: https://github.com/bazel-contrib/rules_python/issues/886") - _py_binary_impl(**add_migration_tag(attrs)) - -register_extension_info( - extension = py_binary, - label_regex_for_dep = "{extension_name}", -) + _py_binary(**attrs) diff --git a/python/py_cc_link_params_info.bzl b/python/py_cc_link_params_info.bzl index 02eff71c4d..9f25f17f4b 100644 --- a/python/py_cc_link_params_info.bzl +++ b/python/py_cc_link_params_info.bzl @@ -1,10 +1,5 @@ """Public entry point for PyCcLinkParamsInfo.""" -load("@rules_python_internal//:rules_python_config.bzl", "config") -load("//python/private:py_cc_link_params_info.bzl", _starlark_PyCcLinkParamsInfo = "PyCcLinkParamsInfo") +load("//python/private:py_cc_link_params_info.bzl", _PyCcLinkParamsInfo = "PyCcLinkParamsInfo") -PyCcLinkParamsInfo = ( - _starlark_PyCcLinkParamsInfo if ( - config.enable_pystar or config.BuiltinPyCcLinkParamsProvider == None - ) else config.BuiltinPyCcLinkParamsProvider -) +PyCcLinkParamsInfo = _PyCcLinkParamsInfo diff --git a/python/py_info.bzl b/python/py_info.bzl index 5697f58419..8e46926c53 100644 --- a/python/py_info.bzl +++ b/python/py_info.bzl @@ -14,8 +14,17 @@ """Public entry point for PyInfo.""" -load("@rules_python_internal//:rules_python_config.bzl", "config") -load("//python/private:py_info.bzl", _starlark_PyInfo = "PyInfo") -load("//python/private:reexports.bzl", "BuiltinPyInfo") +load( + "//python/private:py_info.bzl", + _PyInfo = "PyInfo", + _VenvSymlinkEntry = "VenvSymlinkEntry", + _VenvSymlinkKind = "VenvSymlinkKind", +) -PyInfo = _starlark_PyInfo if config.enable_pystar or BuiltinPyInfo == None else BuiltinPyInfo +PyInfo = _PyInfo + +# buildifier: disable=name-conventions +VenvSymlinkEntry = _VenvSymlinkEntry + +# buildifier: disable=name-conventions +VenvSymlinkKind = _VenvSymlinkKind diff --git a/python/py_library.bzl b/python/py_library.bzl index 8b8d46870b..c5cae241f6 100644 --- a/python/py_library.bzl +++ b/python/py_library.bzl @@ -14,13 +14,7 @@ """Public entry point for py_library.""" -load("@rules_python_internal//:rules_python_config.bzl", "config") -load("//python/private:py_library_macro.bzl", _starlark_py_library = "py_library") -load("//python/private:register_extension_info.bzl", "register_extension_info") -load("//python/private:util.bzl", "add_migration_tag") - -# buildifier: disable=native-python -_py_library_impl = _starlark_py_library if config.enable_pystar else native.py_library +load("//python/private:py_library_macro.bzl", _py_library = "py_library") def py_library(**attrs): """Creates an executable Python program. @@ -39,9 +33,4 @@ def py_library(**attrs): if attrs.get("srcs_version") in ("PY2", "PY2ONLY"): fail("Python 2 is no longer supported: https://github.com/bazel-contrib/rules_python/issues/886") - _py_library_impl(**add_migration_tag(attrs)) - -register_extension_info( - extension = py_library, - label_regex_for_dep = "{extension_name}", -) + _py_library(**attrs) diff --git a/python/py_runtime.bzl b/python/py_runtime.bzl index dad2965cf5..86f644a770 100644 --- a/python/py_runtime.bzl +++ b/python/py_runtime.bzl @@ -14,11 +14,7 @@ """Public entry point for py_runtime.""" -load("//python/private:py_runtime_macro.bzl", _starlark_py_runtime = "py_runtime") -load("//python/private:util.bzl", "IS_BAZEL_6_OR_HIGHER", "add_migration_tag") - -# buildifier: disable=native-python -_py_runtime_impl = _starlark_py_runtime if IS_BAZEL_6_OR_HIGHER else native.py_runtime +load("//python/private:py_runtime_macro.bzl", _py_runtime = "py_runtime") def py_runtime(**attrs): """Creates an executable Python program. @@ -39,4 +35,4 @@ def py_runtime(**attrs): if attrs.get("python_version") == "PY2": fail("Python 2 is no longer supported: see https://github.com/bazel-contrib/rules_python/issues/886") - _py_runtime_impl(**add_migration_tag(attrs)) + _py_runtime(**attrs) diff --git a/python/py_runtime_info.bzl b/python/py_runtime_info.bzl index 3a31c0f2f4..ac7b1b0fb6 100644 --- a/python/py_runtime_info.bzl +++ b/python/py_runtime_info.bzl @@ -14,8 +14,6 @@ """Public entry point for PyRuntimeInfo.""" -load("@rules_python_internal//:rules_python_config.bzl", "config") -load("//python/private:py_runtime_info.bzl", _starlark_PyRuntimeInfo = "PyRuntimeInfo") -load("//python/private:reexports.bzl", "BuiltinPyRuntimeInfo") +load("//python/private:py_runtime_info.bzl", _PyRuntimeInfo = "PyRuntimeInfo") -PyRuntimeInfo = _starlark_PyRuntimeInfo if config.enable_pystar else BuiltinPyRuntimeInfo +PyRuntimeInfo = _PyRuntimeInfo diff --git a/python/py_runtime_pair.bzl b/python/py_runtime_pair.bzl index 26d378fce2..f44c722d54 100644 --- a/python/py_runtime_pair.bzl +++ b/python/py_runtime_pair.bzl @@ -14,11 +14,7 @@ """Public entry point for py_runtime_pair.""" -load("@bazel_tools//tools/python:toolchain.bzl", _bazel_tools_impl = "py_runtime_pair") -load("//python/private:py_runtime_pair_macro.bzl", _starlark_impl = "py_runtime_pair") -load("//python/private:util.bzl", "IS_BAZEL_6_OR_HIGHER") - -_py_runtime_pair = _starlark_impl if IS_BAZEL_6_OR_HIGHER else _bazel_tools_impl +load("//python/private:py_runtime_pair_macro.bzl", _py_runtime_pair = "py_runtime_pair") # NOTE: This doc is copy/pasted from the builtin py_runtime_pair rule so our # doc generator gives useful API docs. diff --git a/python/py_test.bzl b/python/py_test.bzl index b5657730b7..2f31e0865e 100644 --- a/python/py_test.bzl +++ b/python/py_test.bzl @@ -14,13 +14,7 @@ """Public entry point for py_test.""" -load("@rules_python_internal//:rules_python_config.bzl", "config") -load("//python/private:py_test_macro.bzl", _starlark_py_test = "py_test") -load("//python/private:register_extension_info.bzl", "register_extension_info") -load("//python/private:util.bzl", "add_migration_tag") - -# buildifier: disable=native-python -_py_test_impl = _starlark_py_test if config.enable_pystar else native.py_test +load("//python/private:py_test_macro.bzl", _py_test = "py_test") def py_test(**attrs): """Creates an executable Python program. @@ -43,9 +37,4 @@ def py_test(**attrs): fail("Python 2 is no longer supported: https://github.com/bazel-contrib/rules_python/issues/886") # buildifier: disable=native-python - _py_test_impl(**add_migration_tag(attrs)) - -register_extension_info( - extension = py_test, - label_regex_for_dep = "{extension_name}", -) + _py_test(**attrs) diff --git a/python/runfiles/__init__.py b/python/runfiles/__init__.py index 3dc4141749..cdd7d9af00 100644 --- a/python/runfiles/__init__.py +++ b/python/runfiles/__init__.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .runfiles import * +from .runfiles import * # noqa: F403 diff --git a/python/runfiles/runfiles.py b/python/runfiles/runfiles.py index 58f59c5406..7236d4b851 100644 --- a/python/runfiles/runfiles.py +++ b/python/runfiles/runfiles.py @@ -16,19 +16,29 @@ See @rules_python//python/runfiles/README.md for usage instructions. -:::{versionadded} VERSION_NEXT_FEATURE +:::{versionadded} 1.7.0 Support for Bazel's `--incompatible_compact_repo_mapping_manifest` flag was added. This enables prefix-based repository mappings to reduce memory usage for large dependency graphs under bzlmod. ::: """ -import collections.abc + import inspect import os +import pathlib import posixpath import sys from collections import defaultdict -from typing import Dict, Optional, Tuple, Union +from typing import Dict, Generator, Optional, Tuple, Union + +if sys.version_info >= (3, 11): + from typing import Self +elif sys.version_info >= (3, 10): + from typing import TypeAlias + + Self: TypeAlias = "Path" # type: ignore +else: + from typing import Any as Self class _RepositoryMapping: @@ -137,7 +147,247 @@ def is_empty(self) -> bool: Returns: True if there are no mappings, False otherwise """ - return len(self._exact_mappings) == 0 and len(self._grouped_prefixed_mappings) == 0 + return ( + len(self._exact_mappings) == 0 and len(self._grouped_prefixed_mappings) == 0 + ) + + +class Path(pathlib.Path): + """A pathlib-like path object for runfiles. + + This class extends `pathlib.Path` and resolves paths + using the associated `Runfiles` instance when converted to a string. + """ + + # Mypy isn't smart enough to realize `self` in the methods + # refers to our Path class instead of pathlib.Path + _runfiles: Optional["Runfiles"] + _source_repo: Optional[str] + + # For Python < 3.12 compatibility when subclassing Path directly + _flavour = getattr(type(pathlib.Path()), "_flavour", None) + + def __new__( + cls, + *args: Union[str, os.PathLike], + runfiles: Optional["Runfiles"] = None, + source_repo: Optional[str] = None, + ) -> Self: + """Private constructor. Use Runfiles.root() to create instances.""" + obj = super().__new__(cls, *args) + # Type checkers might complain about adding attributes to Path, + # but this is standard for pathlib subclasses. + obj._runfiles = runfiles # type: ignore + obj._source_repo = source_repo # type: ignore + return obj + + def __init__( + self, + *args: Union[str, os.PathLike], + runfiles: Optional["Runfiles"] = None, + source_repo: Optional[str] = None, + ) -> None: + # In Python 3.12+, pathlib was refactored and Path.__init__ now accepts + # *args. Prior to 3.12, Path did not define __init__, so + # super().__init__(*args) would fall through to object.__init__, which + # raises a TypeError because it takes no arguments. + if sys.version_info >= (3, 12): + super().__init__(*args) + else: + super().__init__() + + # We override resolve() and absolute() to ensure that in Python < 3.12, + # where pathlib internally uses object.__new__ instead of our custom + # __new__ or with_segments(), the runfiles state is preserved. We delegate + # to self._as_path() because super().resolve() creates intermediate objects + # that would otherwise crash during internal stat() calls. + # override + def resolve(self, strict: bool = False) -> Self: + return type(self)( + self._as_path().resolve(strict=strict), + runfiles=self._runfiles, + source_repo=self._source_repo, + ) + + # override + def absolute(self) -> Self: + return type(self)( + self._as_path().absolute(), + runfiles=self._runfiles, + source_repo=self._source_repo, + ) + + # override + def with_segments(self, *pathsegments: Union[str, os.PathLike]) -> Self: + """Used by Python 3.12+ pathlib to create new path objects.""" + return type(self)( + *pathsegments, + runfiles=self._runfiles, + source_repo=self._source_repo, + ) + + # For Python < 3.12 + # override + def _make_child(self, args: Tuple[str, ...]) -> Self: + obj = super()._make_child(args) # type: ignore + obj._runfiles = self._runfiles # type: ignore + obj._source_repo = self._source_repo # type: ignore + return obj + + # override + @property + def parents(self) -> Tuple[Self, ...]: + return tuple( + type(self)( + p, + runfiles=self._runfiles, + source_repo=self._source_repo, + ) + for p in super().parents + ) + + # override + @property + def parent(self) -> Self: + return type(self)( + super().parent, + runfiles=self._runfiles, + source_repo=self._source_repo, + ) + + @property + def runfile_path(self) -> str: + """Returns the runfiles-root relative path.""" + path_posix = super().__str__().replace("\\", "/") + if path_posix == ".": + return "" + return path_posix + + # override + def with_name(self, name: str) -> Self: + return type(self)( + super().with_name(name), + runfiles=self._runfiles, + source_repo=self._source_repo, + ) + + # override + def with_suffix(self, suffix: str) -> Self: + return type(self)( + super().with_suffix(suffix), + runfiles=self._runfiles, + source_repo=self._source_repo, + ) + + def _as_path(self) -> pathlib.Path: + return pathlib.Path(str(self)) + + # override + def stat(self, *, follow_symlinks: bool = True) -> os.stat_result: + return self._as_path().stat(follow_symlinks=follow_symlinks) + + # override + def lstat(self) -> os.stat_result: + return self._as_path().lstat() + + # override + def exists(self) -> bool: + return self._as_path().exists() + + # override + def is_dir(self) -> bool: + return self._as_path().is_dir() + + # override + def is_file(self) -> bool: + return self._as_path().is_file() + + # override + def is_symlink(self) -> bool: + return self._as_path().is_symlink() + + # override + def is_block_device(self) -> bool: + return self._as_path().is_block_device() + + # override + def is_char_device(self) -> bool: + return self._as_path().is_char_device() + + # override + def is_fifo(self) -> bool: + return self._as_path().is_fifo() + + # override + def is_socket(self) -> bool: + return self._as_path().is_socket() + + # override + def open( + self, + mode: str = "r", + buffering: int = -1, + encoding: Optional[str] = None, + errors: Optional[str] = None, + newline: Optional[str] = None, + ): + return self._as_path().open( + mode=mode, + buffering=buffering, + encoding=encoding, + errors=errors, + newline=newline, + ) + + # override + def read_bytes(self) -> bytes: + return self._as_path().read_bytes() + + # override + def read_text( + self, encoding: Optional[str] = None, errors: Optional[str] = None + ) -> str: + return self._as_path().read_text(encoding=encoding, errors=errors) + + # override + def iterdir(self) -> Generator[Self, None, None]: + resolved = self._as_path() + for p in resolved.iterdir(): + yield self / p.name + + # override + def glob(self, pattern: str) -> Generator[Self, None, None]: + resolved = self._as_path() + for p in resolved.glob(pattern): + yield self / p.relative_to(resolved) + + # override + def rglob(self, pattern: str) -> Generator[Self, None, None]: + resolved = self._as_path() + for p in resolved.rglob(pattern): + yield self / p.relative_to(resolved) + + def __repr__(self) -> str: + return "runfiles.Path({!r})".format(self.runfile_path) + + def __str__(self) -> str: + path_posix = super().__str__().replace("\\", "/") + if not path_posix or path_posix == ".": + # pylint: disable=protected-access + return self._runfiles._python_runfiles_root # type: ignore + resolved = self._runfiles.Rlocation(path_posix, source_repo=self._source_repo) # type: ignore + if resolved is not None: + return resolved + + # pylint: disable=protected-access + return posixpath.join(self._runfiles._python_runfiles_root, path_posix) # type: ignore + + def __fspath__(self) -> str: + return str(self) + + def runfiles_root(self) -> Self: + """Returns a Path object representing the runfiles root.""" + return self._runfiles.root(source_repo=self._source_repo) # type: ignore class _ManifestBased: @@ -229,6 +479,9 @@ def RlocationChecked(self, path: str) -> str: # runfiles strategy on those platforms. return posixpath.join(self._runfiles_root, path) + def _GetRunfilesDir(self) -> str: + return self._runfiles_root + def EnvVars(self) -> Dict[str, str]: return { "RUNFILES_DIR": self._runfiles_root, @@ -246,11 +499,21 @@ class Runfiles: def __init__(self, strategy: Union[_ManifestBased, _DirectoryBased]) -> None: self._strategy = strategy - self._python_runfiles_root = _FindPythonRunfilesRoot() + self._python_runfiles_root = strategy._GetRunfilesDir() self._repo_mapping = _RepositoryMapping.create_from_file( strategy.RlocationChecked("_repo_mapping") ) + def root(self, source_repo: Optional[str] = None) -> Path: + """Returns a Path object representing the runfiles root. + + The repository mapping used by the returned Path object is that of the + caller of this method. + """ + if source_repo is None and not self._repo_mapping.is_empty(): + source_repo = self.CurrentRepository(frame=2) + return Path(runfiles=self, source_repo=source_repo) + def Rlocation(self, path: str, source_repo: Optional[str] = None) -> Optional[str]: """Returns the runtime path of a runfile. @@ -316,15 +579,13 @@ def Rlocation(self, path: str, source_repo: Optional[str] = None) -> Optional[st # which also should not be mapped. return self._strategy.RlocationChecked(path) - assert ( - source_repo is not None - ), "BUG: if the `source_repo` is None, we should never go past the `if` statement above" + assert source_repo is not None, ( + "BUG: if the `source_repo` is None, we should never go past the `if` statement above" + ) # Look up the target repository using the repository mapping if target_canonical is not None: - return self._strategy.RlocationChecked( - target_canonical + "/" + remainder - ) + return self._strategy.RlocationChecked(target_canonical + "/" + remainder) # No mapping found - assume target_repo is already canonical or # we're not using Bzlmod @@ -386,13 +647,16 @@ def CurrentRepository(self, frame: int = 1) -> str: # located in the main repository. # With Python 3.11 and higher, the Python launcher sets # PYTHONSAFEPATH, which prevents this behavior. + # On Windows, the current toolchain being used has a buggy zip file + # bootstrap, which leaves RUNFILES_DIR pointing at the first stage + # path and not the module path. In this case too, assume that the + # module is located in the main repository. # TODO: This doesn't cover the case of a script being run from an # external repository, which could be heuristically detected # by parsing the script's path. - if ( - sys.version_info.minor <= 10 - and sys.path[0] != self._python_runfiles_root - ): + if (sys.version_info.minor <= 10 or sys.platform == "win32") and sys.path[ + 0 + ] != self._python_runfiles_root: return "" raise ValueError( "{} does not lie under the runfiles root {}".format( @@ -469,18 +733,6 @@ def Create(env: Optional[Dict[str, str]] = None) -> Optional["Runfiles"]: _Runfiles = Runfiles -def _FindPythonRunfilesRoot() -> str: - """Finds the root of the Python runfiles tree.""" - root = __file__ - # Walk up our own runfiles path to the root of the runfiles tree from which - # the current file is being run. This path coincides with what the Bazel - # Python stub sets up as sys.path[0]. Since that entry can be changed at - # runtime, we rederive it here. - for _ in range("rules_python/python/runfiles/runfiles.py".count("/") + 1): - root = os.path.dirname(root) - return root - - def CreateManifestBased(manifest_path: str) -> Runfiles: return Runfiles.CreateManifestBased(manifest_path) diff --git a/python/uv/BUILD.bazel b/python/uv/BUILD.bazel index 7ce6ce0523..93aefbfbd7 100644 --- a/python/uv/BUILD.bazel +++ b/python/uv/BUILD.bazel @@ -45,33 +45,25 @@ current_toolchain( ) bzl_library( - name = "lock_bzl", + name = "lock", srcs = ["lock.bzl"], - # EXPERIMENTAL: Visibility is restricted to allow for changes. - visibility = ["//:__subpackages__"], - deps = ["//python/uv/private:lock_bzl"], + deps = ["//python/uv/private:lock"], ) bzl_library( - name = "uv_bzl", + name = "uv", srcs = ["uv.bzl"], - # EXPERIMENTAL: Visibility is restricted to allow for changes. - visibility = ["//:__subpackages__"], - deps = ["//python/uv/private:uv_bzl"], + deps = ["//python/uv/private:uv"], ) bzl_library( - name = "uv_toolchain_bzl", + name = "uv_toolchain", srcs = ["uv_toolchain.bzl"], - # EXPERIMENTAL: Visibility is restricted to allow for changes. - visibility = ["//:__subpackages__"], - deps = ["//python/uv/private:uv_toolchain_bzl"], + deps = ["//python/uv/private:uv_toolchain"], ) bzl_library( - name = "uv_toolchain_info_bzl", + name = "uv_toolchain_info", srcs = ["uv_toolchain_info.bzl"], - # EXPERIMENTAL: Visibility is restricted to allow for changes. - visibility = ["//:__subpackages__"], - deps = ["//python/uv/private:uv_toolchain_info_bzl"], + deps = ["//python/uv/private:uv_toolchain_info"], ) diff --git a/python/uv/lock.bzl b/python/uv/lock.bzl index 82b00bc2d2..7fd50082ea 100644 --- a/python/uv/lock.bzl +++ b/python/uv/lock.bzl @@ -21,8 +21,13 @@ Differences with the legacy {obj}`compile_pip_requirements` rule: - 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 +Note, this does not provide a test target like {obj}`compile_pip_requirements` does. +The `uv pip compile` command is not hermetic and thus a test based on it would most likely be flaky: +- It may require auth injected into it, so most likely it requires a local tag added so that the bazel action runs without sandboxing. +- It requires network access. + +Given those points, a test target should be an explicit and properly documented target and not a hidden implicit 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 @@ -40,6 +45,28 @@ native_test( ) ``` +### `[tool.uv]` settings support + +When a `pyproject.toml` file is included in {attr}`lock.srcs`, the +`--project` flag is automatically passed to `uv pip compile` using the +directory of the shortest-path `pyproject.toml`. This causes `uv` to +read `[tool.uv]` settings such as `no-build-isolation`, +`exclude-dependencies`, and `[tool.uv.workspace]` from that file. + +If the auto-detection doesn't select the right project (e.g. in complex +workspace layouts), use the `project` parameter to override it: + +```starlark +lock( + name = "requirements", + srcs = [ + "pyproject.toml", + "requirements.in", + ], + project = "subproject", +) +``` + EXPERIMENTAL: This is experimental and may be changed without notice. """ diff --git a/python/uv/private/BUILD.bazel b/python/uv/private/BUILD.bazel index 3e4d6c7baa..ce0ce0ec81 100644 --- a/python/uv/private/BUILD.bazel +++ b/python/uv/private/BUILD.bazel @@ -15,13 +15,8 @@ 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"], -) +# public only because this is used from a macro to template +_NOT_REALLY_PUBLIC = ["//visibility:public"] filegroup( name = "distribution", @@ -29,80 +24,117 @@ filegroup( visibility = ["//python/uv:__pkg__"], ) +filegroup( + name = "lock_copier_template", + srcs = ["template/lock_copier.py"], + target_compatible_with = [] if BZLMOD_ENABLED else ["@platforms//:incompatible"], + visibility = _NOT_REALLY_PUBLIC, +) + +filegroup( + name = "uv_lock_template", + srcs = select({ + "@platforms//os:windows": ["template/uv_lock.bat"], + "//conditions:default": ["template/uv_lock.sh"], + }), + target_compatible_with = [] if BZLMOD_ENABLED else ["@platforms//:incompatible"], + visibility = _NOT_REALLY_PUBLIC, +) + +filegroup( + name = "uv_pip_compile_template", + srcs = select({ + "@platforms//os:windows": ["template/uv_pip_compile.bat"], + "//conditions:default": ["template/uv_pip_compile.sh"], + }), + target_compatible_with = [] if BZLMOD_ENABLED else ["@platforms//:incompatible"], + visibility = _NOT_REALLY_PUBLIC, +) + bzl_library( - name = "current_toolchain_bzl", + name = "current_toolchain", srcs = ["current_toolchain.bzl"], visibility = ["//python/uv:__subpackages__"], + deps = [":toolchain_types"], ) +# keep bzl_library( - name = "lock_bzl", + name = "lock", srcs = ["lock.bzl"], - visibility = ["//python/uv:__subpackages__"], + visibility = [ + "//python/uv:__subpackages__", + "//tools/private:__subpackages__", + ], deps = [ - ":toolchain_types_bzl", - "//python:py_binary_bzl", - "//python/private:bzlmod_enabled_bzl", - "//python/private:common_labels_bzl", - "//python/private:toolchain_types_bzl", + ":toolchain_types", + "//python:py_binary", + "//python/private:bzlmod_enabled", + "//python/private:common_labels", + "//python/private:toolchain_types", "@bazel_skylib//lib:shell", ], ) bzl_library( - name = "toolchain_types_bzl", - srcs = ["toolchain_types.bzl"], + name = "toolchains_hub", + srcs = ["toolchains_hub.bzl"], visibility = ["//python/uv:__subpackages__"], + deps = [":toolchain_types"], ) bzl_library( - name = "uv_bzl", + name = "uv", srcs = ["uv.bzl"], visibility = ["//python/uv:__subpackages__"], deps = [ - ":toolchain_types_bzl", - ":uv_repository_bzl", - ":uv_toolchains_repo_bzl", - "//python/private:auth_bzl", - "//python/private:common_labels_bzl", + ":toolchain_types", + ":uv_repository", + ":uv_toolchains_repo", + "//python/private:auth", + "//python/private:common_labels", ], ) bzl_library( - name = "uv_repository_bzl", + name = "uv_repository", srcs = ["uv_repository.bzl"], visibility = ["//python/uv:__subpackages__"], - deps = ["//python/private:auth_bzl"], + deps = ["//python/private:auth"], ) bzl_library( - name = "uv_toolchain_bzl", + name = "uv_toolchain", srcs = ["uv_toolchain.bzl"], visibility = ["//python/uv:__subpackages__"], - deps = [":uv_toolchain_info_bzl"], + deps = [":uv_toolchain_info"], ) bzl_library( - name = "uv_toolchain_info_bzl", - srcs = ["uv_toolchain_info.bzl"], + name = "uv_toolchains_repo", + srcs = ["uv_toolchains_repo.bzl"], visibility = ["//python/uv:__subpackages__"], + deps = ["//python/private:text_util"], ) +# keep bzl_library( - name = "uv_toolchains_repo_bzl", - srcs = ["uv_toolchains_repo.bzl"], - visibility = ["//python/uv:__subpackages__"], - deps = [ - "//python/private:text_util_bzl", + name = "uv_lock_to_requirements", + srcs = ["uv_lock_to_requirements.bzl"], + visibility = [ + "//python/private:__subpackages__", + "//python/uv:__subpackages__", ], ) -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"], +bzl_library( + name = "toolchain_types", + srcs = ["toolchain_types.bzl"], + visibility = ["//python/uv:__subpackages__"], +) + +bzl_library( + name = "uv_toolchain_info", + srcs = ["uv_toolchain_info.bzl"], + visibility = ["//python/uv:__subpackages__"], ) diff --git a/python/uv/private/lock.bat b/python/uv/private/lock.bat deleted file mode 100755 index 3954c10347..0000000000 --- a/python/uv/private/lock.bat +++ /dev/null @@ -1,7 +0,0 @@ -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 281a0decc0..695591e924 100644 --- a/python/uv/private/lock.bzl +++ b/python/uv/private/lock.bzl @@ -30,6 +30,7 @@ _RunLockInfo = provider( "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.", + "template": "The template file for writing a script.", }, ) @@ -70,9 +71,11 @@ def _args(ctx): add_all = _add_all, ) -def _lock_impl(ctx): - srcs = ctx.files.srcs +def _common_lock(ctx, locker): fname = "{}.out".format(ctx.label.name) + + # TODO @aignas 2026-06-21: do not append python_version for uv.lock as it should work for all + # python versions python_version = ctx.attr.python_version if python_version: fname = "{}.{}.out".format( @@ -85,64 +88,142 @@ def _lock_impl(ctx): uv = toolchain_info.uv_toolchain_info.uv[DefaultInfo].files_to_run.executable args = _args(ctx) + args.add(uv) + + # The output params are: + # * srcs are the srcs for the locking command action inputs tracking + # * output_filename if set is to ensure that we can do special prep for uv.lock versus + # requirements.txt + # * mnemonic is for the action mnemonic + # * progress_message is the same + srcs, output_filename, mnemonic, progress_message = locker(args, output) + 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") + + project = None + if ctx.attr.project: + project = ctx.attr.project + else: + # Autodetect the project based on the `pyproject.toml` location - it will be the first src that + # we see that is named "pyproject.toml" + for src in srcs: + if src.basename == "pyproject.toml": + if project == None: + project = src.dirname + elif len(project) > len(src.dirname): + # select the shortest match + project = src.dirname + + if project == None: + project = ctx.label.package + + if project: + args.add_all([project], before_each = "--project") + 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 + python_files = runtime.files or depset() 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") + args.add("--no-progress") + args.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, - ), - ), + src_out = ctx.files.existing_output[0].path + elif output_filename: + # special case - the output filename has to be in the source tree and it has to have a + # special name, we use the project folder to determine this. + + if not project: + fail("Cannot lock this if the project dir is unset or cannot be infered") + + src_out = "{project}/{out_filename}".format( + project = project, + out_filename = output_filename, ) else: - command = '"$@"' + src_out = "" + + is_windows = ctx.attr.is_windows + if is_windows: + path_sep = "\\" + ext = ".bat" + else: + path_sep = "/" + ext = "" - srcs = srcs + ctx.files.build_constraints + ctx.files.constraints + output_path = output.path.replace("/", path_sep) if is_windows else output.path + src_out_path = src_out.replace("/", path_sep) if is_windows else src_out + + # On Windows, all args must be embedded in the .bat script because + # arguments are not passed on the command line. + if is_windows: + args_parts = [] + for i, arg in enumerate(args.run_info): + if hasattr(arg, "path"): + arg = arg.path + + # Only use backslashes for the executable itself (first arg) + # to ensure CMD can run it, but keep forward slashes for arguments + # so that uv writes consistent paths in comments. + if i == 0: + a = arg.replace("/", "\\") + else: + a = arg + a = a.replace('"', '""') + args_parts.append('"' + a + '"') + + # uv pip compile adds --output-file to run_shell (not run_info). + # For the lock case, output_filename is "uv.lock" and uv lock + # writes to the project directory without --output-file. + if not output_filename: + args_parts.append('"--output-file"') + args_parts.append('"' + output_path + '"') + windows_args = " ".join(args_parts) + else: + windows_args = " ".join([]) + + script = ctx.actions.declare_file(ctx.label.name + "_lock" + ext) + ctx.actions.expand_template( + template = ctx.files._template[0], + substitutions = { + '"{{args}}"': windows_args, + "{{out}}": output_path, + "{{src_out}}": src_out_path, + }, + output = script, + is_executable = True, + ) - ctx.actions.run_shell( - command = command, + ctx.actions.run( + executable = script, + mnemonic = mnemonic, 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, ], - progress_message = "Creating a requirements.txt with uv: %{label}", + + # 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 = progress_message, env = ctx.attr.env, ) @@ -155,9 +236,36 @@ def _lock_impl(ctx): srcs + [uv], transitive = [python_files], ), + template = ctx.files._template[0], ), ] +def _pip_compile_impl(ctx): + def _setup_args(args, output): + args.add_all(["pip", "compile"]) + 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.run_shell.add("--output-file", output) + mnemonic = "PyRequirementsLockUv" + progress_message = "Creating a requirements.txt with uv: %{label}" + + args.add_all(ctx.files.srcs) + srcs = ctx.files.srcs + ctx.files.build_constraints + ctx.files.constraints + + return srcs, None, mnemonic, progress_message + + return _common_lock(ctx, _setup_args) + def _transition_impl(input_settings, attr): settings = { labels.PYTHON_VERSION: input_settings[labels.PYTHON_VERSION], @@ -172,16 +280,60 @@ _python_version_transition = transition( outputs = [labels.PYTHON_VERSION], ) -_lock = rule( - implementation = _lock_impl, +_common_attrs = { + "args": attr.string_list( + 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. +""", + ), + "is_windows": attr.bool(mandatory = True), + "output": attr.string( + doc = "Public, see the docs in the macro.", + mandatory = True, + ), + "project": attr.string( + doc = """\ +Overrides the `--project` directory passed to `uv pip compile`. +If not set, the project directory is auto-detected: when +`pyproject.toml` files are in {obj}`lock.srcs`, the one with the +shortest directory path is selected. This makes `uv` read +`[tool.uv]` settings (e.g. `no-build-isolation`, +`exclude-dependencies`) from that `pyproject.toml`. +""", + ), + "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.", + ), + "_allowlist_function_transition": attr.label( + default = "@bazel_tools//tools/allowlists/function_transition_allowlist", + ), +} + +_pip_compile = rule( + implementation = _pip_compile_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. + +:::{versionchanged} 2.1.0 +Added the {attr}`project` to configure the project setting if autodetection fails. +::: """, 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.", @@ -190,33 +342,10 @@ to use RBE) and also prepares information for a `bazel run` executable rule. 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, @@ -227,8 +356,46 @@ modifications and the locking is not done from scratch. The string to input for the 'uv pip compile'. """, ), - "_allowlist_function_transition": attr.label( - default = "@bazel_tools//tools/allowlists/function_transition_allowlist", + "_template": attr.label( + default = "//python/uv/private:uv_pip_compile_template", + doc = """\ +The template to be used for 'uv pip compile'. This is either .bat or bash +script depending on what the target platform is executed on. +""", + ), + } | _common_attrs, + toolchains = [ + EXEC_TOOLS_TOOLCHAIN_TYPE, + UV_TOOLCHAIN_TYPE, + ], + cfg = _python_version_transition, +) + +def _lock_impl(ctx): + def _setup_args(args, _output): + args.add("lock") + mnemonic = "PyUvLock" + progress_message = "Creating a uv.lock with uv: %{label}" + + return ctx.files.srcs, "uv.lock", mnemonic, progress_message + + return _common_lock(ctx, _setup_args) + +_lock = rule( + implementation = _lock_impl, + doc = """\ +The lock rule that does the locking in a build action and also prepares information for a `bazel +run` executable rule. + +:::{versionadded} 2.2.0 +::: +""", + attrs = _common_attrs | { + "_template": attr.label( + default = "//python/uv/private:uv_lock_template", + doc = """\ +The template to be used for 'uv lock'. Used when output ends with '.lock'. +""", ), }, toolchains = [ @@ -238,10 +405,10 @@ The string to input for the 'uv pip compile'. cfg = _python_version_transition, ) -def _lock_run_impl(ctx): +def _run_impl(ctx): if ctx.attr.is_windows: path_sep = "\\" - ext = ".exe" + ext = ".bat" else: path_sep = "/" ext = "" @@ -250,12 +417,18 @@ 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) ctx.actions.expand_template( - template = ctx.files._template[0], + template = info.template, substitutions = { '"{{args}}"': " ".join([_maybe_path(arg) for arg in info.args]), "{{src_out}}": "{}/{}".format(ctx.label.package, ctx.attr.output).replace( @@ -277,8 +450,8 @@ def _lock_run_impl(ctx): ), ] -_lock_run = rule( - implementation = _lock_run_impl, +_run_locker = rule( + implementation = _run_impl, doc = """\ """, attrs = { @@ -291,13 +464,6 @@ _lock_run = rule( "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. """, ), }, @@ -318,8 +484,10 @@ def _maybe_file(path): path: {type}`str` the file name. """ for p in native.glob([path], allow_empty = True): - if path == p: - return p + if path != p: + continue + + return p return None @@ -332,7 +500,7 @@ def _expand_template_impl(ctx): dst = "{}/{}".format(pkg, ctx.attr.output) if pkg else ctx.attr.output ctx.actions.expand_template( - template = ctx.files._template[0], + template = ctx.files._lock_copier_template[0], substitutions = { "{{dst}}": dst, "{{src}}": "{}".format(ctx.files.src[0].short_path), @@ -348,8 +516,8 @@ _expand_template = rule( "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", + "_lock_copier_template": attr.label( + default = "//python/uv/private:lock_copier_template", allow_single_file = True, ), }, @@ -367,6 +535,7 @@ def lock( env = None, generate_hashes = True, python_version = None, + project = None, strip_extras = False, **kwargs): """Pin the requirements based on the src files. @@ -406,13 +575,23 @@ def lock( 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`. + requirements. Only meaningful for `requirements.txt` style output. + 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`. + project: {type}`str | None` overrides the `--project` directory + passed to `uv pip compile`. By default the project directory + is auto-detected: when {obj}`lock.srcs` contains + `pyproject.toml` files, the one with the shortest directory + path is selected. This causes `uv` to read `[tool.uv]` + settings such as `no-build-isolation` and + `exclude-dependencies` from that `pyproject.toml`. If no + `pyproject.toml` is in `srcs` and no `project` is given, the + Bazel package directory is used as fallback. + {versionadded} 2.1.0 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. @@ -430,38 +609,52 @@ def lock( if not BZLMOD_ENABLED: kwargs["target_compatible_with"] = ["@platforms//:incompatible"] - _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, - strip_extras = strip_extras, - update_target = update_target, - output = out, - tags = [ + uv_kwargs = { + "is_windows": select({ + "@platforms//os:windows": True, + "//conditions:default": False, + }), + "output": out, + } | kwargs + + lock_target_kwargs = { + "args": args, + "env": env, + "existing_output": maybe_out, + "project": project, + "python_version": python_version, + "srcs": srcs, + "tags": [ "no-cache", "requires-network", ] + tags, - **kwargs - ) + } | uv_kwargs + + # NOTE @aignas 2026-06-20: if the user passes these args the command will fail + # with an error message instead of silently ignoring the args + if build_constraints: + lock_target_kwargs["build_constraints"] = build_constraints + if constraints: + lock_target_kwargs["constraints"] = constraints + + if out.endswith(".lock"): + _lock(name = name, **lock_target_kwargs) + else: + _pip_compile( + name = name, + generate_hashes = generate_hashes, + strip_extras = strip_extras, + update_target = update_target, + **lock_target_kwargs + ) # A target for updating the in-tree version directly by skipping the in-action - # uv pip compile. - _lock_run( + # uv pip compile or uv lock, depending what is defined for the locker_target. + _run_locker( name = locker_target, lock = name, - output = out, - is_windows = select({ - "@platforms//os:windows": True, - "//conditions:default": False, - }), tags = tags, - **kwargs + **uv_kwargs ) # FIXME @aignas 2025-03-20: is it possible to extend `py_binary` so that the diff --git a/python/uv/private/lock.sh b/python/uv/private/lock.sh deleted file mode 100755 index ffb19b2bea..0000000000 --- a/python/uv/private/lock.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env 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/template/lock_copier.py similarity index 95% rename from python/uv/private/lock_copier.py rename to python/uv/private/template/lock_copier.py index bcc64c1661..8756fc4de6 100644 --- a/python/uv/private/lock_copier.py +++ b/python/uv/private/template/lock_copier.py @@ -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 /{src} /{dst}") + print(f"cp /{src.as_posix()} /{dst}") build_workspace = Path(environ["BUILD_WORKSPACE_DIRECTORY"]) dst_real_path = build_workspace / dst diff --git a/python/uv/private/template/uv_lock.bat b/python/uv/private/template/uv_lock.bat new file mode 100755 index 0000000000..d4f4558c53 --- /dev/null +++ b/python/uv/private/template/uv_lock.bat @@ -0,0 +1,21 @@ +@echo off +if not defined BUILD_WORKSPACE_DIRECTORY goto :not_in_workspace +"{{args}}" %* +exit /b %ERRORLEVEL% + +:not_in_workspace + +if not exist "{{src_out}}" goto :no_src_out +copy /y "{{src_out}}" "{{out}}" +del /f "{{src_out}}" +copy /y "{{out}}" "{{src_out}}" +"{{args}}" %* +set "exit_code=%ERRORLEVEL%" +copy /y "{{src_out}}" "{{out}}" +exit /b %exit_code% + +:no_src_out +"{{args}}" %* +set "exit_code=%ERRORLEVEL%" +copy /y "{{src_out}}" "{{out}}" +exit /b %exit_code% \ No newline at end of file diff --git a/python/uv/private/template/uv_lock.sh b/python/uv/private/template/uv_lock.sh new file mode 100755 index 0000000000..a6dd6d3c39 --- /dev/null +++ b/python/uv/private/template/uv_lock.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ -n "${BUILD_WORKSPACE_DIRECTORY:-}" ]]; then + exec "{{args}}" "$@" +fi + +# Build action mode +# +# If the uv.lock exists, remove because the existing uv.lock file is read-only, then symlink so +# that we can reuse the existing contents and not do a full relock all the time. If +# nothing exists, just symlink. +# +# On Windows we do it with file copies: +# 1. If the file exists: +# 1. Copy the current file to out. +# 2. Rm the existing file +# 3. Copy the contents back +# 4. Run uv +# 5. Copy the contents to out. +# 1. If the current uv.lock does not exist yet +# 1. Run uv +# 2. Copy the contents to out. +readonly out="{{out}}" +if [[ -f "{{src_out}}" ]]; then + cp "{{src_out}}" "$out" + rm "{{src_out}}" + ln -s "$(pwd)"/"$out" "{{src_out}}" +else + ln -s "$(pwd)"/"$out" "{{src_out}}" +fi +exec "$@" diff --git a/python/uv/private/template/uv_pip_compile.bat b/python/uv/private/template/uv_pip_compile.bat new file mode 100755 index 0000000000..2ea1b5a44a --- /dev/null +++ b/python/uv/private/template/uv_pip_compile.bat @@ -0,0 +1,8 @@ +@echo off +if not defined BUILD_WORKSPACE_DIRECTORY goto :else +set "out=%BUILD_WORKSPACE_DIRECTORY%\{{src_out}}" +"{{args}}" --output-file "%out%" %* +exit /b %ERRORLEVEL% + +:else +"{{args}}" %* diff --git a/python/uv/private/template/uv_pip_compile.sh b/python/uv/private/template/uv_pip_compile.sh new file mode 100755 index 0000000000..ba379ed0f9 --- /dev/null +++ b/python/uv/private/template/uv_pip_compile.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ -n "${BUILD_WORKSPACE_DIRECTORY:-}" ]]; then + readonly out="${BUILD_WORKSPACE_DIRECTORY}/{{src_out}}" + exec "{{args}}" --output-file "$out" "$@" +fi + +# Build action mode: seed the output with the source file, then run +# the full command (which includes --output-file from the action args). +readonly out="{{out}}" +if [[ -f "{{src_out}}" ]]; then + cp "{{src_out}}" "$out" +fi +exec "$@" diff --git a/python/uv/private/uv.bzl b/python/uv/private/uv.bzl index fe0911e3ea..969b6f672d 100644 --- a/python/uv/private/uv.bzl +++ b/python/uv/private/uv.bzl @@ -380,6 +380,10 @@ def _overlap(first_collection, second_collection): return False +# See https://github.com/astral-sh/setup-uv/pull/809. +GITHUB_RELEASES_PREFIX = "https://github.com/astral-sh/uv/releases/download/" +ASTRAL_MIRROR_PREFIX = "https://releases.astral.sh/github/uv/releases/download/" + def _get_tool_urls_from_dist_manifest(module_ctx, *, base_url, manifest_filename, platforms, get_auth = get_auth, **auth_attrs): """Download the results about remote tool sources. @@ -420,6 +424,17 @@ def _get_tool_urls_from_dist_manifest(module_ctx, *, base_url, manifest_filename "uv-aarch64-apple-darwin.tar.gz.sha256" "... ] + hosting + order + 0 "simple" + 1 "github" + github + artifact_base_url "https://github.com" + artifact_download_path "/astral-sh/uv/releases/download/0.11.2" + owner "astral-sh" + repo "uv" + simple + download_url "https://releases.astral.sh/github/uv/releases/download/0.11.2" artifacts uv-aarch64-apple-darwin.tar.gz name "uv-aarch64-apple-darwin.tar.gz" @@ -441,6 +456,8 @@ def _get_tool_urls_from_dist_manifest(module_ctx, *, base_url, manifest_filename } ] checksum "uv-aarch64-apple-darwin.tar.gz.sha256" + checksums + sha256 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" uv-aarch64-apple-darwin.tar.gz.sha256 name "uv-aarch64-apple-darwin.tar.gz.sha256" kind "checksum" @@ -460,21 +477,55 @@ def _get_tool_urls_from_dist_manifest(module_ctx, *, base_url, manifest_filename fail(result) dist_manifest = json.decode(module_ctx.read(dist_manifest)) + # Use the simple download_url from the manifest, when available. + dist_base_url = ( + dist_manifest + .get("releases", [{}])[0] + .get("hosting", {}) + .get("simple", {}) + .get("download_url", base_url) + ) + + # For official releases, add the astral mirror to improve availability. + # See https://github.com/astral-sh/setup-uv/pull/809. + if dist_base_url.startswith(GITHUB_RELEASES_PREFIX): + astral_base_url = ASTRAL_MIRROR_PREFIX + dist_base_url[len(GITHUB_RELEASES_PREFIX):] + base_urls = [ + astral_base_url, + dist_base_url, + ] + else: + base_urls = [dist_base_url] + artifacts = dist_manifest["artifacts"] tool_sources = {} downloads = {} for fname, artifact in artifacts.items(): if artifact.get("kind") != "executable-zip": continue + target_triples = artifact["target_triples"] + if not _overlap(target_triples, platforms): + # We are not interested in this platform, so skip. + continue - checksum = artifacts[artifact["checksum"]] - if not _overlap(checksum["target_triples"], platforms): - # we are not interested in this platform, so skip + # Releases of uv >= 0.11.0 have the sha256 directly inline. + sha256 = artifact.get("checksums", {}).get("sha256", "") + if len(sha256) > 0: + for platform in target_triples: + tool_sources[platform] = struct( + urls = [ + "{}/{}".format(base, fname) + for base in base_urls + ], + sha256 = sha256, + ) continue + # For uv < 0.11.0, we'll need to fetch the individual sha256 files. + checksum = artifacts[artifact["checksum"]] checksum_fname = checksum["name"] checksum_path = module_ctx.path(checksum_fname) - urls = ["{}/{}".format(base_url, checksum_fname)] + urls = ["{}/{}".format(dist_base_url, checksum_fname)] downloads[checksum_path] = struct( download = module_ctx.download( url = urls, @@ -483,7 +534,7 @@ def _get_tool_urls_from_dist_manifest(module_ctx, *, base_url, manifest_filename auth = get_auth(module_ctx, urls, ctx_attr = auth_attr), ), archive_fname = fname, - platforms = checksum["target_triples"], + platforms = target_triples, ) for checksum_path, download in downloads.items(): @@ -503,7 +554,10 @@ def _get_tool_urls_from_dist_manifest(module_ctx, *, base_url, manifest_filename for platform in download.platforms: tool_sources[platform] = struct( - urls = ["{}/{}".format(base_url, archive_fname)], + urls = [ + "{}/{}".format(base, archive_fname) + for base in base_urls + ], sha256 = sha256, ) diff --git a/python/uv/private/uv_lock_to_requirements.bzl b/python/uv/private/uv_lock_to_requirements.bzl new file mode 100644 index 0000000000..73d912bbfd --- /dev/null +++ b/python/uv/private/uv_lock_to_requirements.bzl @@ -0,0 +1,139 @@ +"""Convert a parsed uv.lock to requirements.txt format.""" + +def uv_lock_extras_map(uv_lock): + """Compute extras for each package from uv.lock data. + + Args: + uv_lock: a decoded JSON struct from a uv.lock file. + + Returns: + A dict of {package_name: [extra1, extra2, ...]} for packages with extras. + """ + extras_map = {} + for pkg in uv_lock.get("package", []): + pkg_name = pkg.get("name", "") + + for extra in pkg.get("provides-extras", pkg.get("extras", [])): + _add_extras(extras_map, pkg_name, [extra]) + + opt_deps = pkg.get("optional-dependencies", {}) + if opt_deps: + _add_extras(extras_map, pkg_name, _sorted(opt_deps.keys())) + + deps = pkg.get("dependencies", []) + for dep in deps: + dep_name = dep.get("name", "") + dep_extras_raw = dep.get("extra", []) + dep_extras = [dep_extras_raw] if type(dep_extras_raw) == "string" else dep_extras_raw + if dep_extras and dep_name != pkg_name: + _add_extras(extras_map, dep_name, dep_extras) + + metadata = pkg.get("metadata", {}) + for rd in metadata.get("requires-dist", []): + rd_name = rd.get("name", "") + rd_extras_raw = rd.get("extras", []) + rd_extras = [rd_extras_raw] if type(rd_extras_raw) == "string" else rd_extras_raw + if rd_extras and rd_name != pkg_name: + _add_extras(extras_map, rd_name, rd_extras) + + return extras_map + +def uv_lock_to_requirements(uv_lock): + """Convert a parsed uv.lock JSON struct to a requirements.txt formatted string. + + Args: + uv_lock: a decoded JSON struct from a uv.lock file. + + Returns: + A requirements.txt formatted string. + """ + packages = uv_lock.get("package", []) + extras_map = uv_lock_extras_map(uv_lock) + + dependents = {} + for pkg in packages: + pkg_name = pkg.get("name", "") + deps = pkg.get("dependencies", []) + for dep in deps: + dep_name = dep.get("name", "") + if dep_name != pkg_name: + _add_dependent(dependents, dep_name, pkg_name) + opt_deps = pkg.get("optional-dependencies", {}) + for _extra, deps in opt_deps.items(): + for dep in deps: + dep_name = dep.get("name", "") + if dep_name != pkg_name: + _add_dependent(dependents, dep_name, pkg_name) + + lines = [] + for pkg in packages: + source = pkg.get("source", {}) + if not source.get("registry"): + continue + + pkg_name = pkg.get("name", "") + version = pkg.get("version", "") + + markers = pkg.get("resolution-markers", []) + hashes = _collect_hashes(pkg) + + pkg_extras = extras_map.get(pkg_name, []) + if pkg_extras: + req = "{}[{}]=={}".format(pkg_name, ",".join(pkg_extras), version) + else: + req = "{}=={}".format(pkg_name, version) + if markers: + req += " ; " + " or ".join(markers) + + if hashes: + req += " \\" + lines.append(req) + + _emit_hashes(lines, hashes) + + dep_vias = _sorted(dependents.get(pkg_name, [])) + if dep_vias: + if len(dep_vias) == 1: + lines.append(" # via " + dep_vias[0]) + else: + lines.append(" # via") + for via in dep_vias: + lines.append(" # " + via) + + lines.append("") + + return "\n".join(lines) + +def _add_dependent(dependents, dep_name, dependent_name): + if dep_name not in dependents: + dependents[dep_name] = [] + if dependent_name not in dependents[dep_name]: + dependents[dep_name].append(dependent_name) + +def _add_extras(extras_map, pkg_name, extras): + existing = extras_map.get(pkg_name, []) + for extra in extras: + if extra not in existing: + existing.append(extra) + extras_map[pkg_name] = _sorted(existing) + +def _collect_hashes(pkg): + hashes = [] + for wheel in pkg.get("wheels", []): + whash = wheel.get("hash", "") + if whash.startswith("sha256:"): + hashes.append(whash[len("sha256:"):]) + sdist = pkg.get("sdist") + if sdist: + shash = sdist.get("hash", "") + if shash.startswith("sha256:"): + hashes.append(shash[len("sha256:"):]) + return _sorted(hashes) + +def _sorted(items): + return sorted(items) + +def _emit_hashes(lines, hashes): + for i, h in enumerate(hashes): + suffix = " \\" if i < len(hashes) - 1 else "" + lines.append(" --hash=sha256:{}{}".format(h, suffix)) diff --git a/python/uv/private/uv_repository.bzl b/python/uv/private/uv_repository.bzl index fed4f576d3..79a6495bdc 100644 --- a/python/uv/private/uv_repository.bzl +++ b/python/uv/private/uv_repository.bzl @@ -57,7 +57,7 @@ def _uv_repo_impl(repository_ctx): ), ) - return { + attrs = { "name": repository_ctx.attr.name, "platform": repository_ctx.attr.platform, "sha256": result.sha256, @@ -65,6 +65,16 @@ def _uv_repo_impl(repository_ctx): "version": repository_ctx.attr.version, } + # Bazel <8.3.0 lacks repository_ctx.repo_metadata + if not hasattr(repository_ctx, "repo_metadata"): + return attrs + + reproducible = repository_ctx.attr.sha256 != "" + return repository_ctx.repo_metadata( + reproducible = reproducible, + attrs_for_reproducibility = {} if reproducible else attrs, + ) + uv_repository = repository_rule( _uv_repo_impl, doc = "Fetch external tools needed for uv toolchain", diff --git a/python/versions.bzl b/python/versions.bzl index 30929f82fd..6e3efb34d6 100644 --- a/python/versions.bzl +++ b/python/versions.bzl @@ -15,8 +15,12 @@ """The Python versions we use for the toolchains. """ +load("//python/private:pbs_manifest.bzl", "parse_runtime_manifest") load("//python/private:platform_info.bzl", "platform_info") +##load("@rules_python_internal//:manifest_tool_versions.bzl", "MANIFEST_ENTRIES") +load("//python/private:runtimes_manifest_workspace.bzl", "MANIFEST_TEXT") + # Values present in the @platforms//os package MACOS_NAME = "osx" LINUX_NAME = "linux" @@ -26,846 +30,26 @@ FREETHREADED = "-freethreaded" MUSL = "-musl" INSTALL_ONLY = "install_only" -DEFAULT_RELEASE_BASE_URL = "https://github.com/astral-sh/python-build-standalone/releases/download" +_GITHUB_PREFIX = "https://github.com/astral-sh/python-build-standalone/releases/download" +_LEGACY_GITHUB_PREFIX = "https://github.com/indygreg/python-build-standalone/releases/download" +_ASTRAL_PREFIX = "https://releases.astral.sh/github/python-build-standalone/releases/download" -# When updating the versions and releases, run the following command to get -# the hashes: -# bazel run //python/private:print_toolchains_checksums --//python/config_settings:python_version={major}.{minor}.{patch} -# -# To print hashes for all of the specified versions, run: -# bazel run //python/private:print_toolchains_checksums --//python/config_settings:python_version="" -# -# Note, to users looking at how to specify their tool versions, coverage_tool version for each -# interpreter can be specified by: -# "3.8.10": { -# "url": "20210506/cpython-{python_version}-{platform}-pgo+lto-20210506T0943.tar.zst", -# "sha256": { -# "x86_64-apple-darwin": "8d06bec08db8cdd0f64f4f05ee892cf2fcbc58cfb1dd69da2caab78fac420238", -# "x86_64-unknown-linux-gnu": "aec8c4c53373b90be7e2131093caa26063be6d9d826f599c935c0e1042af3355", -# }, -# "coverage_tool": { -# "x86_64-apple-darwin": """, -# "x86_64-unknown-linux-gnu": """, -# }, -# "strip_prefix": "python", -# }, -# -# It is possible to provide lists in "url". It is also possible to provide patches or patch_strip. -# -# buildifier: disable=unsorted-dict-items -TOOL_VERSIONS = { - "3.8.20": { - "url": "20241002/cpython-{python_version}+20241002-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "2ddfc04bdb3e240f30fb782fa1deec6323799d0e857e0b63fa299218658fd3d4", - "aarch64-unknown-linux-gnu": "9d8798f9e79e0fc0f36fcb95bfa28a1023407d51a8ea5944b4da711f1f75f1ed", - "x86_64-apple-darwin": "68d060cd373255d2ca5b8b3441363d5aa7cc45b0c11bbccf52b1717c2b5aa8bb", - "x86_64-pc-windows-msvc": "41b6709fec9c56419b7de1940d1f87fa62045aff81734480672dcb807eedc47e", - "x86_64-unknown-linux-gnu": "285e141c36f88b2e9357654c5f77d1f8fb29cc25132698fe35bb30d787f38e87", - }, - "strip_prefix": "python", - }, - "3.9.10": { - "url": "20220227/cpython-{python_version}+20220227-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "ad66c2a3e7263147e046a32694de7b897a46fb0124409d29d3a93ede631c8aee", - "aarch64-unknown-linux-gnu": "12dd1f125762f47975990ec744532a1cf3db74ad60f4dfb476ca42deb7f78ca4", - "x86_64-apple-darwin": "fdaf594142446029e314a9beb91f1ac75af866320b50b8b968181e592550cd68", - "x86_64-pc-windows-msvc": "c145d9d8143ce163670af124b623d7a2405143a3708b033b4d33eed355e61b24", - "x86_64-unknown-linux-gnu": "455089cc576bd9a58db45e919d1fc867ecdbb0208067dffc845cc9bbf0701b70", - }, - "strip_prefix": "python", - }, - "3.9.12": { - "url": "20220502/cpython-{python_version}+20220502-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "8dee06c07cc6429df34b6abe091a4684a86f7cec76f5d1ccc1c3ce2bd11168df", - "aarch64-unknown-linux-gnu": "2ee1426c181e65133e57dc55c6a685cb1fb5e63ef02d684b8a667d5c031c4203", - "x86_64-apple-darwin": "2453ba7f76b3df3310353b48c881d6cff622ba06e30d2b6ae91588b2bc9e481a", - "x86_64-pc-windows-msvc": "3024147fd987d9e1b064a3d94932178ff8e0fe98cfea955704213c0762fee8df", - "x86_64-unknown-linux-gnu": "ccca12f698b3b810d79c52f007078f520d588232a36bc12ede944ec3ea417816", - }, - "strip_prefix": "python", - }, - "3.9.13": { - "url": "20220802/cpython-{python_version}+20220802-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "d9603edc296a2dcbc59d7ada780fd12527f05c3e0b99f7545112daf11636d6e5", - "aarch64-unknown-linux-gnu": "80415aac1b96255b9211f6a4c300f31e9940c7e07a23d0dec12b53aa52c0d25e", - "x86_64-apple-darwin": "9540a7efb7c8a54a48aff1cb9480e49588d9c0a3f934ad53f5b167338174afa3", - "x86_64-pc-windows-msvc": "b538127025a467c64b3351babca2e4d2ea7bdfb7867d5febb3529c34456cdcd4", - "x86_64-unknown-linux-gnu": "ce1cfca2715e7e646dd618a8cb9baff93000e345ccc979b801fc6ccde7ce97df", - }, - "strip_prefix": "python", - }, - "3.9.15": { - "url": "20221106/cpython-{python_version}+20221106-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "64dc7e1013481c9864152c3dd806c41144c79d5e9cd3140e185c6a5060bdc9ab", - "aarch64-unknown-linux-gnu": "52a8c0a67fb919f80962d992da1bddb511cdf92faf382701ce7673e10a8ff98f", - "x86_64-apple-darwin": "f2bcade6fc976c472f18f2b3204d67202d43ae55cf6f9e670f95e488f780da08", - "x86_64-pc-windows-msvc": "022daacab215679b87f0d200d08b9068a721605fa4721ebeda38220fc641ccf6", - "x86_64-unknown-linux-gnu": "cdc3a4cfddcd63b6cebdd75b14970e02d8ef0ac5be4d350e57ab5df56c19e85e", - }, - "strip_prefix": "python", - }, - "3.9.16": { - "url": "20230507/cpython-{python_version}+20230507-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "c1de1d854717a6245f45262ef1bb17b09e2c587590e7e3f406593c143ff875bd", - "aarch64-unknown-linux-gnu": "f629b75ebfcafe9ceee2e796b7e4df5cf8dbd14f3c021afca078d159ab797acf", - "ppc64le-unknown-linux-gnu": "ff3ac35c58f67839aff9b5185a976abd3d1abbe61af02089f7105e876c1fe284", - "x86_64-apple-darwin": "3abc4d5fbbc80f5f848f280927ac5d13de8dc03aabb6ae65d8247cbb68e6f6bf", - "x86_64-pc-windows-msvc": "cdabb47204e96ce7ea31fbd0b5ed586114dd7d8f8eddf60a509a7f70b48a1c5e", - "x86_64-unknown-linux-gnu": "2b6e146234a4ef2a8946081fc3fbfffe0765b80b690425a49ebe40b47c33445b", - }, - "strip_prefix": "python", - }, - "3.9.17": { - "url": "20230726/cpython-{python_version}+20230726-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "73dbe2d702210b566221da9265acc274ba15275c5d0d1fa327f44ad86cde9aa1", - "aarch64-unknown-linux-gnu": "b77012ddaf7e0673e4aa4b1c5085275a06eee2d66f33442b5c54a12b62b96cbe", - "ppc64le-unknown-linux-gnu": "c591a28d943dce5cf9833e916125fdfbeb3120270c4866ee214493ccb5b83c3c", - "s390x-unknown-linux-gnu": "01454d7cc7c9c2fccde42ba868c4f372eaaafa48049d49dd94c9cf2875f497e6", - "x86_64-apple-darwin": "dfe1bea92c94b9cb779288b0b06e39157c5ff7e465cdd24032ac147c2af485c0", - "x86_64-pc-windows-msvc": "9b9a1e21eff29dcf043cea38180cf8ca3604b90117d00062a7b31605d4157714", - "x86_64-unknown-linux-gnu": "26c4a712b4b8e11ed5c027db5654eb12927c02da4857b777afb98f7a930ce637", - }, - "strip_prefix": "python", - }, - "3.9.18": { - "url": "20240224/cpython-{python_version}+20240224-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "2548f911a6e316575c303ba42bb51540dc9b47a9f76a06a2a37460d93b177aa2", - "aarch64-unknown-linux-gnu": "e5bc5196baa603d635ee6b0cd141e359752ad3e8ea76127eb9141a3155c51200", - "ppc64le-unknown-linux-gnu": "d6b18df7a25fe034fd5ce4e64216df2cc78b2d4d908d2a1c94058ae700d73d22", - "s390x-unknown-linux-gnu": "15d059507c7e900e9665f31e8d903e5a24a68ceed24f9a1c5ac06ab42a354f3f", - "x86_64-apple-darwin": "171d8b472fce0295be0e28bb702c43d5a2a39feccb3e72efe620ac3843c3e402", - "x86_64-pc-windows-msvc": "a9bdbd728ed4c353a4157ecf74386117fb2a2769a9353f491c528371cfe7f6cd", - "x86_64-unknown-linux-gnu": "0e5663025121186bd17d331538a44f48b41baff247891d014f3f962cbe2716b4", - }, - "strip_prefix": "python", - }, - "3.9.19": { - "url": "20240726/cpython-{python_version}+20240726-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "0e5a7aae57c53d7a849bc7f67764a947b626e3fe8d4d41a8eed11d9e4be0b1c6", - "aarch64-unknown-linux-gnu": "05ec896db9a9d4fe8004b4e4b6a6fdc588a015fedbddb475490885b0d9c7d9b3", - "ppc64le-unknown-linux-gnu": "bfff0e3d536b2f0c315e85926cc317b7b756701b6de781a8972cefbdbc991ca2", - "s390x-unknown-linux-gnu": "059ec97080b205ea5f1ddf71c18e22b691e8d68192bd37d13ad8f4359915299d", - "x86_64-apple-darwin": "f2ae9fcac044a329739b8c1676245e8cb6b3094416220e71823d2673bdea0bdb", - "x86_64-pc-windows-msvc": "a8df6a00140055c9accb0be632e7add951d587bbe3d63c40827bbd5145d8f557", - "x86_64-unknown-linux-gnu": "cbf94cb1c9d4b5501d9b3652f6e8400c2cab7c41dfea48d344d9e7f29692b91b", - }, - "strip_prefix": "python", - }, - "3.9.20": { - "url": "20241016/cpython-{python_version}+20241016-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "34ab2bc4c51502145e1a624b4e4ea06877e3d1934a88cc73ac2e0fd5fd439b75", - "aarch64-unknown-linux-gnu": "1e486c054a4e86666cf24e04f5e29456324ba9c2b95bf1cae1805be90d3da154", - "ppc64le-unknown-linux-gnu": "9a24ccdbfc7f67545d859128f02a3150a160ea6c2fc134b0773bf56f2d90b397", - "s390x-unknown-linux-gnu": "2cee381069bf344fb20eba609af92dfe7ba67eb75bea08eeccf11048a2c380c0", - "x86_64-apple-darwin": "193dc7f0284e4917d52b17a077924474882ee172872f2257cfe3375d6d468ed9", - "x86_64-pc-windows-msvc": "5069008a237b90f6f7a86956903f2a0221b90d471daa6e4a94831eaa399e3993", - "x86_64-unknown-linux-gnu": "c20ee831f7f46c58fa57919b75a40eb2b6a31e03fd29aaa4e8dab4b9c4b60d5d", - "x86_64-unknown-linux-musl": "5c1cc348e317fe7af1acd6a7f665b46eccb554b20d6533f0e76c53f44d4556cc", - }, - "strip_prefix": "python", - }, - "3.9.21": { - "url": "20250317/cpython-{python_version}+20250317-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "2a7d83db10c082ce59e9c4b8bd6c5790310198fb759a7c94aceebac1d93676d3", - "aarch64-unknown-linux-gnu": "758ebbc4d60b3ca26cf21720232043ad626373fbeb6632122e5db622a1f55465", - "ppc64le-unknown-linux-gnu": "3c7c0cc16468659049ac2f843ffba29144dd987869c943b83c2730569b7f57bd", - "riscv64-unknown-linux-gnu": "ef1463ad5349419309060854a5f942b0bd7bd0b9245b53980129836187e68ad9", - "s390x-unknown-linux-gnu": "e66e52dcbe3e20153e7d5844451bf58a69f41b858348e0f59c547444bfe191ee", - "x86_64-apple-darwin": "786ebd91e4dd0920acf60aa3428a627a937342d2455f7eb5e9a491517c32db3d", - "x86_64-pc-windows-msvc": "5392cee2ef7cd20b34128384d0b31864fb3c02bdb7a8ae6995cfec621bb657bc", - "x86_64-unknown-linux-gnu": "6f426b5494e90701ffa2753e229252e8b3ac61151a09c8cd6c0a649512df8ab2", - "x86_64-unknown-linux-musl": "6113c6c5f88d295bb26279b8a49d74126ee12db137854e0d8c3077051a4eddc4", - }, - "strip_prefix": "python", - }, - "3.9.23": { - "url": "20250808/cpython-{python_version}+20250808-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "d32da9eae3f516cc0bd8240bfef54dede757d6daf1d8cf605eacbc8a205884e8", - "aarch64-unknown-linux-gnu": "0318b6c9ad6fb229da8d40aa3671ee27eeb678530246a1b172b72071f76091bc", - "ppc64le-unknown-linux-gnu": "b40b3509dc72abb21f4310f0e94678b36ff73432dc84c41fea132a51c4017f79", - "riscv64-unknown-linux-gnu": "a7d847dc62177cf06237dfa26c317148b22418ded51aa89e8cf7242784293ad4", - "s390x-unknown-linux-gnu": "425abe5d3ec98e9b18c908209a4ffe239a283ee648e0eea65821e45f074689e7", - "x86_64-apple-darwin": "c1bfab90aea566ffaeff65299a20503a880ea93054bbd8bbed98f4f11e9e7383", - "x86_64-pc-windows-msvc": "fb400b25cbcbfed6aeaaca8d9a3cdf1a09b602bf5ed6d1ae7075cde40c1cd81e", - "x86_64-unknown-linux-gnu": "77fd3fa10abbb08949eda70ca7fb94f72e2f9e0016611be328a7b31c3aa9894d", - "x86_64-unknown-linux-musl": "a8a0df23bc1bc050ed8730c65d818382667cf37ba96a08fccd5bb12a689e6a1c", - }, - "strip_prefix": "python", - }, - "3.10.2": { - "url": "20220227/cpython-{python_version}+20220227-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "1409acd9a506e2d1d3b65c1488db4e40d8f19d09a7df099667c87a506f71c0ef", - "aarch64-unknown-linux-gnu": "8f351a8cc348bb45c0f95b8634c8345ec6e749e483384188ad865b7428342703", - "x86_64-apple-darwin": "8146ad4390710ec69b316a5649912df0247d35f4a42e2aa9615bffd87b3e235a", - "x86_64-pc-windows-msvc": "a1d9a594cd3103baa24937ad9150c1a389544b4350e859200b3e5c036ac352bd", - "x86_64-unknown-linux-gnu": "9b64eca2a94f7aff9409ad70bdaa7fbbf8148692662e764401883957943620dd", - }, - "strip_prefix": "python", - }, - "3.10.4": { - "url": "20220502/cpython-{python_version}+20220502-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "2c99983d1e83e4b6e7411ed9334019f193fba626344a50c36fba6c25d4de78a2", - "aarch64-unknown-linux-gnu": "d8098c0c54546637e7516f93b13403b11f9db285def8d7abd825c31407a13d7e", - "x86_64-apple-darwin": "f2711eaffff3477826a401d09a013c6802f11c04c63ab3686aa72664f1216a05", - "x86_64-pc-windows-msvc": "bee24a3a5c83325215521d261d73a5207ab7060ef3481f76f69b4366744eb81d", - "x86_64-unknown-linux-gnu": "f6f871e53a7b1469c13f9bd7920ad98c4589e549acad8e5a1e14760fff3dd5c9", - }, - "strip_prefix": "python", - }, - "3.10.6": { - "url": "20220802/cpython-{python_version}+20220802-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "efaf66acdb9a4eb33d57702607d2e667b1a319d58c167a43c96896b97419b8b7", - "aarch64-unknown-linux-gnu": "81625f5c97f61e2e3d7e9f62c484b1aa5311f21bd6545451714b949a29da5435", - "x86_64-apple-darwin": "7718411adf3ea1480f3f018a643eb0550282aefe39e5ecb3f363a4a566a9398c", - "x86_64-pc-windows-msvc": "91889a7dbdceea585ff4d3b7856a6bb8f8a4eca83a0ff52a73542c2e67220eaa", - "x86_64-unknown-linux-gnu": "55aa2190d28dcfdf414d96dc5dcea9fe048fadcd583dc3981fec020869826111", - }, - "strip_prefix": "python", - }, - "3.10.8": { - "url": "20221106/cpython-{python_version}+20221106-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "d52b03817bd245d28e0a8b2f715716cd0fcd112820ccff745636932c76afa20a", - "aarch64-unknown-linux-gnu": "33170bef18c811906b738be530f934640491b065bf16c4d276c6515321918132", - "x86_64-apple-darwin": "525b79c7ce5de90ab66bd07b0ac1008bafa147ddc8a41bef15ffb7c9c1e9e7c5", - "x86_64-pc-windows-msvc": "f2b6d2f77118f06dd2ca04dae1175e44aaa5077a5ed8ddc63333c15347182bfe", - "x86_64-unknown-linux-gnu": "6c8db44ae0e18e320320bbaaafd2d69cde8bfea171ae2d651b7993d1396260b7", - }, - "strip_prefix": "python", - }, - "3.10.9": { - "url": "20230116/cpython-{python_version}+20230116-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "018d05a779b2de7a476f3b3ff2d10f503d69d14efcedd0774e6dab8c22ef84ff", - "aarch64-unknown-linux-gnu": "2003750f40cd09d4bf7a850342613992f8d9454f03b3c067989911fb37e7a4d1", - "x86_64-apple-darwin": "0e685f98dce0e5bc8da93c7081f4e6c10219792e223e4b5886730fd73a7ba4c6", - "x86_64-pc-windows-msvc": "59c6970cecb357dc1d8554bd0540eb81ee7f6d16a07acf3d14ed294ece02c035", - "x86_64-unknown-linux-gnu": "d196347aeb701a53fe2bb2b095abec38d27d0fa0443f8a1c2023a1bed6e18cdf", - }, - "strip_prefix": "python", - }, - "3.10.11": { - "url": "20230507/cpython-{python_version}+20230507-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "8348bc3c2311f94ec63751fb71bd0108174be1c4def002773cf519ee1506f96f", - "aarch64-unknown-linux-gnu": "c7573fdb00239f86b22ea0e8e926ca881d24fde5e5890851339911d76110bc35", - "ppc64le-unknown-linux-gnu": "73a9d4c89ed51be39dd2de4e235078281087283e9fdedef65bec02f503e906ee", - "x86_64-apple-darwin": "bd3fc6e4da6f4033ebf19d66704e73b0804c22641ddae10bbe347c48f82374ad", - "x86_64-pc-windows-msvc": "9c2d3604a06fcd422289df73015cd00e7271d90de28d2c910f0e2309a7f73a68", - "x86_64-unknown-linux-gnu": "c5bcaac91bc80bfc29cf510669ecad12d506035ecb3ad85ef213416d54aecd79", - }, - "strip_prefix": "python", - }, - "3.10.12": { - "url": "20230726/cpython-{python_version}+20230726-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "bc66c706ea8c5fc891635fda8f9da971a1a901d41342f6798c20ad0b2a25d1d6", - "aarch64-unknown-linux-gnu": "fee80e221663eca5174bd794cb5047e40d3910dbeadcdf1f09d405a4c1c15fe4", - "ppc64le-unknown-linux-gnu": "bb5e8cb0d2e44241725fa9b342238245503e7849917660006b0246a9c97b1d6c", - "s390x-unknown-linux-gnu": "8d33d435ae6fb93ded7fc26798cc0a1a4f546a4e527012a1e2909cc314b332df", - "x86_64-apple-darwin": "8a6e3ed973a671de468d9c691ed9cb2c3a4858c5defffcf0b08969fba9c1dd04", - "x86_64-pc-windows-msvc": "c1a31c353ca44de7d1b1a3b6c55a823e9c1eed0423d4f9f66e617bdb1b608685", - "x86_64-unknown-linux-gnu": "a476dbca9184df9fc69fe6309cda5ebaf031d27ca9e529852437c94ec1bc43d3", - }, - "strip_prefix": "python", - }, - "3.10.13": { - "url": "20240224/cpython-{python_version}+20240224-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "5fdc0f6a5b5a90fd3c528e8b1da8e3aac931ea8690126c2fdb4254c84a3ff04a", - "aarch64-unknown-linux-gnu": "a898a88705611b372297bb8fe4d23cc16b8603ce5f24494c3a8cfa65d83787f9", - "ppc64le-unknown-linux-gnu": "c23706e138a0351fc1e9def2974af7b8206bac7ecbbb98a78f5aa9e7535fee42", - "s390x-unknown-linux-gnu": "09be8fb2cdfbb4a93d555f268f244dbe4d8ff1854b2658e8043aa4ec08aede3e", - "x86_64-apple-darwin": "6378dfd22f58bb553ddb02be28304d739cd730c1f95c15c74955c923a1bc3d6a", - "x86_64-pc-windows-msvc": "086f7fe9156b897bb401273db8359017104168ac36f60f3af4e31ac7acd6634e", - "x86_64-unknown-linux-gnu": "d995d032ca702afd2fc3a689c1f84a6c64972ecd82bba76a61d525f08eb0e195", - }, - "strip_prefix": "python", - }, - "3.10.14": { - "url": "20240726/cpython-{python_version}+20240726-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "164d89f0df2feb689981864ecc1dffb19e6aa3696c8880166de555494fe92607", - "aarch64-unknown-linux-gnu": "39bcd46b4d70e40da177c55259be16d5c2be7a3f7f93f1e3bde47e71b4833f29", - "ppc64le-unknown-linux-gnu": "549d38b9ef59cba9ab2990025255231bfa1cb32b4bc5eac321667640fdee19d1", - "s390x-unknown-linux-gnu": "de4bc878a8666c734f983db971610980870148f333bda8b0c34abfaeae88d7ec", - "x86_64-apple-darwin": "1a1455838cd1e8ed0da14a152a2d559a2fd3a6047ba7013e841db4a35a228c1d", - "x86_64-pc-windows-msvc": "7f68821a8b5445267eca480660364ebd06ec84632b336770c6e39de07ac0f6c3", - "x86_64-unknown-linux-gnu": "32b34cd13d9d745b3db3f3b8398ab2c07de74544829915dbebd8dce39bdc405e", - }, - "strip_prefix": "python", - }, - "3.10.15": { - "url": "20241016/cpython-{python_version}+20241016-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "f64776f455a44c24d50f947c813738cfb7b9ac43732c44891bc831fa7940a33c", - "aarch64-unknown-linux-gnu": "eb58581f85fde83d1f3e8e1f8c6f5a15c7ae4fdbe3b1d1083931f9167fdd8dbc", - "ppc64le-unknown-linux-gnu": "0c45af4e7525e2db59901606db32b2896ac1e9830c6f95551402207f537c2ce4", - "s390x-unknown-linux-gnu": "de205896b070e6f5259ac0f2b3379eead875ea84e6a6ef533b89886fcbb46a4c", - "x86_64-apple-darwin": "90b46dfb1abd98d45663c7a2a8c45d3047a59391d8586d71b459cec7b75f662b", - "x86_64-pc-windows-msvc": "e48952619796c66ec9719867b87be97edca791c2ef7fbf87d42c417c3331609e", - "x86_64-unknown-linux-gnu": "3db2171e03c1a7acdc599fba583c1b92306d3788b375c9323077367af1e9d9de", - "x86_64-unknown-linux-musl": "ed519c47d9620eb916a6f95ec2875396e7b1a9ab993ee40b2f31b837733f318c", - }, - "strip_prefix": "python", - }, - "3.10.16": { - "url": "20250317/cpython-{python_version}+20250317-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "e99f8457d9c79592c036489c5cfa78df76e4762d170665e499833e045d82608f", - "aarch64-unknown-linux-gnu": "76d0f04d2444e77200fdc70d1c57480e29cca78cb7420d713bc1c523709c198d", - "ppc64le-unknown-linux-gnu": "39c9b3486de984fe1d72d90278229c70d6b08bcf69cd55796881b2d75077b603", - "riscv64-unknown-linux-gnu": "ebe949ada9293581c17d9bcdaa8f645f67d95f73eac65def760a71ef9dd6600d", - "s390x-unknown-linux-gnu": "9b2fc0b7f1c75b48e799b6fa14f7e24f5c61f2db82e3c65d13ed25e08f7f0857", - "x86_64-apple-darwin": "e03e62dbe95afa2f56b7344ff3bd061b180a0b690ff77f9a1d7e6601935e05ca", - "x86_64-pc-windows-msvc": "c7e0eb0ff5b36758b7a8cacd42eb223c056b9c4d36eded9bf5b9fe0c0b9aeb08", - "x86_64-unknown-linux-gnu": "b350c7e63956ca8edb856b91316328e0fd003a840cbd63d08253af43b2c63643", - "x86_64-unknown-linux-musl": "6ed64923ee4fbea4c5780f1a5a66651d239191ac10bd23420db4f5e4e0bf79c4", - }, - "strip_prefix": "python", - }, - "3.10.18": { - "url": "20250808/cpython-{python_version}+20250808-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "a94c02b2d597cd6b075a713fe4e9a909cc97ca6a3b2b2ce86eda21be2062d48e", - "aarch64-unknown-linux-gnu": "ef7de3b715d519e246d98ff7856247f7f7b357068705f09c6f300b7e7b76c701", - "ppc64le-unknown-linux-gnu": "f580efed11cc54e1a221c052e8bc88bfbc12844d3ca8949da828351a1232386e", - "riscv64-unknown-linux-gnu": "0d7e460e30203a9225b6f417ae972f66415a1cc0e32b37ebc48d195816282669", - "s390x-unknown-linux-gnu": "d4ada974daadb08a0184c19232ee3b03b3137aa70609760e1a94aaf7b12989ef", - "x86_64-apple-darwin": "da96fe2ba841640215788ddb9f151f03629360e37fcb94d4f76e5095b87df0d4", - "x86_64-pc-windows-msvc": "a648f3c9d136985ccfe57a5507e73d9d0839f7fd09eebd7c247857f2feaecb2a", - "x86_64-unknown-linux-gnu": "0b310a73bb9e7a495dbcad5f685e508ca2e7b36ee8f29301a52285730c425789", - "x86_64-unknown-linux-musl": "9cecf6ea2effbe183faebcf7e1160425a4ee17a68e49f2eefe5e1c59c51fa7ee", - }, - "strip_prefix": "python", - }, - "3.11.1": { - "url": "20230116/cpython-{python_version}+20230116-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "4918cdf1cab742a90f85318f88b8122aeaa2d04705803c7b6e78e81a3dd40f80", - "aarch64-unknown-linux-gnu": "debf15783bdcb5530504f533d33fda75a7b905cec5361ae8f33da5ba6599f8b4", - "x86_64-apple-darwin": "20a4203d069dc9b710f70b09e7da2ce6f473d6b1110f9535fb6f4c469ed54733", - "x86_64-pc-windows-msvc": "edc08979cb0666a597466176511529c049a6f0bba8adf70df441708f766de5bf", - "x86_64-unknown-linux-gnu": "02a551fefab3750effd0e156c25446547c238688a32fabde2995c941c03a6423", - }, - "strip_prefix": "python", - }, - "3.11.3": { - "url": "20230507/cpython-{python_version}+20230507-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "09e412506a8d63edbb6901742b54da9aa7faf120b8dbdce56c57b303fc892c86", - "aarch64-unknown-linux-gnu": "8190accbbbbcf7620f1ff6d668e4dd090c639665d11188ce864b62554d40e5ab", - "ppc64le-unknown-linux-gnu": "767d24f3570b35fedb945f5ac66224c8983f2d556ab83c5cfaa5f3666e9c212c", - "x86_64-apple-darwin": "f710b8d60621308149c100d5175fec39274ed0b9c99645484fd93d1716ef4310", - "x86_64-pc-windows-msvc": "24741066da6f35a7ff67bee65ce82eae870d84e1181843e64a7076d1571e95af", - "x86_64-unknown-linux-gnu": "da50b87d1ec42b3cb577dfd22a3655e43a53150f4f98a4bfb40757c9d7839ab5", - }, - "strip_prefix": "python", - }, - "3.11.4": { - "url": "20230726/cpython-{python_version}+20230726-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "cb6d2948384a857321f2aa40fa67744cd9676a330f08b6dad7070bda0b6120a4", - "aarch64-unknown-linux-gnu": "2e84fc53f4e90e11963281c5c871f593abcb24fc796a50337fa516be99af02fb", - "ppc64le-unknown-linux-gnu": "df7b92ed9cec96b3bb658fb586be947722ecd8e420fb23cee13d2e90abcfcf25", - "s390x-unknown-linux-gnu": "e477f0749161f9aa7887964f089d9460a539f6b4a8fdab5166f898210e1a87a4", - "x86_64-apple-darwin": "47e1557d93a42585972772e82661047ca5f608293158acb2778dccf120eabb00", - "x86_64-pc-windows-msvc": "878614c03ea38538ae2f758e36c85d2c0eb1eaaca86cd400ff8c76693ee0b3e1", - "x86_64-unknown-linux-gnu": "e26247302bc8e9083a43ce9e8dd94905b40d464745b1603041f7bc9a93c65d05", - }, - "strip_prefix": "python", - }, - "3.11.5": { - "url": "20230826/cpython-{python_version}+20230826-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "dab64b3580118ad2073babd7c29fd2053b616479df5c107d31fe2af1f45e948b", - "aarch64-unknown-linux-gnu": "bb5c5d1ea0f199fe2d3f0996fff4b48ca6ddc415a3dbd98f50bff7fce48aac80", - "ppc64le-unknown-linux-gnu": "14121b53e9c8c6d0741f911ae00102a35adbcf5c3cdf732687ef7617b7d7304d", - "s390x-unknown-linux-gnu": "fe459da39874443579d6fe88c68777c6d3e331038e1fb92a0451879fb6beb16d", - "x86_64-apple-darwin": "4a4efa7378c72f1dd8ebcce1afb99b24c01b07023aa6b8fea50eaedb50bf2bfc", - "x86_64-pc-windows-msvc": "00f002263efc8aea896bcfaaf906b1f4dab3e5cd3db53e2b69ab9a10ba220b97", - "x86_64-unknown-linux-gnu": "fbed6f7694b2faae5d7c401a856219c945397f772eea5ca50c6eb825cbc9d1e1", - }, - "strip_prefix": "python", - }, - "3.11.6": { - "url": "20231002/cpython-{python_version}+20231002-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "916c35125b5d8323a21526d7a9154ca626453f63d0878e95b9f613a95006c990", - "aarch64-unknown-linux-gnu": "3e26a672df17708c4dc928475a5974c3fb3a34a9b45c65fb4bd1e50504cc84ec", - "ppc64le-unknown-linux-gnu": "7937035f690a624dba4d014ffd20c342e843dd46f89b0b0a1e5726b85deb8eaf", - "s390x-unknown-linux-gnu": "f9f19823dba3209cedc4647b00f46ed0177242917db20fb7fb539970e384531c", - "x86_64-apple-darwin": "178cb1716c2abc25cb56ae915096c1a083e60abeba57af001996e8bc6ce1a371", - "x86_64-pc-windows-msvc": "3933545e6d41462dd6a47e44133ea40995bc6efeed8c2e4cbdf1a699303e95ea", - "x86_64-unknown-linux-gnu": "ee37a7eae6e80148c7e3abc56e48a397c1664f044920463ad0df0fc706eacea8", - }, - "strip_prefix": "python", - }, - "3.11.7": { - "url": "20240107/cpython-{python_version}+20240107-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "b042c966920cf8465385ca3522986b12d745151a72c060991088977ca36d3883", - "aarch64-unknown-linux-gnu": "b102eaf865eb715aa98a8a2ef19037b6cc3ae7dfd4a632802650f29de635aa13", - "ppc64le-unknown-linux-gnu": "b44e1b74afe75c7b19143413632c4386708ae229117f8f950c2094e9681d34c7", - "s390x-unknown-linux-gnu": "49520e3ff494708020f306e30b0964f079170be83e956be4504f850557378a22", - "x86_64-apple-darwin": "a0e615eef1fafdc742da0008425a9030b7ea68a4ae4e73ac557ef27b112836d4", - "x86_64-pc-windows-msvc": "67077e6fa918e4f4fd60ba169820b00be7c390c497bf9bc9cab2c255ea8e6f3e", - "x86_64-unknown-linux-gnu": "4a51ce60007a6facf64e5495f4cf322e311ba9f39a8cd3f3e4c026eae488e140", - }, - "strip_prefix": "python", - }, - "3.11.8": { - "url": "20240224/cpython-{python_version}+20240224-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "389a51139f5abe071a0d70091ca5df3e7a3dfcfcbe3e0ba6ad85fb4c5638421e", - "aarch64-unknown-linux-gnu": "389b9005fb78dd5a6f68df5ea45ab7b30d9a4b3222af96999e94fd20d4ad0c6a", - "ppc64le-unknown-linux-gnu": "eb2b31f8e50309aae493c6a359c32b723a676f07c641f5e8fe4b6aa4dbb50946", - "s390x-unknown-linux-gnu": "844f64f4c16e24965778281da61d1e0e6cd1358a581df1662da814b1eed096b9", - "x86_64-apple-darwin": "097f467b0c36706bfec13f199a2eaf924e668f70c6e2bd1f1366806962f7e86e", - "x86_64-pc-windows-msvc": "b618f1f047349770ee1ef11d1b05899840abd53884b820fd25c7dfe2ec1664d4", - "x86_64-unknown-linux-gnu": "94e13d0e5ad417035b80580f3e893a72e094b0900d5d64e7e34ab08e95439987", - }, - "strip_prefix": "python", - }, - "3.11.9": { - "url": "20240726/cpython-{python_version}+20240726-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "cbdac9462bab9671c8e84650e425d3f43b775752a930a2ef954a0d457d5c00c3", - "aarch64-unknown-linux-gnu": "4d17cf988abe24449d649aad3ef974091ab76807904d41839907061925b4c9e3", - "ppc64le-unknown-linux-gnu": "fc4f3c9ef9bfac2ed0282126ff376e544697ad04a5408d6429d46899d7d3bf21", - "s390x-unknown-linux-gnu": "e69b66e53e926460df044f44846eef3fea642f630e829719e1a4112fc370dc56", - "x86_64-apple-darwin": "dc3174666a30f4c38d04e79a80c3159b4b3aa69597c4676701c8386696811611", - "x86_64-pc-windows-msvc": "f694be48bdfec1dace6d69a19906b6083f4dd7c7c61f1138ba520e433e5598f8", - "x86_64-unknown-linux-gnu": "f6e955dc9ddfcad74e77abe6f439dac48ebca14b101ed7c85a5bf3206ed2c53d", - }, - "strip_prefix": "python", - }, - "3.11.10": { - "url": "20241016/cpython-{python_version}+20241016-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "5a69382da99c4620690643517ca1f1f53772331b347e75f536088c42a4cf6620", - "aarch64-unknown-linux-gnu": "803e49259280af0f5466d32829cd9d65a302b0226e424b3f0b261f9daf6aee8f", - "ppc64le-unknown-linux-gnu": "92b666d103902001322f42badbd68da92adc5cebb826af9c1c906c33166e2f34", - "s390x-unknown-linux-gnu": "6d584317651c1ad4a857cb32d1999707e8bb3046fcb2f156d80381814fa19fde", - "x86_64-apple-darwin": "1e23ffe5bc473e1323ab8f51464da62d77399afb423babf67f8e13c82b69c674", - "x86_64-pc-windows-msvc": "647b66ff4552e70aec3bf634dd470891b4a2b291e8e8715b3bdb162f577d4c55", - "x86_64-unknown-linux-gnu": "8b50a442b04724a24c1eebb65a36a0c0e833d35374dbdf9c9470d8a97b164cd9", - "x86_64-unknown-linux-musl": "d36fc77a8dd76155a7530f6235999a693b9e7c48aa11afeb5610a091cae5aa6f", - }, - "strip_prefix": "python", - }, - "3.11.13": { - "url": "20250808/cpython-{python_version}+20250808-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "d089bfd2c7b98a0942750a195e70d3172beda76d7747097b8afd87028b6e59b6", - "aarch64-unknown-linux-gnu": "bc57105f8a16acd57b71d926143c7f6ecf61729b40c8b4656f1b98bebd47c710", - "ppc64le-unknown-linux-gnu": "16a0165b0744940702b8fff80b8bf973ac914f78cb6fca28d389583f675e84de", - "riscv64-unknown-linux-gnu": "d8e62306be8f41c46bcd62ca68f91a1467f47adff632a35ff413dc1043ed56e8", - "s390x-unknown-linux-gnu": "4e302a4514a73baefdd9b327062bdafeb4115a799deec91c185f6ab45a857241", - "x86_64-apple-darwin": "d946d618f8bba8308b67e460a30612a71e2ccc309f85f6628aaae24e2b816981", - "x86_64-pc-windows-msvc": "ed963aee33d29ad8abfbb5fe63e42f57a2638a4a11a88e11d8bb66e61f20a6e5", - "aarch64-pc-windows-msvc": "a632857c966237e7fd38b44c47c350f6e30d8ec54dcad6c832865ad670f0f22f", - "x86_64-unknown-linux-gnu": "3ad988c702cbb017fef1208d47dea4138a2e85fd0f7f01ec5e1e335e597131b9", - "x86_64-unknown-linux-musl": "3a5810f0696f844289aa06d5c3a1efeab66eee999c25196b7d1954192a2c2100", - }, - "strip_prefix": "python", - }, - "3.12.0": { - "url": "20231002/cpython-{python_version}+20231002-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "4734a2be2becb813830112c780c9879ac3aff111a0b0cd590e65ec7465774d02", - "aarch64-unknown-linux-gnu": "bccfe67cf5465a3dfb0336f053966e2613a9bc85a6588c2fcf1366ef930c4f88", - "ppc64le-unknown-linux-gnu": "b5dae075467ace32c594c7877fe6ebe0837681f814601d5d90ba4c0dfd87a1f2", - "s390x-unknown-linux-gnu": "5681621349dd85d9726d1b67c84a9686ce78f72e73a6f9e4cc4119911655759e", - "x86_64-apple-darwin": "5a9e88c8aa52b609d556777b52ebde464ae4b4f77e4aac4eb693af57395c9abf", - "x86_64-pc-windows-msvc": "facfaa1fbc8653f95057f3c4a0f8aa833dab0e0b316e24ee8686bc761d4b4f8d", - "x86_64-unknown-linux-gnu": "e51a5293f214053ddb4645b2c9f84542e2ef86870b8655704367bd4b29d39fe9", - }, - "strip_prefix": "python", - }, - "3.12.1": { - "url": "20240107/cpython-{python_version}+20240107-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "f93f8375ca6ac0a35d58ff007043cbd3a88d9609113f1cb59cf7c8d215f064af", - "aarch64-unknown-linux-gnu": "236533ef20e665007a111c2f36efb59c87ae195ad7dca223b6dc03fb07064f0b", - "ppc64le-unknown-linux-gnu": "78051f0d1411ee62bc2af5edfccf6e8400ac4ef82887a2affc19a7ace6a05267", - "s390x-unknown-linux-gnu": "60631211c701f8d2c56e5dd7b154e68868128a019b9db1d53a264f56c0d4aee2", - "x86_64-apple-darwin": "eca96158c1568dedd9a0b3425375637a83764d1fa74446438293089a8bfac1f8", - "x86_64-pc-windows-msvc": "fd5a9e0f41959d0341246d3643f2b8794f638adc0cec8dd5e1b6465198eae08a", - "x86_64-unknown-linux-gnu": "74e330b8212ca22fd4d9a2003b9eec14892155566738febc8e5e572f267b9472", - }, - "strip_prefix": "python", - }, - "3.12.2": { - "url": "20240224/cpython-{python_version}+20240224-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "01c064c00013b0175c7858b159989819ead53f4746d40580b5b0b35b6e80fba6", - "aarch64-unknown-linux-gnu": "e52550379e7c4ac27a87de832d172658bc04150e4e27d4e858e6d8cbb96fd709", - "ppc64le-unknown-linux-gnu": "74bc02c4bbbd26245c37b29b9e12d0a9c1b7ab93477fed8b651c988b6a9a6251", - "s390x-unknown-linux-gnu": "ecd6b0285e5eef94deb784b588b4b425a15a43ae671bf206556659dc141a9825", - "x86_64-apple-darwin": "a53a6670a202c96fec0b8c55ccc780ea3af5307eb89268d5b41a9775b109c094", - "x86_64-pc-windows-msvc": "1e5655a6ccb1a64a78460e4e3ee21036c70246800f176a6c91043a3fe3654a3b", - "x86_64-unknown-linux-gnu": "57a37b57f8243caa4cdac016176189573ad7620f0b6da5941c5e40660f9468ab", - }, - "strip_prefix": "python", - }, - "3.12.3": { - "url": "20240415/cpython-{python_version}+20240415-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "ccc40e5af329ef2af81350db2a88bbd6c17b56676e82d62048c15d548401519e", - "aarch64-unknown-linux-gnu": "ec8126de97945e629cca9aedc80a29c4ae2992c9d69f2655e27ae73906ba187d", - "ppc64le-unknown-linux-gnu": "c5dcf08b8077e617d949bda23027c49712f583120b3ed744f9b143da1d580572", - "s390x-unknown-linux-gnu": "872fc321363b8cdd826fd2cb1adfd1ceb813bc1281f9d410c1c2c4e177e8df86", - "x86_64-apple-darwin": "c37a22fca8f57d4471e3708de6d13097668c5f160067f264bb2b18f524c890c8", - "x86_64-pc-windows-msvc": "f7cfa4ad072feb4578c8afca5ba9a54ad591d665a441dd0d63aa366edbe19279", - "x86_64-unknown-linux-gnu": "a73ba777b5d55ca89edef709e6b8521e3f3d4289581f174c8699adfb608d09d6", - }, - "strip_prefix": "python", - }, - "3.12.4": { - "url": "20240726/cpython-{python_version}+20240726-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "1801025e825c04b3907e4ef6220a13607bc0397628c9485897073110ef7fde15", - "aarch64-unknown-linux-gnu": "a098b18b7e9fea0c66867b76c0124fce9465765017572b2e7b522154c87c78d7", - "ppc64le-unknown-linux-gnu": "04011c4c5b7fe34b0b895edf4ad8748e410686c1d69aaee11d6688d481023bcb", - "s390x-unknown-linux-gnu": "8f8f3e29cf0c2facdbcfee70660939fda7667ac24fee8656d3388fc72f3acc7c", - "x86_64-apple-darwin": "4c325838c1b0ed13698506fcd515be25c73dcbe195f8522cf98f9148a97601ed", - "x86_64-pc-windows-msvc": "74309b0f322716409883d38c621743ea7fa0376eb00927b8ee1e1671d3aff450", - "x86_64-unknown-linux-gnu": "e133dd6fc6a2d0033e2658637cc22e9c95f9d7073b80115037ee1f16417a54ac", - }, - "strip_prefix": "python", - }, - "3.12.7": { - "url": "20241016/cpython-{python_version}+20241016-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "4c18852bf9c1a11b56f21bcf0df1946f7e98ee43e9e4c0c5374b2b3765cf9508", - "aarch64-unknown-linux-gnu": "bba3c6be6153f715f2941da34f3a6a69c2d0035c9c5396bc5bb68c6d2bd1065a", - "ppc64le-unknown-linux-gnu": "0a1d1d92e33a969bd2f40a80af53c97b6c0cc1060d384ceff50ff801593bf9d6", - "s390x-unknown-linux-gnu": "935676a0c960b552f95e9ac2e1e385de5de4b34038ff65ffdc688838f1189c17", - "x86_64-apple-darwin": "60c5271e7edc3c2ab47440b7abf4ed50fbc693880b474f74f05768f5b657045a", - "x86_64-pc-windows-msvc": "f05531bff16fa77b53be0776587b97b466070e768e6d5920894de988bdcd547a", - "x86_64-unknown-linux-gnu": "43576f7db1033dd57b900307f09c2e86f371152ac8a2607133afa51cbfc36064", - "x86_64-unknown-linux-musl": "5ed4a4078db3cbac563af66403aaa156cd6e48831d90382a1820db2b120627b5", - }, - "strip_prefix": "python", - }, - "3.12.8": { - "url": "20241206/cpython-{python_version}+20241206-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "e3c4aa607717b23903ca2650d5c3ee24f89b97543e2db2b0f463bddc7a9e92f3", - "aarch64-unknown-linux-gnu": "ce674b55442b732973afb2932c281bb1ded4ad7e22bcf9b07071165770758c7e", - "ppc64le-unknown-linux-gnu": "b7214790b273de9ed0532420054b72ba1393d62d2fc844ec55ade193771bd90c", - "s390x-unknown-linux-gnu": "73102f5dbd7d1e7e9c2f2c80aedf2893d99a7fa407f6674ec8b2f57ba07daee5", - "x86_64-apple-darwin": "3ba35c706577d755e8e52a4c161a042464577c0e695e2a605362fa469e26de10", - "x86_64-pc-windows-msvc": "767b4be3ddf6b99e5ade519789c1615c191d8cf99d5aff4685cc18b48931f1e6", - "x86_64-unknown-linux-gnu": "b9d6ee5ddac1198e72d53112698773fc8bb597de095592eb849ca794306699ba", - "x86_64-unknown-linux-musl": "6f305888703691dd04cfff85284d23ea0b0146ed7c4415e472f1fb72b3f32cdf", - }, - "strip_prefix": "python", - }, - "3.12.9": { - "url": "20250317/cpython-{python_version}+20250317-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "7c7fd9809da0382a601a79287b5d62d61ce0b15f5a5ee836233727a516e85381", - "aarch64-unknown-linux-gnu": "00c6bf9acef21ac741fea24dc449d0149834d30e9113429e50a95cce4b00bb80", - "ppc64le-unknown-linux-gnu": "25d77599dfd5849f17391d92da0da99079e4e94f19a881f763f5cc62530ef7e1", - "riscv64-unknown-linux-gnu": "e97ab0fdf443b302c56a52b4fd08f513bf3be66aa47263f0f9df3c6e60e05f2e", - "s390x-unknown-linux-gnu": "7492d079ffa8425c8f6c58e43b237c37e3fb7b31e2e14635927bb4d3397ba21e", - "x86_64-apple-darwin": "1ee1b1bb9fbce5c145c4bec9a3c98d7a4fa22543e09a7c1d932bc8599283c2dc", - "x86_64-pc-windows-msvc": "d15361fd202dd74ae9c3eece1abdab7655f1eba90bf6255cad1d7c53d463ed4d", - "x86_64-unknown-linux-gnu": "ef382fb88cbb41a3b0801690bd716b8a1aec07a6c6471010bcc6bd14cd575226", - "x86_64-unknown-linux-musl": "94e3837da1adf9964aab2d6047b33f70167de3096d1f9a2d1fa9340b1bbf537d", - }, - "strip_prefix": "python", - }, - "3.12.11": { - "url": "20250808/cpython-{python_version}+20250808-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "8792c4a84c364ab975feca0c27d3157a5435b7baab325a346ae56b223893b661", - "aarch64-unknown-linux-gnu": "4d7ba5314fab02130d6538f074961ffbf61310cade9180e59026074f9a8939cb", - "aarch64-pc-windows-msvc": "00bf7d7e8bcf5d1e9c4dfca0247d8e035147777cd57ee9d4c64dedca86b0a464", - "ppc64le-unknown-linux-gnu": "2c862eb40a81549d9c11e6bf5a7f07c3406310b14e6a4d16dcdf1c4763ef7090", - "riscv64-unknown-linux-gnu": "0bb729b95fabd49c7b495f7c44a9086e3970ea57daf66365741574bd36a17e81", - "s390x-unknown-linux-gnu": "99e465882d217d24ac90e99fac8f32e6a644d0340ac05ee510fb5cdf53f0cfb8", - "x86_64-apple-darwin": "e0c932709dafb05f00e528a7560ef8ee559ac82b75faca60dd1245bca1c1553f", - "x86_64-pc-windows-msvc": "81214ef71964a40ec269a79067ca490d45298c350583bc3af0e5781451a05c3c", - "x86_64-unknown-linux-gnu": "63d78840bf209af8da8f24e335d910f88387b892ca9187be571d481c071751bb", - "x86_64-unknown-linux-musl": "d633d070780590aa03ac5575cd9d7b9e17682d80f14b400313c009c387cf706b", - }, - "strip_prefix": "python", - }, - "3.13.0": { - "url": "20241016/cpython-{python_version}+20241016-{platform}-{build}.{ext}", - "sha256": { - "aarch64-apple-darwin": "31397953849d275aa2506580f3fa1cb5a85b6a3d392e495f8030e8b6412f5556", - "aarch64-unknown-linux-gnu": "e8378c0162b2e0e4cc1f62b29443a3305d116d09583304dbb0149fecaff6347b", - "ppc64le-unknown-linux-gnu": "fc4b7f27c4e84c78f3c8e6c7f8e4023e4638d11f1b36b6b5ce457b1926cebb53", - "s390x-unknown-linux-gnu": "66b19e6a07717f6cfcd3a8ca953f0a2eaa232291142f3d26a8d17c979ec0f467", - "x86_64-apple-darwin": "cff1b7e7cd26f2d47acac1ad6590e27d29829776f77e8afa067e9419f2f6ce77", - "x86_64-pc-windows-msvc": "b25926e8ce4164cf103bacc4f4d154894ea53e07dd3fdd5ebb16fb1a82a7b1a0", - "x86_64-unknown-linux-gnu": "2c8cb15c6a2caadaa98af51df6fe78a8155b8471cb3dd7b9836038e0d3657fb4", - "x86_64-unknown-linux-musl": "2f61ee3b628a56aceea63b46c7afe2df3e22a61da706606b0c8efda57f953cf4", - "aarch64-apple-darwin-freethreaded": "efc2e71c0e05bc5bedb7a846e05f28dd26491b1744ded35ed82f8b49ccfa684b", - "aarch64-unknown-linux-gnu-freethreaded": "59b50df9826475d24bb7eff781fa3949112b5e9c92adb29e96a09cdf1216d5bd", - "ppc64le-unknown-linux-gnu-freethreaded": "1217efa5f4ce67fcc9f7eb64165b1bd0912b2a21bc25c1a7e2cb174a21a5df7e", - "s390x-unknown-linux-gnu-freethreaded": "6c3e1e4f19d2b018b65a7e3ef4cd4225c5b9adfbc490218628466e636d5c4b8c", - "x86_64-apple-darwin-freethreaded": "2e07dfea62fe2215738551a179c87dbed1cc79d1b3654f4d7559889a6d5ce4eb", - "x86_64-pc-windows-msvc-freethreaded": "bfd89f9acf866463bc4baf01733da5e767d13f5d0112175a4f57ba91f1541310", - "x86_64-unknown-linux-gnu-freethreaded": "a73adeda301ad843cce05f31a2d3e76222b656984535a7b87696a24a098b216c", - }, - "strip_prefix": { - "aarch64-apple-darwin": "python", - "aarch64-unknown-linux-gnu": "python", - "ppc64le-unknown-linux-gnu": "python", - "s390x-unknown-linux-gnu": "python", - "x86_64-apple-darwin": "python", - "x86_64-pc-windows-msvc": "python", - "x86_64-unknown-linux-gnu": "python", - "x86_64-unknown-linux-musl": "python", - "aarch64-apple-darwin-freethreaded": "python/install", - "aarch64-unknown-linux-gnu-freethreaded": "python/install", - "ppc64le-unknown-linux-gnu-freethreaded": "python/install", - "s390x-unknown-linux-gnu-freethreaded": "python/install", - "x86_64-apple-darwin-freethreaded": "python/install", - "x86_64-pc-windows-msvc-freethreaded": "python/install", - "x86_64-unknown-linux-gnu-freethreaded": "python/install", - }, - }, - "3.13.1": { - "url": "20241205/cpython-{python_version}+20241205-{platform}-{build}.{ext}", - "sha256": { - "aarch64-apple-darwin": "88b88b609129c12f4b3841845aca13230f61e97ba97bd0fb28ee64b0e442a34f", - "aarch64-unknown-linux-gnu": "fdfa86c2746d2ae700042c461846e6c37f70c249925b58de8cd02eb8d1423d4e", - "ppc64le-unknown-linux-gnu": "27b20b3237c55430ca1304e687d021f88373f906249f9cd272c5ff2803d5e5c3", - "s390x-unknown-linux-gnu": "7d0187e20cb5e36c689eec27e4d3de56d8b7f1c50dc5523550fc47377801521f", - "x86_64-apple-darwin": "47eef6efb8664e2d1d23a7cdaf56262d784f8ace48f3bfca1b183e95a49888d6", - "x86_64-pc-windows-msvc": "f51f0493a5f979ff0b8d8c598a8d74f2a4d86a190c2729c85e0af65c36a9cbbe", - "x86_64-unknown-linux-gnu": "242b2727df6c1e00de6a9f0f0dcb4562e168d27f428c785b0eb41a6aeb34d69a", - "x86_64-unknown-linux-musl": "76b30c6373b9c0aa2ba610e07da02f384aa210ac79643da38c66d3e6171c6ef5", - "aarch64-apple-darwin-freethreaded": "08f05618bdcf8064a7960b25d9ba92155447c9b08e0cf2f46a981e4c6a1bb5a5", - "aarch64-unknown-linux-gnu-freethreaded": "9f2fcb809f9ba6c7c014a8803073a88786701a98971135bce684355062e4bb35", - "ppc64le-unknown-linux-gnu-freethreaded": "15ceea78dff78ca8ccaac8d9c54b808af30daaa126f1f561e920a6896e098634", - "s390x-unknown-linux-gnu-freethreaded": "ed3c6118d1d12603309c930e93421ac7a30a69045ffd43006f63ecf71d72c317", - "x86_64-apple-darwin-freethreaded": "dc780fecd215d2cc9e573abf1e13a175fcfa8f6efd100ef888494a248a16cda8", - "x86_64-pc-windows-msvc-freethreaded": "7537b2ab361c0eabc0eabfca9ffd9862d7f5f6576eda13b97e98aceb5eea4fd3", - "x86_64-unknown-linux-gnu-freethreaded": "9ec1b81213f849d91f5ebe6a16196e85cd6ff7c05ca823ce0ab7ba5b0e9fee84", - }, - "strip_prefix": { - "aarch64-apple-darwin": "python", - "aarch64-unknown-linux-gnu": "python", - "ppc64le-unknown-linux-gnu": "python", - "s390x-unknown-linux-gnu": "python", - "x86_64-apple-darwin": "python", - "x86_64-pc-windows-msvc": "python", - "x86_64-unknown-linux-gnu": "python", - "x86_64-unknown-linux-musl": "python", - "aarch64-apple-darwin-freethreaded": "python/install", - "aarch64-unknown-linux-gnu-freethreaded": "python/install", - "ppc64le-unknown-linux-gnu-freethreaded": "python/install", - "s390x-unknown-linux-gnu-freethreaded": "python/install", - "x86_64-apple-darwin-freethreaded": "python/install", - "x86_64-pc-windows-msvc-freethreaded": "python/install", - "x86_64-unknown-linux-gnu-freethreaded": "python/install", - }, - }, - "3.13.2": { - "url": "20250317/cpython-{python_version}+20250317-{platform}-{build}.{ext}", - "sha256": { - "aarch64-apple-darwin": "faa44274a331eb39786362818b21b3a4e74514e8805000b20b0e55c590cecb94", - "aarch64-unknown-linux-gnu": "9c67260446fee6ea706dad577a0b32936c63f449c25d66e4383d5846b2ab2e36", - "ppc64le-unknown-linux-gnu": "345b53d2f86c9dbd7f1320657cb227ff9a42ef63ff21f129abbbc8c82a375147", - "riscv64-unknown-linux-gnu": "172d22b2330737f3a028ea538ffe497c39a066a8d3200b22dd4d177a3332ad85", - "s390x-unknown-linux-gnu": "ec3b16ea8a97e3138acec72bc5ff35949950c62c8994a8ec8e213fd93f0e806b", - "x86_64-apple-darwin": "ee4526e84b5ce5b11141c50060b385320f2773616249a741f90c96d460ce8e8f", - "x86_64-pc-windows-msvc": "84d7b52f3558c8e35c670a4fa14080c75e3ec584adfae49fec8b51008b75b21e", - "x86_64-unknown-linux-gnu": "db011f0cd29cab2291584958f4e2eb001b0e6051848d89b38a2dc23c5c54e512", - "x86_64-unknown-linux-musl": "00bb2d629f7eacbb5c6b44dc04af26d1f1da64cee3425b0d8eb5135a93830296", - "aarch64-apple-darwin-freethreaded": "c98c9c977e6fa05c3813bd49f3553904d89d60fed27e2e36468da7afa1d6d5e2", - "aarch64-unknown-linux-gnu-freethreaded": "b8635e59e3143fd17f19a3dfe8ccc246ee6587c87da359bd1bcab35eefbb5f19", - "ppc64le-unknown-linux-gnu-freethreaded": "6ae8fa44cb2edf4ab49cff1820b53c40c10349c0f39e11b8cd76ce7f3e7e1def", - "riscv64-unknown-linux-gnu-freethreaded": "2af1b8850c52801fb6189e7a17a51e0c93d9e46ddefcca72247b76329c97d02a", - "s390x-unknown-linux-gnu-freethreaded": "c074144cc80c2af32c420b79a9df26e8db405212619990c1fbdd308bd75afe3f", - "x86_64-apple-darwin-freethreaded": "0d73e4348d8d4b5159058609d2303705190405b485dd09ad05d870d7e0f36e0f", - "x86_64-pc-windows-msvc-freethreaded": "c51b4845fda5421e044067c111192f645234081d704313f74ee77fa013a186ea", - "x86_64-unknown-linux-gnu-freethreaded": "1aea5062614c036904b55c1cc2fb4b500b7f6f7a4cacc263f4888889d355eef8", - }, - "strip_prefix": { - "aarch64-apple-darwin": "python", - "aarch64-unknown-linux-gnu": "python", - "ppc64le-unknown-linux-gnu": "python", - "s390x-unknown-linux-gnu": "python", - "riscv64-unknown-linux-gnu": "python", - "x86_64-apple-darwin": "python", - "x86_64-pc-windows-msvc": "python", - "x86_64-unknown-linux-gnu": "python", - "x86_64-unknown-linux-musl": "python", - "aarch64-apple-darwin-freethreaded": "python/install", - "aarch64-unknown-linux-gnu-freethreaded": "python/install", - "ppc64le-unknown-linux-gnu-freethreaded": "python/install", - "riscv64-unknown-linux-gnu-freethreaded": "python/install", - "s390x-unknown-linux-gnu-freethreaded": "python/install", - "x86_64-apple-darwin-freethreaded": "python/install", - "x86_64-pc-windows-msvc-freethreaded": "python/install", - "x86_64-unknown-linux-gnu-freethreaded": "python/install", - }, - }, - "3.13.4": { - "url": "20250610/cpython-{python_version}+20250610-{platform}-{build}.{ext}", - "sha256": { - "aarch64-apple-darwin": "c2ce6601b2668c7bd1f799986af5ddfbff36e88795741864aba6e578cb02ed7f", - "aarch64-unknown-linux-gnu": "3c2596ece08ffe17e11bc1f27aeb4ce1195d2490a83d695d36ef4933d5c5ca53", - "ppc64le-unknown-linux-gnu": "b3cc13ee177b8db1d3e9b2eac413484e3c6a356f97d91dc59de8d3fd8cf79d6b", - "riscv64-unknown-linux-gnu": "d1b989e57a9ce29f6c945eeffe0e9750c222fdd09e99d2f8d6b0d8532a523053", - "s390x-unknown-linux-gnu": "d1d19fb01961ac6476712fdd6c5031f74c83666f6f11aa066207e9a158f7e3d8", - "x86_64-apple-darwin": "79feb6ca68f3921d07af52d9db06cf134e6f36916941ea850ab0bc20f5ff638b", - "x86_64-pc-windows-msvc": "29ac3585cc2dcfd79e3fe380c272d00e9d34351fc456e149403c86d3fea34057", - "x86_64-unknown-linux-gnu": "44e5477333ebca298a7a0a316985c6c3533b8645f92a83f7f73c44033832bf32", - "x86_64-unknown-linux-musl": "a3afbfa94b9ff4d9fc426b47eb3c8446cada535075b8d51b7bdc9d9ab9911fc2", - "aarch64-apple-darwin-freethreaded": "278dccade56b4bbeecb9a613b77012cf5c1433a5e9b8ef99230d5e61f31d9e02", - "aarch64-unknown-linux-gnu-freethreaded": "b1c1bd6ab9ef95b464d92a6a911cef1a8d9f0b0f6a192f694ef18ed15d882edf", - "ppc64le-unknown-linux-gnu-freethreaded": "ed66ae213a62b286b9b7338b816ccd2815f5248b7a28a185dc8159fe004149ae", - "riscv64-unknown-linux-gnu-freethreaded": "913264545215236660e4178bc3e5b57a20a444a8deb5c11680c95afc960b4016", - "s390x-unknown-linux-gnu-freethreaded": "7556a38ab5e507c1ec22bc38f9859982bc956cab7f4de05a2faac114feb306db", - "x86_64-apple-darwin-freethreaded": "64ab7ac8c88002d9ba20a92f72945bfa350268e944a7922500af75d20330574d", - "x86_64-pc-windows-msvc-freethreaded": "9457504547edb2e0156bf76b53c7e4941c7f61c0eff9fd5f4d816d3df51c58e3", - "x86_64-unknown-linux-gnu-freethreaded": "864df6e6819e8f8e855ce30f34410fdc5867d0616e904daeb9a40e5806e970d7", - }, - "strip_prefix": { - "aarch64-apple-darwin": "python", - "aarch64-unknown-linux-gnu": "python", - "ppc64le-unknown-linux-gnu": "python", - "s390x-unknown-linux-gnu": "python", - "riscv64-unknown-linux-gnu": "python", - "x86_64-apple-darwin": "python", - "x86_64-pc-windows-msvc": "python", - "x86_64-unknown-linux-gnu": "python", - "x86_64-unknown-linux-musl": "python", - "aarch64-apple-darwin-freethreaded": "python/install", - "aarch64-unknown-linux-gnu-freethreaded": "python/install", - "ppc64le-unknown-linux-gnu-freethreaded": "python/install", - "riscv64-unknown-linux-gnu-freethreaded": "python/install", - "s390x-unknown-linux-gnu-freethreaded": "python/install", - "x86_64-apple-darwin-freethreaded": "python/install", - "x86_64-pc-windows-msvc-freethreaded": "python/install", - "x86_64-unknown-linux-gnu-freethreaded": "python/install", - }, - }, - "3.13.6": { - "url": "20250808/cpython-{python_version}+20250808-{platform}-{build}.{ext}", - "sha256": { - "aarch64-apple-darwin": "8a1efa6af4e80f08e2c97dda822a3d6c24d6c98e518242f802c6a43ae8401488", - "aarch64-unknown-linux-gnu": "11fa0591ae2211c08a42ae54944260e36ddf88a1d5604ea0c49e2477be4e5388", - "ppc64le-unknown-linux-gnu": "8dcf34ae1a685fe1893b52917ae04f23328edadc4acae28499d43850c2bdd26c", - "riscv64-unknown-linux-gnu": "f8ed75aa6cc2011a046be00b629c3c8295267f34280324feaff34c73e7afce39", - "s390x-unknown-linux-gnu": "7707ee5d19a78bc64ef8a66751ec7f97b64ea06714c7b1b52e8b321c2923ead8", - "x86_64-apple-darwin": "27badce7201321a8363219e438a6205165e5b4884012b1046532203df2ec9379", - "x86_64-pc-windows-msvc": "af5cc733c33b9aa9f1d74c81a59351e9b27215486d8b6cdbc06d97646a58c953", - "aarch64-pc-windows-msvc": "8e1617bd407ec1a874499daab26ae95080d1e0267ae616d34490137a28705827", - "aarch64-pc-windows-msvc-freethreaded": "552cfabcc3b103f4b1c4036d2592d5f0373c9554a2c4d2b6631b04ef7e592067", - "x86_64-unknown-linux-gnu": "f844e8c8b6847628b472f7e97d8893a4e93acd5382a902b465776063668c4d64", - "x86_64-unknown-linux-musl": "70076dea0ff65b3c05aae1a97b4a556bf613cc73db30309e59134f9d318f4f7b", - "aarch64-apple-darwin-freethreaded": "f2143304012e021a603bf1807bf3e4ce163832e43ab9a9829e53cb136497f207", - "aarch64-unknown-linux-gnu-freethreaded": "d84a7d64c284be387386b9f5da273f6d05486eb6bd8f9e86e2575cb59604cb22", - "ppc64le-unknown-linux-gnu-freethreaded": "e76fcaf1bf80a615520dbe7f85ca0bb557fad96d132d836b0ac721e7cc1e2a37", - "riscv64-unknown-linux-gnu-freethreaded": "24e08a39ba4fc77753e61541e52eed39cc871f4a92a80a3c5dd495056bd8eff9", - "s390x-unknown-linux-gnu-freethreaded": "1609b223fd38a4a7a4d20e7173d7d9390fe2258f7dd9a15dc9ef0fa49613735d", - "x86_64-apple-darwin-freethreaded": "4360a1278dd0a96b526d108c8fd23498a9d2028dd7791e510fd51ff5ea3f462a", - "x86_64-pc-windows-msvc-freethreaded": "4e727cdbe4057b16a170f887c0fa4227a825ac59bcda84ae946c77cc932af78c", - "x86_64-unknown-linux-gnu-freethreaded": "e48c13c59cc3c01b79f63c8bccec27d2db6e97f64213b8731e2077b6ed8ed52c", - }, - "strip_prefix": { - "aarch64-apple-darwin": "python", - "aarch64-unknown-linux-gnu": "python", - "ppc64le-unknown-linux-gnu": "python", - "s390x-unknown-linux-gnu": "python", - "riscv64-unknown-linux-gnu": "python", - "x86_64-apple-darwin": "python", - "x86_64-pc-windows-msvc": "python", - "aarch64-pc-windows-msvc": "python", - "x86_64-unknown-linux-gnu": "python", - "x86_64-unknown-linux-musl": "python", - "aarch64-apple-darwin-freethreaded": "python/install", - "aarch64-unknown-linux-gnu-freethreaded": "python/install", - "ppc64le-unknown-linux-gnu-freethreaded": "python/install", - "riscv64-unknown-linux-gnu-freethreaded": "python/install", - "s390x-unknown-linux-gnu-freethreaded": "python/install", - "x86_64-apple-darwin-freethreaded": "python/install", - "x86_64-pc-windows-msvc-freethreaded": "python/install", - "aarch64-pc-windows-msvc-freethreaded": "python/install", - "x86_64-unknown-linux-gnu-freethreaded": "python/install", - }, - }, - "3.14.0rc1": { - "url": "20250808/cpython-{python_version}+20250808-{platform}-{build}.{ext}", - "sha256": { - "aarch64-apple-darwin": "016b9eb7c6c41d358a095f52203297812a566376b1e4372571b850f621dc720d", - "aarch64-unknown-linux-gnu": "bfa5cb2f56032f4ed2c105f5b3b59ea1809672cb74b453e4450399517a594137", - "ppc64le-unknown-linux-gnu": "eb8fade967732032be70d5129ed66ad28dfe57e2964550f0f6800dddc1fdcb80", - "riscv64-unknown-linux-gnu": "82313ee3c45ad0dc825044fef161840dd60277003bf5458da24d46206fff1e09", - "s390x-unknown-linux-gnu": "5af30600105c42e920e9709afc8deae07c309cd46959c22dc099a1fb45b73902", - "x86_64-apple-darwin": "a74da55830354eb13c5e1fd12bb9b0b624ed0daeafec48444eda86b21476e4c8", - "x86_64-pc-windows-msvc": "71d7fe086604835e5ebd38b45829c1e7ce38fb8f5399d287d219f930cd6efdc1", - "aarch64-pc-windows-msvc": "631c007e1b90dfc9f22d05d4f4e5ef9f70bfa01b675a2bd8590994369746f852", - "x86_64-unknown-linux-gnu": "644028c49cdd9d082274f7265857bc5b5bb4eea8c3e58187e5b3ebb74de9ad3a", - "x86_64-unknown-linux-musl": "af82c7e3ddaebbfb10967c5dd9751bc2fa7f735806a16ebde7600e519c34e587", - "aarch64-apple-darwin-freethreaded": "d611134bcf090db920d8192ec26e899433cdbae42476fd92ee07cc13b5e32d1f", - "aarch64-unknown-linux-gnu-freethreaded": "9fcbb8947c07421506187b3f605f34e94c68824d3fd362d84cd1dcdf13285ee3", - "ppc64le-unknown-linux-gnu-freethreaded": "8cb6937fb0804ca0d5d867af15b6e7fd05d79d0faa5a6e617e6f6280580a5f66", - "riscv64-unknown-linux-gnu-freethreaded": "7a235a6f5b814f5ae789fea65b097ad52f840619dfd054135d8ae8443fa8e362", - "s390x-unknown-linux-gnu-freethreaded": "fee35437df67782b348d57b32cb1acac61384504af28860d552b0d6aeb3ae19e", - "x86_64-apple-darwin-freethreaded": "07deb66e52c91c69e15a3d644ff1527dbd9e278c389ed58341af10872dc15ab5", - "x86_64-pc-windows-msvc-freethreaded": "7a4cdb4c213a2f486b5d6b1044970f6529b7e5365a2d5503ffa8224e62da9ccf", - "aarch64-pc-windows-msvc-freethreaded": "aa9f871afc67419e867535eb0d368e5ec7828138799985d21448443ff1185087", - "x86_64-unknown-linux-gnu-freethreaded": "b6237adf6cf3b8ae00238936d61045d33a6b45147e6540a91ae6e6696aeff23c", - }, - "strip_prefix": { - "aarch64-apple-darwin": "python", - "aarch64-unknown-linux-gnu": "python", - "ppc64le-unknown-linux-gnu": "python", - "s390x-unknown-linux-gnu": "python", - "riscv64-unknown-linux-gnu": "python", - "x86_64-apple-darwin": "python", - "x86_64-pc-windows-msvc": "python", - "aarch64-pc-windows-msvc": "python", - "x86_64-unknown-linux-gnu": "python", - "x86_64-unknown-linux-musl": "python", - "aarch64-apple-darwin-freethreaded": "python/install", - "aarch64-unknown-linux-gnu-freethreaded": "python/install", - "ppc64le-unknown-linux-gnu-freethreaded": "python/install", - "riscv64-unknown-linux-gnu-freethreaded": "python/install", - "s390x-unknown-linux-gnu-freethreaded": "python/install", - "x86_64-apple-darwin-freethreaded": "python/install", - "x86_64-pc-windows-msvc-freethreaded": "python/install", - "aarch64-pc-windows-msvc-freethreaded": "python/install", - "x86_64-unknown-linux-gnu-freethreaded": "python/install", - }, - }, -} +DEFAULT_RELEASE_BASE_URL = _GITHUB_PREFIX +DEFAULT_RELEASE_BASE_URLS = [ + _GITHUB_PREFIX, + _ASTRAL_PREFIX, + _LEGACY_GITHUB_PREFIX, +] # buildifier: disable=unsorted-dict-items MINOR_MAPPING = { - "3.8": "3.8.20", - "3.9": "3.9.23", - "3.10": "3.10.18", - "3.11": "3.11.13", - "3.12": "3.12.11", - "3.13": "3.13.6", - "3.14": "3.14.0rc1", + "3.9": "3.9.25", + "3.10": "3.10.20", + "3.11": "3.11.15", + "3.12": "3.12.13", + "3.13": "3.13.13", + "3.14": "3.14.4", + "3.15": "3.15.0a8", } def _generate_platforms(): @@ -900,6 +84,14 @@ def _generate_platforms(): os_name = LINUX_NAME, arch = "aarch64", ), + "arm64e-apple-darwin": platform_info( + compatible_with = [ + "@platforms//os:macos", + "@platforms//cpu:arm64e", + ], + os_name = MACOS_NAME, + arch = "aarch64", + ), "armv7-unknown-linux-gnu": platform_info( compatible_with = [ "@platforms//os:linux", @@ -1015,18 +207,41 @@ def _generate_platforms(): PLATFORMS = _generate_platforms() -def get_release_info(platform, python_version, base_url = DEFAULT_RELEASE_BASE_URL, tool_versions = TOOL_VERSIONS): +def get_release_info(platform, python_version, base_urls = DEFAULT_RELEASE_BASE_URLS, tool_versions = None): """Resolve the release URL for the requested interpreter version Args: platform: The platform string for the interpreter python_version: The version of the interpreter to get - base_url: The URL to prepend to the 'url' attr in the tool_versions dict + base_urls: The list of URLs to prepend to the 'url' attr in the tool_versions dict tool_versions: A dict listing the interpreter versions, their SHAs and URL Returns: A tuple of (filename, url, archive strip prefix, patches, patch_strip) """ + if tool_versions == None: + tool_versions = TOOL_VERSIONS + + if type(base_urls) == type(""): + base_urls = [base_urls] + elif base_urls == None: + base_urls = DEFAULT_RELEASE_BASE_URLS + + expanded_base_urls = [] + for b_url in base_urls: + if b_url not in expanded_base_urls: + expanded_base_urls.append(b_url) + if b_url.startswith(_GITHUB_PREFIX): + suffix = b_url[len(_GITHUB_PREFIX):] + a_url = _ASTRAL_PREFIX + suffix + if a_url not in expanded_base_urls: + expanded_base_urls.append(a_url) + elif b_url.startswith(_LEGACY_GITHUB_PREFIX): + suffix = b_url[len(_LEGACY_GITHUB_PREFIX):] + a_url = _ASTRAL_PREFIX + suffix + if a_url not in expanded_base_urls: + expanded_base_urls.append(a_url) + base_urls = expanded_base_urls url = tool_versions[python_version]["url"] @@ -1045,9 +260,14 @@ def get_release_info(platform, python_version, base_url = DEFAULT_RELEASE_BASE_U for u in url: p, _, _ = platform.partition(FREETHREADED) - release_id = int(u.split("/")[-2]) + # Assume an unknown release_id is a newer url format + release_id = 99999999 + url_parts = u.split("/") + if len(url_parts) >= 2 and url_parts[-2].isdigit(): + maybe_release_id = url_parts[-2] + release_id = int(maybe_release_id) - if FREETHREADED.lstrip("-") in platform: + if FREETHREADED.lstrip("-") in platform and release_id < 20260325: build = "{}+{}-full".format( FREETHREADED.lstrip("-"), { @@ -1060,6 +280,7 @@ def get_release_info(platform, python_version, base_url = DEFAULT_RELEASE_BASE_U "x86_64-apple-darwin": "pgo+lto", "x86_64-pc-windows-msvc": "pgo", "x86_64-unknown-linux-gnu": "pgo+lto", + "x86_64-unknown-linux-musl": "pgo+lto", }[p], ) else: @@ -1077,7 +298,8 @@ def get_release_info(platform, python_version, base_url = DEFAULT_RELEASE_BASE_U if "://" in release_filename: # is absolute url? rendered_urls.append(release_filename) else: - rendered_urls.append("/".join([base_url, release_filename])) + for b_url in base_urls: + rendered_urls.append("/".join([b_url, release_filename])) if release_filename == None: fail("release_filename should be set by now; were any download URLs given?") @@ -1104,3 +326,67 @@ def gen_python_config_settings(name = ""): flag_values = PLATFORMS[platform].flag_values, constraint_values = PLATFORMS[platform].compatible_with, ) + +def _manifest_entry_sort_key(entry): + flavor_rank = {"full": 3, "install_only": 1, "install_only_stripped": 2}.get(entry.archive_flavor, 4) + microarch = entry.microarch + if not microarch: + microarch_rank = 0 + elif microarch.startswith("v") and microarch[1:].isdigit(): + microarch_rank = int(microarch[1:]) + else: + microarch_rank = 999 + return (flavor_rank, microarch_rank) + +def _tool_versions_from_manifest_entries(entries, base_url = DEFAULT_RELEASE_BASE_URL): + """Converts parsed manifest entries into the TOOL_VERSIONS dictionary format. + + Args: + entries: {type}`list[struct]` the parsed manifest entries. + base_url: {type}`str` fallback base URL for standalone distributions. + + Returns: + {type}`dict[str, dict]` the tool versions map. + """ + available_versions = {} + entries = sorted( + entries, + key = _manifest_entry_sort_key, + ) + + for entry in entries: + location = entry.location + sha256 = entry.sha256 + py_version = entry.python_version + + matched_platform = "{}-{}-{}".format(entry.arch, entry.vendor, entry.os) + if entry.libc: + matched_platform += "-" + entry.libc + if entry.freethreaded: + matched_platform += "-freethreaded" + + if matched_platform not in PLATFORMS: + continue + + archive_flavor = entry.archive_flavor + if archive_flavor not in ["install_only", "install_only_stripped", "full"]: + continue + + v_dict = available_versions.setdefault(py_version, {}) + if matched_platform in v_dict.get("sha256", {}): + continue + + if "://" in location: + urls = [location] + else: + urls = ["{}/{}".format(base_url, location)] + + strip_prefix = "python/install" if archive_flavor == "full" else "python" + + v_dict.setdefault("sha256", {})[matched_platform] = sha256 + v_dict.setdefault("url", {})[matched_platform] = urls + v_dict.setdefault("strip_prefix", {})[matched_platform] = strip_prefix + + return available_versions + +TOOL_VERSIONS = _tool_versions_from_manifest_entries(parse_runtime_manifest(MANIFEST_TEXT)) diff --git a/python/zipapp/BUILD.bazel b/python/zipapp/BUILD.bazel new file mode 100644 index 0000000000..77a8752701 --- /dev/null +++ b/python/zipapp/BUILD.bazel @@ -0,0 +1,30 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +filegroup( + name = "distribution", + srcs = glob(["**"]), + visibility = ["//python:__pkg__"], +) + +bzl_library( + name = "py_zipapp_binary", + srcs = ["py_zipapp_binary.bzl"], + deps = [ + "//python/private:util", + "//python/private/zipapp:py_zipapp_rule", + ], +) + +# keep +bzl_library( + name = "py_zipapp_test", + srcs = ["py_zipapp_test.bzl"], + deps = [ + "//python/private:util", + "//python/private/zipapp:py_zipapp_rule", + ], +) diff --git a/python/zipapp/py_zipapp_binary.bzl b/python/zipapp/py_zipapp_binary.bzl new file mode 100644 index 0000000000..0b9c9bf95c --- /dev/null +++ b/python/zipapp/py_zipapp_binary.bzl @@ -0,0 +1,24 @@ +"""`py_zipapp_binary` macro. + +:::{seealso} + +{obj}`features.zipapp_rules` to detect if this rule is available. +::: +""" + +load("//python/private:util.bzl", "add_tag") +load("//python/private/zipapp:py_zipapp_rule.bzl", _py_zipapp_binary_rule = "py_zipapp_binary") + +def py_zipapp_binary(**kwargs): + """Builds a Python zipapp from a py_binary/py_test target. + + :::{versionadded} 1.9.0 + ::: + + Args: + **kwargs: Args passed onto {rule}`py_zipapp_binary`. + """ + add_tag(kwargs, "@rules_python//python:py_zipapp_binary") + _py_zipapp_binary_rule( + **kwargs + ) diff --git a/python/zipapp/py_zipapp_test.bzl b/python/zipapp/py_zipapp_test.bzl new file mode 100644 index 0000000000..261e584755 --- /dev/null +++ b/python/zipapp/py_zipapp_test.bzl @@ -0,0 +1,24 @@ +"""`py_zipapp_test` macro. + +:::{seealso} + +{obj}`features.zipapp_rules` to detect if this rule is available. +::: +""" + +load("//python/private:util.bzl", "add_tag") +load("//python/private/zipapp:py_zipapp_rule.bzl", _py_zipapp_test = "py_zipapp_test") + +def py_zipapp_test(**kwargs): + """Builds a Python zipapp from a py_binary/py_test target. + + :::{versionadded} 1.9.0 + ::: + + Args: + **kwargs: Args passed onto {rule}`py_zipapp_test`. + """ + add_tag(kwargs, "@rules_python//python:py_zipapp_test") + _py_zipapp_test( + **kwargs + ) diff --git a/replicate_ci b/replicate_ci new file mode 100755 index 0000000000..dd9b67e93e --- /dev/null +++ b/replicate_ci @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 + +import argparse +import os +import shlex +import subprocess +import sys + +import yaml + + +def parse_args(): + parser = argparse.ArgumentParser( + description="Replicate and emulate BazelCI job configurations from presubmit.yml." + ) + parser.add_argument( + "job", + help="The key or name of the CI job to emulate (e.g., ubuntu_workspace).", + ) + return parser.parse_args() + + +def run_cmd(cmd, cwd=None, env=None, shell=False): + if shell: + cmd_str = cmd if isinstance(cmd, str) else " ".join(cmd) + else: + cmd_str = shlex.join(cmd) if isinstance(cmd, list) else str(cmd) + + print(f"\nšŸš€ Executing: {cmd_str}") + if cwd and cwd != os.getcwd(): + print(f"šŸ“ Directory: {cwd}") + if env and "USE_BAZEL_VERSION" in env: + print(f"šŸ”§ Bazel Version: {env['USE_BAZEL_VERSION']}") + + res = subprocess.run(cmd, cwd=cwd, env=env, shell=shell) + if res.returncode != 0: + print( + f"\nāŒ Command failed with return code {res.returncode}: {cmd_str}", + file=sys.stderr, + ) + return False + return True + + +def resolve_bazel_version(task_bazel): + if not task_bazel or task_bazel.startswith("${{"): + return None + return task_bazel + + +def execute_ci_job(job_key, task, repo_root): + job_name = task.get("name", job_key) + print(f"\n{'=' * 80}\nšŸŽÆ Replicating CI Job: {job_key} ('{job_name}')\n{'=' * 80}") + + # Setup working directory + cwd = repo_root + if "working_directory" in task: + cwd = os.path.join(repo_root, task["working_directory"]) + if not os.path.exists(cwd): + print( + f"āŒ Error: working_directory '{task['working_directory']}' does not exist at '{cwd}'", + file=sys.stderr, + ) + return False + + # Setup environment + env = os.environ.copy() + bzl_version = resolve_bazel_version(task.get("bazel")) + if bzl_version: + env["USE_BAZEL_VERSION"] = bzl_version + + # Execute pre-commands + is_windows = sys.platform.startswith("win") + pre_cmds = task.get("batch_commands" if is_windows else "shell_commands", []) + for pre_cmd in pre_cmds: + if not run_cmd(pre_cmd, cwd=cwd, env=env, shell=True): + return False + + # Execute Build Targets + build_targets = [t for t in task.get("build_targets", []) if t != "--"] + if build_targets: + build_flags = task.get("build_flags", []) + cmd = ["bazel", "build"] + build_flags + ["--"] + build_targets + if not run_cmd(cmd, cwd=cwd, env=env): + return False + + # Execute Test Targets + test_targets = [t for t in task.get("test_targets", []) if t != "--"] + if test_targets: + test_flags = task.get("test_flags", []) + if "--build_tests_only" not in test_flags: + test_flags = ["--build_tests_only"] + test_flags + cmd = ["bazel", "test"] + test_flags + ["--"] + test_targets + if not run_cmd(cmd, cwd=cwd, env=env): + return False + + # Execute Coverage Targets + coverage_targets = [t for t in task.get("coverage_targets", []) if t != "--"] + if coverage_targets: + coverage_flags = task.get("test_flags", []) + cmd = ["bazel", "coverage"] + coverage_flags + ["--"] + coverage_targets + if not run_cmd(cmd, cwd=cwd, env=env): + return False + + print(f"\nšŸŽ‰ Successfully replicated CI Job: {job_key}") + return True + + +def main(): + args = parse_args() + + repo_root = os.path.abspath(os.path.dirname(__file__)) + presubmit_path = os.path.join(repo_root, ".bazelci/presubmit.yml") + if not os.path.exists(presubmit_path): + print( + f"āŒ Error: Presubmit file not found at '{presubmit_path}'", + file=sys.stderr, + ) + sys.exit(1) + + with open(presubmit_path) as f: + presubmit = yaml.safe_load(f) + + tasks = presubmit.get("tasks", {}) + if not tasks: + print( + f"āŒ Error: No tasks found in '{presubmit_path}'", + file=sys.stderr, + ) + sys.exit(1) + + # If no job specified, print available jobs and exit + if not args.job: + print("āŒ Error: No CI job specified. Provide a job key.\n", file=sys.stderr) + print("šŸ“‹ Available CI Job Keys:", file=sys.stderr) + for key in sorted(tasks.keys()): + name = tasks[key].get("name", key) + print(f" • {key} ({name})", file=sys.stderr) + sys.exit(1) + + # Match by key or by name + job_key = None + if args.job in tasks: + job_key = args.job + else: + for key, config in tasks.items(): + if config.get("name") == args.job: + job_key = key + break + + if not job_key: + print( + f"āŒ Error: CI job '{args.job}' not found in '{presubmit_path}'\n", + file=sys.stderr, + ) + print("šŸ“‹ Available CI Job Keys:", file=sys.stderr) + for key in sorted(tasks.keys()): + name = tasks[key].get("name", key) + print(f" • {key} ({name})", file=sys.stderr) + sys.exit(1) + + success = execute_ci_job(job_key, tasks[job_key], repo_root) + sys.exit(0 if success else 1) + + +if __name__ == "__main__": + main() diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000000..fc29594bad --- /dev/null +++ b/ruff.toml @@ -0,0 +1,29 @@ +# ruff.toml + +# Support Python 3.8 and above +target-version = "py38" + +# Match Black's default line length +line-length = 88 + +# Ignore testdata files +extend-exclude = ["**/testdata/**", "testdata"] + +[lint] +# Enable Pyflakes (F) and pycodestyle (E, W) for standard linting, +# and isort (I) for import sorting +select = ["F", "E", "W", "I"] + +# Like Black, allow certain violations that may conflict with its formatting +ignore = ["E501"] + +[lint.isort] +# Matches isort's profile = "black" behavior +combine-as-imports = true +force-single-line = false + +[format] +# The Ruff formatter defaults to Black's style +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false diff --git a/specialized_configs.bazelrc b/specialized_configs.bazelrc new file mode 100644 index 0000000000..46334e0a95 --- /dev/null +++ b/specialized_configs.bazelrc @@ -0,0 +1,12 @@ + +# Helper config to run most tests without waiting an inordinate amount +# of time or freezing the system +common:fast-tests --build_tests_only=true +common:fast-tests --build_tag_filters=-large,-enormous,-integration-test +common:fast-tests --test_tag_filters=-large,-enormous,-integration-test + +# Helper config for running a single test locally and investigating resulting state +common:testone --test_output=streamed +common:testone --test_strategy=standalone +common:testone --spawn_strategy=standalone +common:testone --strategy=standalone diff --git a/sphinxdocs/.bazelignore b/sphinxdocs/.bazelignore new file mode 100644 index 0000000000..80f2d3514f --- /dev/null +++ b/sphinxdocs/.bazelignore @@ -0,0 +1,4 @@ +bazel-bin +bazel-out +bazel-testlogs +bazel-sphinxdocs diff --git a/sphinxdocs/.bazelrc b/sphinxdocs/.bazelrc new file mode 100644 index 0000000000..ce4d782113 --- /dev/null +++ b/sphinxdocs/.bazelrc @@ -0,0 +1,33 @@ +import %workspace%/.bazelrc.deleted_packages + +test --test_output=errors + +build --incompatible_default_to_explicit_init_py +build --@rules_python//python/config_settings:incompatible_default_to_explicit_init_py=True + +# Ensure ongoing compatibility with this flag. +common --incompatible_disallow_struct_provider_syntax +# Makes Bazel 7 act more like Bazel 8 +common --incompatible_use_plus_in_repo_names + +# Explicitly enable for Windows +build --enable_runfiles + +# Local disk cache greatly speeds up builds if the regular cache is lost +common --disk_cache=~/.cache/bazel/bazel-disk-cache +common --experimental_downloader_config=downloader_config.cfg +common --http_timeout_scaling=10.0 +common --experimental_repository_downloader_retries=10 + +common --incompatible_python_disallow_native_rules +common --incompatible_no_implicit_file_export +# Pass host PATH to build and test actions so bazel_from_env (used by rules_bazel_integration_test +# for local 'self' testing) can locate the user's bazel binary without disabling strict action env. +common --action_env=PATH +common --test_env=PATH + +build --lockfile_mode=update + +common:fast-tests --build_tests_only=true +common:fast-tests --build_tag_filters=-large,-enormous,-integration-test +common:fast-tests --test_tag_filters=-large,-enormous,-integration-test diff --git a/sphinxdocs/.bazelrc.deleted_packages b/sphinxdocs/.bazelrc.deleted_packages new file mode 100644 index 0000000000..9a90ef21be --- /dev/null +++ b/sphinxdocs/.bazelrc.deleted_packages @@ -0,0 +1,2 @@ +common --deleted_packages=integration_tests/bcr +common --deleted_packages=integration_tests/persistent_worker/workspace diff --git a/sphinxdocs/.bazelversion b/sphinxdocs/.bazelversion new file mode 100644 index 0000000000..512e4c889e --- /dev/null +++ b/sphinxdocs/.bazelversion @@ -0,0 +1 @@ +9.x diff --git a/sphinxdocs/BUILD.bazel b/sphinxdocs/BUILD.bazel index 9ad1e1eef9..8ff9bc956f 100644 --- a/sphinxdocs/BUILD.bazel +++ b/sphinxdocs/BUILD.bazel @@ -1,66 +1,16 @@ -# Copyright 2023 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//:bzl_library.bzl", "bzl_library") -load("@bazel_skylib//rules:common_settings.bzl", "bool_flag") -load("//sphinxdocs/private:sphinx.bzl", "repeated_string_list_flag") - -package( - default_visibility = ["//:__subpackages__"], -) - -# Additional -D values to add to every Sphinx build. -# This is usually used to override the version when building -repeated_string_list_flag( - name = "extra_defines", - build_setting_default = [], -) - -repeated_string_list_flag( - name = "extra_env", - build_setting_default = [], -) - -# Whether to add the `-q` arg to Sphinx invocations, which determines if -# stdout has any output or not (logging INFO messages and progress messages). -# If true, add `-q`. If false, don't add `-q`. This is mostly useful for -# debugging invocations or developing extensions. -bool_flag( - name = "quiet", - build_setting_default = True, -) - -bzl_library( - name = "sphinx_bzl", - srcs = ["sphinx.bzl"], - deps = ["//sphinxdocs/private:sphinx_bzl"], -) - -bzl_library( - name = "sphinx_docs_library_bzl", - srcs = ["sphinx_docs_library.bzl"], - deps = ["//sphinxdocs/private:sphinx_docs_library_macro_bzl"], -) - -bzl_library( - name = "sphinx_stardoc_bzl", - srcs = ["sphinx_stardoc.bzl"], - deps = ["//sphinxdocs/private:sphinx_stardoc_bzl"], -) - -bzl_library( - name = "readthedocs_bzl", - srcs = ["readthedocs.bzl"], - deps = ["//sphinxdocs/private:readthedocs_bzl"], +package(default_visibility = ["//visibility:public"]) + +filegroup( + name = "distribution", + srcs = glob( + ["**/*"], + exclude = [ + "bazel-*/**", + "integration_tests/**", + "tests/**", + ], + ) + [ + "//sphinxdocs:distribution", + ], + visibility = ["//visibility:public"], ) diff --git a/sphinxdocs/MODULE.bazel b/sphinxdocs/MODULE.bazel new file mode 100644 index 0000000000..3d4caf2bb2 --- /dev/null +++ b/sphinxdocs/MODULE.bazel @@ -0,0 +1,41 @@ +module( + name = "sphinxdocs", + version = "0.0.0", + compatibility_level = 1, +) + +bazel_dep(name = "bazel_skylib", version = "1.8.2") +bazel_dep(name = "stardoc", version = "0.7.2", repo_name = "io_bazel_stardoc") +bazel_dep(name = "platforms", version = "0.0.11") +bazel_dep(name = "protobuf", version = "29.0-rc2", repo_name = "com_google_protobuf") +bazel_dep(name = "rules_python", version = "1.8.5") + +dev_pip = use_extension( + "@rules_python//python/extensions:pip.bzl", + "pip", + dev_dependency = True, +) +dev_pip.parse( + hub_name = "dev_pip", + python_version = "3.11", + requirements_lock = "@rules_python//docs:requirements.txt", +) +use_repo(dev_pip, "dev_pip") + +bazel_dep(name = "rules_bazel_integration_test", version = "0.37.1", dev_dependency = True) + +bazel_binaries = use_extension( + "@rules_bazel_integration_test//:extensions.bzl", + "bazel_binaries", + dev_dependency = True, +) +bazel_binaries.local( + name = "self", + path = "integration_tests/bazel_from_env", +) +use_repo( + bazel_binaries, + "bazel_binaries", + "bazel_binaries_bazelisk", + "build_bazel_bazel_self", +) diff --git a/sphinxdocs/docs/BUILD.bazel b/sphinxdocs/docs/BUILD.bazel index 070e0485d7..58c9e7fc18 100644 --- a/sphinxdocs/docs/BUILD.bazel +++ b/sphinxdocs/docs/BUILD.bazel @@ -1,8 +1,8 @@ -load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility load("//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library") load("//sphinxdocs:sphinx_stardoc.bzl", "sphinx_stardocs") +load("//sphinxdocs/private:util.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility -package(default_visibility = ["//:__subpackages__"]) +package(default_visibility = ["//sphinxdocs:__subpackages__"]) # We only build for Linux and Mac because: # 1. The actual doc process only runs on Linux @@ -18,6 +18,7 @@ _TARGET_COMPATIBLE_WITH = select({ sphinx_docs_library( name = "docs_lib", + visibility = ["//visibility:public"], deps = [ ":artisian_api_docs", ":bzl_docs", @@ -47,11 +48,11 @@ sphinx_docs_library( sphinx_stardocs( name = "bzl_docs", srcs = [ - "//sphinxdocs:readthedocs_bzl", - "//sphinxdocs:sphinx_bzl", - "//sphinxdocs:sphinx_docs_library_bzl", - "//sphinxdocs:sphinx_stardoc_bzl", - "//sphinxdocs/private:sphinx_docs_library_bzl", + "//sphinxdocs:readthedocs", + "//sphinxdocs:sphinx", + "//sphinxdocs:sphinx_docs_library", + "//sphinxdocs:sphinx_stardoc", + "//sphinxdocs/private:sphinx_docs_library", ], prefix = "api/sphinxdocs/", target_compatible_with = _TARGET_COMPATIBLE_WITH, diff --git a/sphinxdocs/docs/api/sphinxdocs/index.md b/sphinxdocs/docs/api/sphinxdocs/index.md index bd4e9b6eec..a59edb2b65 100644 --- a/sphinxdocs/docs/api/sphinxdocs/index.md +++ b/sphinxdocs/docs/api/sphinxdocs/index.md @@ -1,7 +1,7 @@ -:::{bzl:currentfile} //sphinxdocs:BUILD.bazel +:::{bzl:currentfile} //:BUILD.bazel ::: -# //sphinxdocs +# // :::{bzl:flag} extra_defines Additional `-D` values to add to every Sphinx build. diff --git a/sphinxdocs/docs/api/sphinxdocs/inventories/index.md b/sphinxdocs/docs/api/sphinxdocs/inventories/index.md index a03645ed44..d0b983fe7f 100644 --- a/sphinxdocs/docs/api/sphinxdocs/inventories/index.md +++ b/sphinxdocs/docs/api/sphinxdocs/inventories/index.md @@ -1,7 +1,7 @@ -:::{bzl:currentfile} //sphinxdocs/inventories:BUILD.bazel +:::{bzl:currentfile} //inventories:BUILD.bazel ::: -# //sphinxdocs/inventories +# //inventories :::{bzl:target} bazel_inventory A Sphinx inventory of Bazel objects. diff --git a/sphinxdocs/docs/index.md b/sphinxdocs/docs/index.md index 2ea1146e1b..43bbc14f60 100644 --- a/sphinxdocs/docs/index.md +++ b/sphinxdocs/docs/index.md @@ -11,7 +11,7 @@ documentation. It comes with: While it is primarily oriented towards docgen for Starlark code, the core of it is agnostic as to what is being documented. -### Optimization +## Optimization Normally, Sphinx keeps various cache files to improve incremental building. Unfortunately, programs performing their own caching don't interact well diff --git a/sphinxdocs/docs/readthedocs.md b/sphinxdocs/docs/readthedocs.md index c347d19850..bcdae833d9 100644 --- a/sphinxdocs/docs/readthedocs.md +++ b/sphinxdocs/docs/readthedocs.md @@ -26,8 +26,8 @@ In the example below, `npm` is used to install Bazelisk and a helper shell script, `readthedocs_build.sh` is used to construct the Bazel invocation. The key purpose of the shell script it to set the -`--@rules_python//sphinxdocs:extra_env` and -`--@rules_python//sphinxdocs:extra_defines` flags. These are used to communicate +`@sphinxdocs//sphinxdocs::extra_env` and +`@sphinxdocs//sphinxdocs::extra_defines` flags. These are used to communicate `READTHEDOCS*` environment variables and settings to the Bazel invocation. ## BUILD config @@ -73,7 +73,7 @@ build: ``` # File: docs/BUILD -load("@rules_python//sphinxdocs:readthedocs.bzl.bzl", "readthedocs_install") +load("//:readthedocs.bzl", "readthedocs_install") readthedocs_install( name = "readthedocs_install", docs = [":docs"], @@ -90,17 +90,17 @@ set -eou pipefail declare -a extra_env while IFS='=' read -r -d '' name value; do if [[ "$name" == READTHEDOCS* ]]; then - extra_env+=("--@rules_python//sphinxdocs:extra_env=$name=$value") + extra_env+=("--@sphinxdocs//sphinxdocs:extra_env=$name=$value") fi done < <(env -0) # In order to get the build number, we extract it from the host name -extra_env+=("--@rules_python//sphinxdocs:extra_env=HOSTNAME=$HOSTNAME") +extra_env+=("--@sphinxdocs//sphinxdocs:extra_env=HOSTNAME=$HOSTNAME") set -x bazel run \ --stamp \ - "--@rules_python//sphinxdocs:extra_defines=version=$READTHEDOCS_VERSION" \ + "--@sphinxdocs//sphinxdocs:extra_defines=version=$READTHEDOCS_VERSION" \ "${extra_env[@]}" \ //docs:readthedocs_install ``` diff --git a/sphinxdocs/docs/sphinx-bzl.md b/sphinxdocs/docs/sphinx-bzl.md index 8376f60679..c96061b984 100644 --- a/sphinxdocs/docs/sphinx-bzl.md +++ b/sphinxdocs/docs/sphinx-bzl.md @@ -11,7 +11,7 @@ a well known target. ## Configuring Sphinx To enable the plugin in Sphinx, depend on -`@rules_python//sphinxdocs/src/sphinx_bzl` and enable it in `conf.py`: +`//sphinxdocs/src/sphinx_bzl` and enable it in `conf.py`: ``` extensions = [ @@ -265,7 +265,7 @@ Documents a flag. It has the same format as `{bzl:target}` ::::::{rst:directive} .. bzl:typedef:: typename Documents a user-defined structural "type". These are typically generated by -the {obj}`sphinx_stardoc` rule after following [User-defined types] to create a +the {bzl:obj}`sphinx_stardoc` rule after following [User-defined types] to create a struct with a `TYPEDEF` field, but can also be manually defined if there's no natural place for it in code, e.g. some ad-hoc structural type. diff --git a/sphinxdocs/docs/starlark-docgen.md b/sphinxdocs/docs/starlark-docgen.md index ba4ab516f5..0b89393ced 100644 --- a/sphinxdocs/docs/starlark-docgen.md +++ b/sphinxdocs/docs/starlark-docgen.md @@ -13,7 +13,7 @@ While the `sphinx_stardoc` rule doesn't require Sphinx itself, the source it generates requires some additional Sphinx plugins and config settings. When defining the `sphinx_build_binary` target, also depend on: -* `@rules_python//sphinxdocs/src/sphinx_bzl:sphinx_bzl` +* `//sphinxdocs/src/sphinx_bzl:sphinx_bzl` * `myst_parser` (e.g. `@pypi//myst_parser`) * `typing_extensions` (e.g. `@pypi//myst_parser`) @@ -21,7 +21,7 @@ When defining the `sphinx_build_binary` target, also depend on: sphinx_build_binary( name = "sphinx-build", deps = [ - "@rules_python//sphinxdocs/src/sphinx_bzl", + "//sphinxdocs/src/sphinx_bzl", "@pypi//myst_parser", "@pypi//typing_extensions", ... @@ -81,7 +81,7 @@ still possible to create such objects using `struct` and lambdas. For the purposes of documentation, they can be documented by creating a module-level `struct` with matching fields *and* also a field named `TYPEDEF`. When the `sphinx_stardoc` rule sees a struct with a `TYPEDEF` field, it generates doc -using the {rst:directive}`bzl:typedef` directive and puts all the struct's fields +using the {rst:dir}`bzl:typedef` directive and puts all the struct's fields within the typedef. The net result is the rendered docs look similar to how a class would be documented in other programming languages. diff --git a/sphinxdocs/downloader_config.cfg b/sphinxdocs/downloader_config.cfg new file mode 100644 index 0000000000..3fa6264eda --- /dev/null +++ b/sphinxdocs/downloader_config.cfg @@ -0,0 +1,21 @@ +# Try GitHub first (primary) +rewrite ^github\.com/bazel-contrib/bazel_features/(.*) github.com/bazel-contrib/bazel_features/$1 +rewrite ^github\.com/bazel-contrib/rules_go/(.*) github.com/bazel-contrib/rules_go/$1 +rewrite ^github\.com/bazelbuild/bazel-skylib/(.*) github.com/bazelbuild/bazel-skylib/$1 +rewrite ^github\.com/bazelbuild/platforms/(.*) github.com/bazelbuild/platforms/$1 +rewrite ^github\.com/bazelbuild/rules_kotlin/(.*) github.com/bazelbuild/rules_kotlin/$1 +rewrite ^github\.com/bazelbuild/rules_shell/(.*) github.com/bazelbuild/rules_shell/$1 +rewrite ^github\.com/bazelbuild/rules_java/(.*) github.com/bazelbuild/rules_java/$1 +rewrite ^github\.com/bazelbuild/stardoc/(.*) github.com/bazelbuild/stardoc/$1 + + +# Fall back to mirror (secondary) +# Tracking upstream BCR mirror addition: https://github.com/bazelbuild/platforms/issues/139 +rewrite ^github\.com/bazel-contrib/bazel_features/(.*) mirror.bazel.build/github.com/bazel-contrib/bazel_features/$1 +rewrite ^github\.com/bazel-contrib/rules_go/(.*) mirror.bazel.build/github.com/bazel-contrib/rules_go/$1 +rewrite ^github\.com/bazelbuild/bazel-skylib/(.*) mirror.bazel.build/github.com/bazelbuild/bazel-skylib/$1 +rewrite ^github\.com/bazelbuild/platforms/(.*) mirror.bazel.build/github.com/bazelbuild/platforms/$1 +rewrite ^github\.com/bazelbuild/rules_kotlin/(.*) mirror.bazel.build/github.com/bazelbuild/rules_kotlin/$1 +rewrite ^github\.com/bazelbuild/rules_shell/(.*) mirror.bazel.build/github.com/bazelbuild/rules_shell/$1 +rewrite ^github\.com/bazelbuild/rules_java/(.*) mirror.bazel.build/github.com/bazelbuild/rules_java/$1 +rewrite ^github\.com/bazelbuild/stardoc/(.*) mirror.bazel.build/github.com/bazelbuild/stardoc/$1 diff --git a/sphinxdocs/integration_tests/BUILD.bazel b/sphinxdocs/integration_tests/BUILD.bazel new file mode 100644 index 0000000000..45e894fc2e --- /dev/null +++ b/sphinxdocs/integration_tests/BUILD.bazel @@ -0,0 +1,8 @@ +load("@rules_python//python:py_library.bzl", "py_library") + +package(default_visibility = ["//visibility:public"]) + +py_library( + name = "runner_lib", + srcs = ["runner.py"], +) diff --git a/sphinxdocs/integration_tests/bazel_from_env b/sphinxdocs/integration_tests/bazel_from_env new file mode 100755 index 0000000000..a372736f32 --- /dev/null +++ b/sphinxdocs/integration_tests/bazel_from_env @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# +# A simple wrapper so rules_bazel_integration_test can use the +# bazel version inherited from the environment. + +bazel "$@" diff --git a/sphinxdocs/integration_tests/bcr/BUILD.bazel b/sphinxdocs/integration_tests/bcr/BUILD.bazel new file mode 100644 index 0000000000..412ba56ec3 --- /dev/null +++ b/sphinxdocs/integration_tests/bcr/BUILD.bazel @@ -0,0 +1,23 @@ +load("@bazel_skylib//rules:build_test.bzl", "build_test") +load("@sphinxdocs//sphinxdocs:sphinx.bzl", "sphinx_build_binary", "sphinx_docs") + +sphinx_docs( + name = "docs", + srcs = ["index.md"], + config = "conf.py", + formats = ["html"], + sphinx = ":sphinx-build", +) + +sphinx_build_binary( + name = "sphinx-build", + deps = [ + "@dev_pip//myst_parser", + "@dev_pip//sphinx", + ], +) + +build_test( + name = "docs_build_test", + targets = [":docs"], +) diff --git a/sphinxdocs/integration_tests/bcr/MODULE.bazel b/sphinxdocs/integration_tests/bcr/MODULE.bazel new file mode 100644 index 0000000000..ae1c752513 --- /dev/null +++ b/sphinxdocs/integration_tests/bcr/MODULE.bazel @@ -0,0 +1,26 @@ +module( + name = "sphinxdocs_example", + version = "0.0.0", +) + +bazel_dep(name = "sphinxdocs", version = "0.0.0") +local_path_override( + module_name = "sphinxdocs", + path = "../..", +) + +bazel_dep(name = "rules_python", version = "1.8.5") + +dev_pip = use_extension( + "@rules_python//python/extensions:pip.bzl", + "pip", + dev_dependency = True, +) +dev_pip.parse( + hub_name = "dev_pip", + python_version = "3.11", + requirements_lock = "@rules_python//docs:requirements.txt", +) +use_repo(dev_pip, "dev_pip") + +bazel_dep(name = "bazel_skylib", version = "1.8.2") diff --git a/sphinxdocs/integration_tests/bcr/conf.py b/sphinxdocs/integration_tests/bcr/conf.py new file mode 100644 index 0000000000..3b751bf402 --- /dev/null +++ b/sphinxdocs/integration_tests/bcr/conf.py @@ -0,0 +1,2 @@ +extensions = ["myst_parser"] +master_doc = "index" diff --git a/sphinxdocs/integration_tests/bcr/index.md b/sphinxdocs/integration_tests/bcr/index.md new file mode 100644 index 0000000000..bfe100937d --- /dev/null +++ b/sphinxdocs/integration_tests/bcr/index.md @@ -0,0 +1 @@ +# Sphinxdocs example diff --git a/sphinxdocs/integration_tests/integration_test.bzl b/sphinxdocs/integration_tests/integration_test.bzl new file mode 100644 index 0000000000..b6786b9c6b --- /dev/null +++ b/sphinxdocs/integration_tests/integration_test.bzl @@ -0,0 +1,68 @@ +"""Helpers for running bazel-in-bazel integration tests for sphinxdocs.""" + +load( + "@rules_bazel_integration_test//bazel_integration_test:defs.bzl", + "bazel_integration_test", + "integration_test_utils", +) +load("@rules_python//python:py_test.bzl", "py_test") + +def _test_runner(*, name, bazel_version, py_main, py_deps): + test_runner = "{}_bazel_{}_py_runner".format(name, bazel_version) + py_test( + name = test_runner, + srcs = [py_main], + main = py_main, + deps = ["//integration_tests:runner_lib"] + py_deps, + tags = ["manual"], + ) + return test_runner + +def sphinxdocs_integration_test( + name, + workspace_path = "workspace", + tags = None, + py_main = None, + py_deps = None, + bazel_versions = None, + **kwargs): + """Runs a bazel-in-bazel integration test for sphinxdocs. + + Args: + name: Name of the test. + workspace_path: The directory name of the sub-workspace. + tags: Test tags. + py_main: Main Python test runner script. + py_deps: Dependencies for py_main. + bazel_versions: List of bazel versions to test. + **kwargs: Passed to bazel_integration_test. + """ + workspace_files = integration_test_utils.glob_workspace_files(workspace_path) + native.filegroup( + name = name + "_workspace_files", + srcs = workspace_files + [ + "//:distribution", + ], + ) + kwargs.setdefault("size", "large") + for bazel_version in bazel_versions or ["self"]: + test_runner = _test_runner( + name = name, + bazel_version = bazel_version, + py_main = py_main, + py_deps = py_deps or [], + ) + bazel_integration_test( + name = "{}_bazel_{}".format(name, bazel_version), + workspace_path = workspace_path, + test_runner = test_runner, + bazel_version = bazel_version, + workspace_files = [name + "_workspace_files"], + tags = (tags or []) + [ + "exclusive", + "no-sandbox", + "no-remote-exec", + "integration-test", + ], + **kwargs + ) diff --git a/sphinxdocs/integration_tests/persistent_worker/BUILD.bazel b/sphinxdocs/integration_tests/persistent_worker/BUILD.bazel new file mode 100644 index 0000000000..60fb4cbc6c --- /dev/null +++ b/sphinxdocs/integration_tests/persistent_worker/BUILD.bazel @@ -0,0 +1,8 @@ +load("//integration_tests:integration_test.bzl", "sphinxdocs_integration_test") + +package(default_visibility = ["//visibility:public"]) + +sphinxdocs_integration_test( + name = "persistent_worker_test", + py_main = "persistent_worker_test.py", +) diff --git a/sphinxdocs/integration_tests/persistent_worker/persistent_worker_test.py b/sphinxdocs/integration_tests/persistent_worker/persistent_worker_test.py new file mode 100644 index 0000000000..30a8b46c00 --- /dev/null +++ b/sphinxdocs/integration_tests/persistent_worker/persistent_worker_test.py @@ -0,0 +1,54 @@ +import unittest + +from integration_tests import runner + + +class PersistentWorkerTest(runner.TestCase): + def _check_index_html(self, text: str, should_exist: bool): + index_html = ( + self.repo_root / "bazel-bin" / "docs" / "_build" / "html" / "index.html" + ) + if not index_html.exists(): + self.fail(f"Could not find index.html at {index_html}") + content = index_html.read_text() + if should_exist: + self.assertIn( + text, + content, + f"Expected '{text}' in index.html after build, but not found.", + ) + else: + self.assertNotIn( + text, + content, + f"Expected '{text}' NOT to be in index.html after build, but found it.", + ) + + def test_incremental_add_and_remove_files(self): + # 1. Initial build + result = self.run_bazel("build", "//:docs") + self.assert_result_matches(result, "bazel-bin") + index_html = ( + self.repo_root / "bazel-bin" / "docs" / "_build" / "html" / "index.html" + ) + self.assertTrue( + index_html.exists(), "index.html should exist after initial build" + ) + + # 2. Add a new markdown file and verify it is included across incremental build + page2_md = self.repo_root / "page2.md" + page2_md.write_text("# Page 2\n\nThis is a newly added page.\n") + result = self.run_bazel("build", "//:docs") + self.assert_result_matches(result, "bazel-bin") + self._check_index_html("page2.html", should_exist=True) + + # 3. Remove the added markdown file and verify the persistent worker cleans up + # stale source files and invalidates toctrees without errors or warnings. + page2_md.unlink() + result = self.run_bazel("build", "//:docs") + self.assert_result_matches(result, "bazel-bin") + self._check_index_html("page2.html", should_exist=False) + + +if __name__ == "__main__": + unittest.main() diff --git a/sphinxdocs/integration_tests/persistent_worker/workspace/.bazelrc b/sphinxdocs/integration_tests/persistent_worker/workspace/.bazelrc new file mode 100644 index 0000000000..f3162b9547 --- /dev/null +++ b/sphinxdocs/integration_tests/persistent_worker/workspace/.bazelrc @@ -0,0 +1,8 @@ +# Disable disk caching and remote action caching so Bazel is forced to dispatch builds +# directly to the running persistent worker instead of restoring pre-built HTML cache results across test steps. +common --disk_cache= +build --noremote_accept_cached +common --http_timeout_scaling=10.0 +common --experimental_repository_downloader_retries=10 +common --experimental_downloader_config=downloader_config.cfg +common --lockfile_mode=off diff --git a/sphinxdocs/integration_tests/persistent_worker/workspace/BUILD.bazel b/sphinxdocs/integration_tests/persistent_worker/workspace/BUILD.bazel new file mode 100644 index 0000000000..60e66004fd --- /dev/null +++ b/sphinxdocs/integration_tests/persistent_worker/workspace/BUILD.bazel @@ -0,0 +1,17 @@ +load("@sphinxdocs//sphinxdocs:sphinx.bzl", "sphinx_build_binary", "sphinx_docs") + +sphinx_docs( + name = "docs", + srcs = glob(["*.md"]), + config = "conf.py", + formats = ["html"], + sphinx = ":sphinx-build", +) + +sphinx_build_binary( + name = "sphinx-build", + deps = [ + "@dev_pip//myst_parser", + "@dev_pip//sphinx", + ], +) diff --git a/sphinxdocs/integration_tests/persistent_worker/workspace/MODULE.bazel b/sphinxdocs/integration_tests/persistent_worker/workspace/MODULE.bazel new file mode 100644 index 0000000000..4e2c25e8e4 --- /dev/null +++ b/sphinxdocs/integration_tests/persistent_worker/workspace/MODULE.bazel @@ -0,0 +1,23 @@ +module(version = "0.0.0") + +bazel_dep(name = "sphinxdocs", version = "0.0.0") +local_path_override( + module_name = "sphinxdocs", + path = "../../..", +) + +bazel_dep(name = "rules_python", version = "1.8.5") + +dev_pip = use_extension( + "@rules_python//python/extensions:pip.bzl", + "pip", + dev_dependency = True, +) +dev_pip.parse( + hub_name = "dev_pip", + python_version = "3.11", + requirements_lock = "@rules_python//docs:requirements.txt", +) +use_repo(dev_pip, "dev_pip") + +bazel_dep(name = "bazel_skylib", version = "1.8.2") diff --git a/sphinxdocs/integration_tests/persistent_worker/workspace/WORKSPACE b/sphinxdocs/integration_tests/persistent_worker/workspace/WORKSPACE new file mode 100644 index 0000000000..4be37fab94 --- /dev/null +++ b/sphinxdocs/integration_tests/persistent_worker/workspace/WORKSPACE @@ -0,0 +1 @@ +# Bzlmod-enabled workspace; this empty WORKSPACE file satisfies rules_bazel_integration_test checks. diff --git a/sphinxdocs/integration_tests/persistent_worker/workspace/conf.py b/sphinxdocs/integration_tests/persistent_worker/workspace/conf.py new file mode 100644 index 0000000000..de33bde101 --- /dev/null +++ b/sphinxdocs/integration_tests/persistent_worker/workspace/conf.py @@ -0,0 +1 @@ +extensions = ["myst_parser"] diff --git a/sphinxdocs/integration_tests/persistent_worker/workspace/downloader_config.cfg b/sphinxdocs/integration_tests/persistent_worker/workspace/downloader_config.cfg new file mode 100644 index 0000000000..3fa6264eda --- /dev/null +++ b/sphinxdocs/integration_tests/persistent_worker/workspace/downloader_config.cfg @@ -0,0 +1,21 @@ +# Try GitHub first (primary) +rewrite ^github\.com/bazel-contrib/bazel_features/(.*) github.com/bazel-contrib/bazel_features/$1 +rewrite ^github\.com/bazel-contrib/rules_go/(.*) github.com/bazel-contrib/rules_go/$1 +rewrite ^github\.com/bazelbuild/bazel-skylib/(.*) github.com/bazelbuild/bazel-skylib/$1 +rewrite ^github\.com/bazelbuild/platforms/(.*) github.com/bazelbuild/platforms/$1 +rewrite ^github\.com/bazelbuild/rules_kotlin/(.*) github.com/bazelbuild/rules_kotlin/$1 +rewrite ^github\.com/bazelbuild/rules_shell/(.*) github.com/bazelbuild/rules_shell/$1 +rewrite ^github\.com/bazelbuild/rules_java/(.*) github.com/bazelbuild/rules_java/$1 +rewrite ^github\.com/bazelbuild/stardoc/(.*) github.com/bazelbuild/stardoc/$1 + + +# Fall back to mirror (secondary) +# Tracking upstream BCR mirror addition: https://github.com/bazelbuild/platforms/issues/139 +rewrite ^github\.com/bazel-contrib/bazel_features/(.*) mirror.bazel.build/github.com/bazel-contrib/bazel_features/$1 +rewrite ^github\.com/bazel-contrib/rules_go/(.*) mirror.bazel.build/github.com/bazel-contrib/rules_go/$1 +rewrite ^github\.com/bazelbuild/bazel-skylib/(.*) mirror.bazel.build/github.com/bazelbuild/bazel-skylib/$1 +rewrite ^github\.com/bazelbuild/platforms/(.*) mirror.bazel.build/github.com/bazelbuild/platforms/$1 +rewrite ^github\.com/bazelbuild/rules_kotlin/(.*) mirror.bazel.build/github.com/bazelbuild/rules_kotlin/$1 +rewrite ^github\.com/bazelbuild/rules_shell/(.*) mirror.bazel.build/github.com/bazelbuild/rules_shell/$1 +rewrite ^github\.com/bazelbuild/rules_java/(.*) mirror.bazel.build/github.com/bazelbuild/rules_java/$1 +rewrite ^github\.com/bazelbuild/stardoc/(.*) mirror.bazel.build/github.com/bazelbuild/stardoc/$1 diff --git a/sphinxdocs/integration_tests/persistent_worker/workspace/index.md b/sphinxdocs/integration_tests/persistent_worker/workspace/index.md new file mode 100644 index 0000000000..d1dca92350 --- /dev/null +++ b/sphinxdocs/integration_tests/persistent_worker/workspace/index.md @@ -0,0 +1,7 @@ +# Test Documentation + +```{toctree} +:glob: true + +* +``` diff --git a/sphinxdocs/integration_tests/runner.py b/sphinxdocs/integration_tests/runner.py new file mode 100644 index 0000000000..cab9730bb8 --- /dev/null +++ b/sphinxdocs/integration_tests/runner.py @@ -0,0 +1,99 @@ +import logging +import os +import os.path +import pathlib +import re +import shlex +import subprocess +import unittest + +_logger = logging.getLogger(__name__) + + +class ExecuteError(Exception): + def __init__(self, result): + self.result = result + + def __str__(self): + return self.result.describe() + + +class ExecuteResult: + def __init__( + self, + args: list[str], + env: dict[str, str], + cwd: pathlib.Path, + proc_result: subprocess.CompletedProcess, + ): + self.args = args + self.env = env + self.cwd = cwd + self.exit_code = proc_result.returncode + self.stdout = proc_result.stdout + self.stderr = proc_result.stderr + + def describe(self) -> str: + env_lines = [ + " " + shlex.quote(f"{key}={value}") + for key, value in sorted(self.env.items()) + ] + env = " \\\n".join(env_lines) + args = shlex.join(self.args) + maybe_stdout_nl = "" if self.stdout.endswith("\n") else "\n" + maybe_stderr_nl = "" if self.stderr.endswith("\n") else "\n" + return f"""\ +COMMAND: +cd {self.cwd} && \\ +env \\ +{env} \\ + {args} +RESULT: exit_code: {self.exit_code} +===== STDOUT START ===== +{self.stdout}{maybe_stdout_nl}===== STDOUT END ===== +===== STDERR START ===== +{self.stderr}{maybe_stderr_nl}===== STDERR END ===== +""" + + +class TestCase(unittest.TestCase): + def setUp(self): + super().setUp() + self.repo_root = pathlib.Path(os.environ["BIT_WORKSPACE_DIR"]) + self.bazel = pathlib.Path(os.environ["BIT_BAZEL_BINARY"]) + outer_test_tmpdir = pathlib.Path(os.environ["TEST_TMPDIR"]) + self.test_tmp_dir = outer_test_tmpdir / "bit_test_tmp" + self.tmp_dir = outer_test_tmpdir / "bit_tmp" + self.bazel_env = { + "PATH": os.environ["PATH"], + "TEST_TMPDIR": str(self.test_tmp_dir), + "TMP": str(self.tmp_dir), + "RUNFILES_DIR": os.environ["TEST_SRCDIR"], + } + + def run_bazel(self, *args: str, check: bool = True) -> ExecuteResult: + args = [str(self.bazel), *args] + env = self.bazel_env + _logger.info("executing: %s", shlex.join(args)) + cwd = self.repo_root + proc_result = subprocess.run( + args=args, + text=True, + capture_output=True, + cwd=cwd, + env=env, + check=False, + ) + exec_result = ExecuteResult(args, env, cwd, proc_result) + if check and exec_result.exit_code: + raise ExecuteError(exec_result) + else: + return exec_result + + def assert_result_matches(self, result: ExecuteResult, regex: str) -> None: + if not re.search(regex, result.stdout + result.stderr): + self.fail( + "Bazel output did not match expected pattern\n" + + f"expected pattern: {regex}\n" + + f"invocation details:\n{result.describe()}" + ) diff --git a/sphinxdocs/sphinxdocs/BUILD.bazel b/sphinxdocs/sphinxdocs/BUILD.bazel new file mode 100644 index 0000000000..9a871f5268 --- /dev/null +++ b/sphinxdocs/sphinxdocs/BUILD.bazel @@ -0,0 +1,104 @@ +# Copyright 2023 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//:bzl_library.bzl", "bzl_library") +load("@bazel_skylib//rules:common_settings.bzl", "bool_flag") +load("//sphinxdocs/private:sphinx.bzl", "repeated_string_list_flag") + +package( + default_visibility = ["//:__subpackages__"], +) + +filegroup( + name = "distribution", + srcs = glob(["**/*"]) + [ + "//sphinxdocs/private:distribution", + ], + visibility = ["//:__pkg__"], +) + +# Additional -D values to add to every Sphinx build. +# This is usually used to override the version when building +repeated_string_list_flag( + name = "extra_defines", + build_setting_default = [], +) + +repeated_string_list_flag( + name = "extra_env", + build_setting_default = [], +) + +# Whether to add the `-q` arg to Sphinx invocations, which determines if +# stdout has any output or not (logging INFO messages and progress messages). +# If true, add `-q`. If false, don't add `-q`. This is mostly useful for +# debugging invocations or developing extensions. +bool_flag( + name = "quiet", + build_setting_default = True, +) + +bzl_library( + name = "sphinx", + srcs = ["sphinx.bzl"], + visibility = ["//visibility:public"], + deps = ["//sphinxdocs/private:sphinx"], +) + +bzl_library( + name = "sphinx_docs_library", + srcs = ["sphinx_docs_library.bzl"], + visibility = ["//visibility:public"], + deps = ["//sphinxdocs/private:sphinx_docs_library_macro"], +) + +bzl_library( + name = "sphinx_stardoc", + srcs = ["sphinx_stardoc.bzl"], + visibility = ["//visibility:public"], + deps = ["//sphinxdocs/private:sphinx_stardoc"], +) + +bzl_library( + name = "readthedocs", + srcs = ["readthedocs.bzl"], + visibility = ["//visibility:public"], + deps = ["//sphinxdocs/private:readthedocs"], +) + +# ========= Deprecated aliases for backwards compatibility ========= + +alias( + name = "sphinx_bzl", + actual = ":sphinx", + deprecation = "Use //sphinxdocs:sphinx instead", +) + +alias( + name = "sphinx_docs_library_bzl", + actual = ":sphinx_docs_library", + deprecation = "Use //sphinxdocs:sphinx_docs_library instead", +) + +alias( + name = "sphinx_stardoc_bzl", + actual = ":sphinx_stardoc", + deprecation = "Use //sphinxdocs:sphinx_stardoc instead", +) + +alias( + name = "readthedocs_bzl", + actual = ":readthedocs", + deprecation = "Use //sphinxdocs:readthedocs instead", +) diff --git a/sphinxdocs/inventories/BUILD.bazel b/sphinxdocs/sphinxdocs/inventories/BUILD.bazel similarity index 100% rename from sphinxdocs/inventories/BUILD.bazel rename to sphinxdocs/sphinxdocs/inventories/BUILD.bazel diff --git a/sphinxdocs/inventories/bazel_inventory.txt b/sphinxdocs/sphinxdocs/inventories/bazel_inventory.txt similarity index 97% rename from sphinxdocs/inventories/bazel_inventory.txt rename to sphinxdocs/sphinxdocs/inventories/bazel_inventory.txt index e14ea76067..866be33886 100644 --- a/sphinxdocs/inventories/bazel_inventory.txt +++ b/sphinxdocs/sphinxdocs/inventories/bazel_inventory.txt @@ -42,6 +42,7 @@ config.target bzl:function 1 rules/lib/toplevel/config#target - config_common.FeatureFlagInfo bzl:type 1 rules/lib/toplevel/config_common#FeatureFlagInfo - config_common.toolchain_type bzl:function 1 rules/lib/toplevel/config_common#toolchain_type - config_setting bzl:rule 1 reference/be/general#config_setting - +configuration_field bzl:type 1 rules/lib/builtins/configuration_field - ctx bzl:type 1 rules/lib/builtins/repository_ctx - ctx.actions bzl:obj 1 rules/lib/builtins/ctx#actions - ctx.aspect_ids bzl:obj 1 rules/lib/builtins/ctx#aspect_ids - @@ -157,6 +158,7 @@ runfiles.merge bzl:type 1 rules/lib/builtins/runfiles#merge - runfiles.merge_all bzl:type 1 rules/lib/builtins/runfiles#merge_all - runfiles.root_symlinks bzl:type 1 rules/lib/builtins/runfiles#root_symlinks - runfiles.symlinks bzl:type 1 rules/lib/builtins/runfiles#symlinks - +stamp bzl:flag 1 reference/command-line-reference#flag--stamp - str bzl:type 1 rules/lib/string - struct bzl:type 1 rules/lib/builtins/struct - target_compatible_with bzl:attr 1 reference/be/common-definitions#common.target_compatible_with - @@ -171,3 +173,4 @@ toolchain.target_settings bzl:attr 1 reference/be/platforms-and-toolchains#toolc toolchain_type bzl:type 1 rules/lib/builtins/toolchain_type.html - transition bzl:type 1 rules/lib/builtins/transition - tuple bzl:type 1 rules/lib/core/tuple - +workspace_status_command bzl:flag 1 reference/command-line-reference#build-flag--workspace_status_command - diff --git a/sphinxdocs/private/BUILD.bazel b/sphinxdocs/sphinxdocs/private/BUILD.bazel similarity index 67% rename from sphinxdocs/private/BUILD.bazel rename to sphinxdocs/sphinxdocs/private/BUILD.bazel index c707b4d1d8..fa5ded15f1 100644 --- a/sphinxdocs/private/BUILD.bazel +++ b/sphinxdocs/sphinxdocs/private/BUILD.bazel @@ -14,16 +14,17 @@ load("@bazel_skylib//:bzl_library.bzl", "bzl_library") load("@com_google_protobuf//bazel:py_proto_library.bzl", "py_proto_library") -load("//python:py_binary.bzl", "py_binary") -load("//python:py_library.bzl", "py_library") +load("@rules_python//python:py_binary.bzl", "py_binary") +load("@rules_python//python:py_library.bzl", "py_library") +load(":visibility.bzl", "NOT_ACTUALLY_PUBLIC") package( - default_visibility = ["//sphinxdocs:__subpackages__"], + default_visibility = ["//:__subpackages__"], ) -# These are only exported because they're passed as files to the //sphinxdocs +# These are only exported because they're passed as files to the @sphinxdocs # macros, and thus must be visible to other packages. They should only be -# referenced by the //sphinxdocs macros. +# referenced by the @sphinxdocs macros. exports_files( [ "readthedocs_install.py", @@ -34,48 +35,63 @@ exports_files( visibility = ["//visibility:public"], ) +filegroup( + name = "distribution", + srcs = glob(["**/*"]), + visibility = ["//sphinxdocs:__pkg__"], +) + +bzl_library( + name = "util", + srcs = ["util.bzl"], + deps = [ + "@bazel_skylib//lib:types", + ], +) + bzl_library( - name = "sphinx_docs_library_macro_bzl", + name = "sphinx_docs_library_macro", srcs = ["sphinx_docs_library_macro.bzl"], deps = [ - ":sphinx_docs_library_bzl", - "//python/private:util_bzl", + ":sphinx_docs_library", + ":util", ], ) bzl_library( - name = "sphinx_docs_library_bzl", + name = "sphinx_docs_library", srcs = ["sphinx_docs_library.bzl"], - deps = [":sphinx_docs_library_info_bzl"], + deps = [":sphinx_docs_library_info"], ) bzl_library( - name = "sphinx_docs_library_info_bzl", + name = "sphinx_docs_library_info", srcs = ["sphinx_docs_library_info.bzl"], ) bzl_library( - name = "sphinx_bzl", + name = "sphinx", srcs = ["sphinx.bzl"], deps = [ - ":sphinx_docs_library_info_bzl", - "//python:py_binary_bzl", + ":sphinx_docs_library_info", + ":util", "@bazel_skylib//:bzl_library", "@bazel_skylib//lib:paths", "@bazel_skylib//lib:types", "@bazel_skylib//rules:build_test", "@bazel_skylib//rules:common_settings", "@io_bazel_stardoc//stardoc:stardoc_lib", + "@rules_python//python:py_binary_bzl", ], ) bzl_library( - name = "sphinx_stardoc_bzl", + name = "sphinx_stardoc", srcs = ["sphinx_stardoc.bzl"], deps = [ - ":sphinx_docs_library_macro_bzl", - "//python/private:util_bzl", - "//sphinxdocs:sphinx_bzl", + ":sphinx_docs_library_macro", + ":util", + "//sphinxdocs:sphinx", "@bazel_skylib//:bzl_library", "@bazel_skylib//lib:paths", "@bazel_skylib//lib:types", @@ -85,23 +101,31 @@ bzl_library( ) bzl_library( - name = "readthedocs_bzl", + name = "readthedocs", srcs = ["readthedocs.bzl"], - deps = ["//python:py_binary_bzl"], + deps = [ + ":util", + "@rules_python//python:py_binary_bzl", + ], +) + +bzl_library( + name = "visibility", + srcs = ["visibility.bzl"], ) py_binary( name = "inventory_builder", srcs = ["inventory_builder.py"], # Only public because it's an implicit attribute - visibility = ["//visibility:public"], + visibility = NOT_ACTUALLY_PUBLIC, ) py_binary( name = "proto_to_markdown", srcs = ["proto_to_markdown.py"], # Only public because it's an implicit attribute - visibility = ["//visibility:public"], + visibility = NOT_ACTUALLY_PUBLIC, deps = [":proto_to_markdown_lib"], ) @@ -109,7 +133,7 @@ py_library( name = "proto_to_markdown_lib", srcs = ["proto_to_markdown.py"], # Only public because it's an implicit attribute - visibility = ["//visibility:public"], + visibility = NOT_ACTUALLY_PUBLIC, deps = [ ":stardoc_output_proto_py_pb2", ], diff --git a/sphinxdocs/private/inventory_builder.py b/sphinxdocs/sphinxdocs/private/inventory_builder.py similarity index 100% rename from sphinxdocs/private/inventory_builder.py rename to sphinxdocs/sphinxdocs/private/inventory_builder.py diff --git a/sphinxdocs/private/proto_to_markdown.py b/sphinxdocs/sphinxdocs/private/proto_to_markdown.py similarity index 99% rename from sphinxdocs/private/proto_to_markdown.py rename to sphinxdocs/sphinxdocs/private/proto_to_markdown.py index 58fb79393d..05278a5c02 100644 --- a/sphinxdocs/private/proto_to_markdown.py +++ b/sphinxdocs/sphinxdocs/private/proto_to_markdown.py @@ -13,11 +13,9 @@ # limitations under the License. import argparse -import io import itertools import pathlib import sys -import textwrap from typing import Callable, TextIO, TypeVar from stardoc.proto import stardoc_output_pb2 diff --git a/sphinxdocs/private/readthedocs.bzl b/sphinxdocs/sphinxdocs/private/readthedocs.bzl similarity index 87% rename from sphinxdocs/private/readthedocs.bzl rename to sphinxdocs/sphinxdocs/private/readthedocs.bzl index a62c51b86a..14bb1c9fcd 100644 --- a/sphinxdocs/private/readthedocs.bzl +++ b/sphinxdocs/sphinxdocs/private/readthedocs.bzl @@ -13,8 +13,8 @@ # limitations under the License. """Starlark rules for integrating Sphinx and Readthedocs.""" -load("//python:py_binary.bzl", "py_binary") -load("//python/private:util.bzl", "add_tag") # buildifier: disable=bzl-visibility +load("@rules_python//python:py_binary.bzl", "py_binary") +load("//sphinxdocs/private:util.bzl", "add_tag") # buildifier: disable=bzl-visibility _INSTALL_MAIN_SRC = Label("//sphinxdocs/private:readthedocs_install.py") @@ -33,7 +33,7 @@ def readthedocs_install(name, docs, **kwargs): is typically a single {obj}`sphinx_stardocs` target. **kwargs: {type}`dict` additional kwargs to pass onto the installer """ - add_tag(kwargs, "@rules_python//sphinxdocs:readthedocs_install") + add_tag(kwargs, "//sphinxdocs:readthedocs_install") py_binary( name = name, srcs = [_INSTALL_MAIN_SRC], @@ -43,6 +43,6 @@ def readthedocs_install(name, docs, **kwargs): "$(rlocationpaths {})".format(d) for d in docs ], - deps = [Label("//python/runfiles")], + deps = [Label("@rules_python//python/runfiles")], **kwargs ) diff --git a/sphinxdocs/private/readthedocs_install.py b/sphinxdocs/sphinxdocs/private/readthedocs_install.py similarity index 100% rename from sphinxdocs/private/readthedocs_install.py rename to sphinxdocs/sphinxdocs/private/readthedocs_install.py diff --git a/sphinxdocs/private/sphinx.bzl b/sphinxdocs/sphinxdocs/private/sphinx.bzl similarity index 97% rename from sphinxdocs/private/sphinx.bzl rename to sphinxdocs/sphinxdocs/private/sphinx.bzl index c1efda3508..b7c051a154 100644 --- a/sphinxdocs/private/sphinx.bzl +++ b/sphinxdocs/sphinxdocs/private/sphinx.bzl @@ -16,8 +16,8 @@ load("@bazel_skylib//lib:paths.bzl", "paths") load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") -load("//python:py_binary.bzl", "py_binary") -load("//python/private:util.bzl", "add_tag", "copy_propagating_kwargs") # buildifier: disable=bzl-visibility +load("@rules_python//python:py_binary.bzl", "py_binary") +load("//sphinxdocs/private:util.bzl", "add_tag", "copy_propagating_kwargs") # buildifier: disable=bzl-visibility load(":sphinx_docs_library_info.bzl", "SphinxDocsLibraryInfo") _SPHINX_BUILD_MAIN_SRC = Label("//sphinxdocs/private:sphinx_build.py") @@ -83,7 +83,7 @@ def sphinx_build_binary(name, py_binary_rule = py_binary, **kwargs): **kwargs: {type}`dict` Additional kwargs to pass onto `py_binary`. The `srcs` and `main` attributes must not be specified. """ - add_tag(kwargs, "@rules_python//sphinxdocs:sphinx_build_binary") + add_tag(kwargs, "//sphinxdocs:sphinx_build_binary") py_binary_rule( name = name, srcs = [_SPHINX_BUILD_MAIN_SRC], @@ -134,7 +134,7 @@ def sphinx_docs( formats: (list of str) the formats (`-b` flag) to generate documentation in. Each format will become an output group. strip_prefix: {type}`str` A prefix to remove from the file paths of the - source files. e.g., given `//docs:foo.md`, stripping `docs/` makes + source files. e.g., given `//sphinxdocs/docs:foo.md`, stripping `docs/` makes Sphinx see `foo.md` in its generated source directory. If not specified, then {any}`native.package_name` is used. extra_opts: {type}`list[str]` Additional options to pass onto Sphinx building. @@ -148,7 +148,7 @@ def sphinx_docs( This can improve incremental building of docs. **kwargs: {type}`dict` Common attributes to pass onto rules. """ - add_tag(kwargs, "@rules_python//sphinxdocs:sphinx_docs") + add_tag(kwargs, "//sphinxdocs:sphinx_docs") common_kwargs = copy_propagating_kwargs(kwargs) internal_name = "_{}".format(name.lstrip("_")) @@ -189,8 +189,9 @@ def sphinx_docs( srcs = [_SPHINX_SERVE_MAIN_SRC], main = _SPHINX_SERVE_MAIN_SRC, data = [html_name], + deps = [Label("@rules_python//python/runfiles")], args = [ - "$(execpath {})".format(html_name), + "$(rlocationpath {})".format(html_name), ], **common_kwargs_with_manual_tag ) @@ -481,7 +482,7 @@ def sphinx_inventory(*, name, src, **kwargs): the value `-` to indicate it is the same as `name` :::{seealso} - {bzl:obj}`//sphinxdocs/inventories` for inventories of Bazel objects. + {bzl:obj}`//inventories` for inventories of Bazel objects. ::: Args: @@ -498,6 +499,7 @@ def _sphinx_inventory_impl(ctx): args.add(output) ctx.actions.run( executable = ctx.executable._builder, + mnemonic = "SphinxInventoryBuilder", arguments = [args], inputs = depset([ctx.file.src]), outputs = [output], diff --git a/sphinxdocs/private/sphinx_build.py b/sphinxdocs/sphinxdocs/private/sphinx_build.py similarity index 74% rename from sphinxdocs/private/sphinx_build.py rename to sphinxdocs/sphinxdocs/private/sphinx_build.py index b438c89fe1..07a123b496 100644 --- a/sphinxdocs/private/sphinx_build.py +++ b/sphinxdocs/sphinxdocs/private/sphinx_build.py @@ -30,7 +30,6 @@ def __init__(self, message, exit_code): class Worker: - def __init__( self, instream: "typing.TextIO", outstream: "typing.TextIO", exec_root: str ): @@ -83,8 +82,11 @@ def run(self) -> None: if response: self._send_response(response) except SphinxMainError as e: - logger.error("Sphinx main returned failure: exit_code=%s request=%s", - request, e.exit_code) + logger.error( + "Sphinx main returned failure: exit_code=%s request=%s", + request, + e.exit_code, + ) request_id = 0 if not request else request.get("requestId", 0) self._send_response( { @@ -142,6 +144,30 @@ def _prepare_sphinx(self, request): logger.info("path %s changed", path) changed_paths.append(path) + # Remove any source files that were tracked in the previous request (`current_digests`) + # but are missing from the current `request["inputs"]` (`incoming_digests`). + # Across incremental branch switches or file removals, if these stale symlinks + # remain in `srcdir` on disk, Sphinx will discover broken/unreadable files during + # `find_files()` and abort with "WARNING: Ignored unreadable document" (fatal with -W). + for path in set(current_digests) - set(incoming_digests): + removed_path = os.path.join(srcdir, path) + if os.path.exists(removed_path) or os.path.islink(removed_path): + logger.info("removing stale source file %s", removed_path) + try: + if os.path.islink(removed_path): + try: + os.remove(removed_path) + except OSError: + os.rmdir(removed_path) + elif os.path.isdir(removed_path): + shutil.rmtree(removed_path) + else: + os.remove(removed_path) + except OSError as e: + logger.warning( + "failed to remove stale source %s: %s", removed_path, e + ) + self._digests[srcdir] = incoming_digests self._extension.changed_paths = changed_paths request_info["changed_sources"] = changed_paths @@ -186,6 +212,12 @@ def _process_request(self, request: "WorkRequest") -> "WorkResponse | None": stdout.truncate(0) stderr.seek(0) stderr.truncate(0) + # If Sphinx cache (`--doctree-dir`) becomes corrupted across incremental + # updates or branch checkouts, exit code 2 is returned. Wiping out the cached + # doctrees before retrying allows Sphinx to recover cleanly from scratch. + for arg in sphinx_args: + if arg.startswith("--doctree-dir="): + shutil.rmtree(arg.split("=", 1)[1], ignore_errors=True) exit_code = main(sphinx_args) if exit_code: @@ -218,7 +250,6 @@ def _process_request(self, request: "WorkRequest") -> "WorkResponse | None": ) raise SphinxMainError(message, exit_code) - # Copying is unfortunately necessary because Bazel doesn't know to # implicily bring along what the symlinks point to. shutil.copytree(worker_outdir, bazel_outdir, dirs_exist_ok=True) @@ -246,14 +277,31 @@ def setup(self, app): return {"parallel_read_safe": True, "parallel_write_safe": True} def _handle_env_get_outdated(self, app, env, added, changed, removed): - changed = { - # NOTE: path2doc returns None if it's not a doc path - env.path2doc(p) - for p in self.changed_paths - } - - logger.info("changed docs: %s", changed) - return changed + changed_docs = set() + for p in self.changed_paths: + # Try multiple path resolutions because depending on how Sphinx and Bazel + # represent inputs (`p`), `env.path2doc` may require relative, srcdir-joined, + # or absolute paths to successfully resolve the document name. + doc = ( + env.path2doc(p) + or env.path2doc(os.path.join(env.srcdir, p)) + or env.path2doc(os.path.abspath(os.path.join(env.srcdir, p))) + ) + if doc: + changed_docs.add(doc) + + # When documents are added or removed across incremental builds or branch checkouts, + # parent documents whose `toctree` includes them (especially via glob patterns or + # explicit references to removed docs) must be invalidated and re-read. Otherwise, + # Sphinx retains stale table of contents entries or throws unresolvable reference errors. + if added or removed: + glob_toctrees = getattr(env, "glob_toctrees", set()) + for doc, includes in getattr(env, "toctree_includes", {}).items(): + if doc in glob_toctrees or not removed.isdisjoint(includes): + changed_docs.add(doc) + + logger.info("changed docs: %s", changed_docs) + return changed_docs def _worker_main(stdin, stdout, exec_root): diff --git a/sphinxdocs/private/sphinx_docs_library.bzl b/sphinxdocs/sphinxdocs/private/sphinx_docs_library.bzl similarity index 100% rename from sphinxdocs/private/sphinx_docs_library.bzl rename to sphinxdocs/sphinxdocs/private/sphinx_docs_library.bzl diff --git a/sphinxdocs/private/sphinx_docs_library_info.bzl b/sphinxdocs/sphinxdocs/private/sphinx_docs_library_info.bzl similarity index 100% rename from sphinxdocs/private/sphinx_docs_library_info.bzl rename to sphinxdocs/sphinxdocs/private/sphinx_docs_library_info.bzl diff --git a/sphinxdocs/private/sphinx_docs_library_macro.bzl b/sphinxdocs/sphinxdocs/private/sphinx_docs_library_macro.bzl similarity index 70% rename from sphinxdocs/private/sphinx_docs_library_macro.bzl rename to sphinxdocs/sphinxdocs/private/sphinx_docs_library_macro.bzl index 095b3769ca..e93a2f5bf6 100644 --- a/sphinxdocs/private/sphinx_docs_library_macro.bzl +++ b/sphinxdocs/sphinxdocs/private/sphinx_docs_library_macro.bzl @@ -1,6 +1,6 @@ """Implementation of sphinx_docs_library macro.""" -load("//python/private:util.bzl", "add_tag") # buildifier: disable=bzl-visibility +load("//sphinxdocs/private:util.bzl", "add_tag") # buildifier: disable=bzl-visibility load(":sphinx_docs_library.bzl", _sphinx_docs_library = "sphinx_docs_library") def sphinx_docs_library(**kwargs): @@ -9,5 +9,5 @@ def sphinx_docs_library(**kwargs): Args: **kwargs: Args passed onto underlying {bzl:rule}`sphinx_docs_library` rule """ - add_tag(kwargs, "@rules_python//sphinxdocs:sphinx_docs_library") + add_tag(kwargs, "//sphinxdocs:sphinx_docs_library") _sphinx_docs_library(**kwargs) diff --git a/sphinxdocs/private/sphinx_run_template.sh b/sphinxdocs/sphinxdocs/private/sphinx_run_template.sh similarity index 100% rename from sphinxdocs/private/sphinx_run_template.sh rename to sphinxdocs/sphinxdocs/private/sphinx_run_template.sh diff --git a/sphinxdocs/private/sphinx_server.py b/sphinxdocs/sphinxdocs/private/sphinx_server.py similarity index 86% rename from sphinxdocs/private/sphinx_server.py rename to sphinxdocs/sphinxdocs/private/sphinx_server.py index 1f4fae86de..5d26eaf728 100644 --- a/sphinxdocs/private/sphinx_server.py +++ b/sphinxdocs/sphinxdocs/private/sphinx_server.py @@ -5,22 +5,26 @@ import time from http import server +from python.runfiles import Runfiles + def main(argv): - build_workspace_directory = os.environ["BUILD_WORKSPACE_DIRECTORY"] - docs_directory = argv[1] - serve_directory = os.path.join(build_workspace_directory, docs_directory) + r = Runfiles.Create() + serve_directory = r.Rlocation(argv[1]) + if not serve_directory: + print(f"Error: could not find runfile for '{argv[1]}'", file=sys.stderr) + return 1 class DirectoryHandler(server.SimpleHTTPRequestHandler): def __init__(self, *args, **kwargs): super().__init__(directory=serve_directory, *args, **kwargs) - address = ("0.0.0.0", 8000) + address = ("0.0.0.0", 8000) # noqa: F841 # with server.ThreadingHTTPServer(address, DirectoryHandler) as (ip, port, httpd): with _start_server(DirectoryHandler, "0.0.0.0", 8000) as (ip, port, httpd): def _print_server_info(): - print(f"Serving...") + print("Serving...") print(f" Address: http://{ip}:{port}") print(f" Serving directory: {serve_directory}") print(f" url: file://{serve_directory}") diff --git a/sphinxdocs/private/sphinx_stardoc.bzl b/sphinxdocs/sphinxdocs/private/sphinx_stardoc.bzl similarity index 96% rename from sphinxdocs/private/sphinx_stardoc.bzl rename to sphinxdocs/sphinxdocs/private/sphinx_stardoc.bzl index d5869b0bc4..75a4daf4d0 100644 --- a/sphinxdocs/private/sphinx_stardoc.bzl +++ b/sphinxdocs/sphinxdocs/private/sphinx_stardoc.bzl @@ -19,8 +19,8 @@ load("@bazel_skylib//lib:paths.bzl", "paths") load("@bazel_skylib//lib:types.bzl", "types") load("@bazel_skylib//rules:build_test.bzl", "build_test") load("@io_bazel_stardoc//stardoc:stardoc.bzl", "stardoc") -load("//python/private:util.bzl", "add_tag", "copy_propagating_kwargs") # buildifier: disable=bzl-visibility load("//sphinxdocs/private:sphinx_docs_library_macro.bzl", "sphinx_docs_library") +load("//sphinxdocs/private:util.bzl", "add_tag", "copy_propagating_kwargs") # buildifier: disable=bzl-visibility _StardocInputHelperInfo = provider( doc = "Extracts the single source file from a bzl library.", @@ -73,7 +73,7 @@ def sphinx_stardocs( **kwargs: Additional kwargs to pass onto each `sphinx_stardoc` target """ internal_name = "_{}".format(name) - add_tag(kwargs, "@rules_python//sphinxdocs:sphinx_stardocs") + add_tag(kwargs, "//sphinxdocs:sphinx_stardocs") common_kwargs = copy_propagating_kwargs(kwargs) common_kwargs["target_compatible_with"] = kwargs.get("target_compatible_with") @@ -160,7 +160,7 @@ def sphinx_stardoc( **kwargs: {type}`dict` common args passed onto rules. """ internal_name = "_{}".format(name.lstrip("_")) - add_tag(kwargs, "@rules_python//sphinxdocs:sphinx_stardoc") + add_tag(kwargs, "//sphinxdocs:sphinx_stardoc") common_kwargs = copy_propagating_kwargs(kwargs) common_kwargs["target_compatible_with"] = kwargs.get("target_compatible_with") @@ -171,7 +171,7 @@ def sphinx_stardoc( **common_kwargs ) - stardoc_name = internal_name + "_stardoc" + stardoc_name = internal_name + "__stardoc" # NOTE: The .binaryproto suffix is an optimization. It makes the stardoc() # call avoid performing a copy of the output to the desired name. @@ -186,7 +186,7 @@ def sphinx_stardoc( **common_kwargs ) - pb2md_name = internal_name + "_pb2md" + pb2md_name = internal_name + "__pb2md" _stardoc_proto_to_markdown( name = pb2md_name, src = stardoc_pb, diff --git a/sphinxdocs/sphinxdocs/private/util.bzl b/sphinxdocs/sphinxdocs/private/util.bzl new file mode 100644 index 0000000000..f285a83c13 --- /dev/null +++ b/sphinxdocs/sphinxdocs/private/util.bzl @@ -0,0 +1,58 @@ +"""Utility functions used by sphinxdocs.""" + +load("@bazel_skylib//lib:types.bzl", "types") + +# When bzlmod is enabled, canonical repos names have @@ in them, while under +# workspace builds, there is never a @@ in labels. +BZLMOD_ENABLED = "@@" in str(Label("//sphinxdocs:unused")) + +def copy_propagating_kwargs(from_kwargs, into_kwargs = None): + """Copies args that must be compatible between two targets with a dependency relationship. + + This is intended for when one target depends on another, so they must have + compatible settings such as `testonly` and `compatible_with`. This usually + happens when a macro generates multiple targets, some of which depend + on one another, so their settings must be compatible. + + Args: + from_kwargs: keyword args dict whose common kwarg will be copied. + into_kwargs: optional keyword args dict that the values from `from_kwargs` + will be copied into. The values in this dict will take precedence + over the ones in `from_kwargs` (i.e., if this has `testonly` already + set, then it won't be overwritten). + NOTE: THIS WILL BE MODIFIED IN-PLACE. + + Returns: + Keyword args to use for the depender target derived from the dependency + target. If `into_kwargs` was passed in, then that same object is + returned; this is to facilitate easy `**` expansion. + """ + if into_kwargs == None: + into_kwargs = {} + + # Include tags because people generally expect tags to propagate. + for attr in ("testonly", "tags", "compatible_with", "restricted_to", "target_compatible_with"): + if attr in from_kwargs and attr not in into_kwargs: + into_kwargs[attr] = from_kwargs[attr] + return into_kwargs + +def add_tag(attrs, tag): + """Adds `tag` to `attrs["tags"]`. + + Args: + attrs: dict of keyword args. It is modified in place. + tag: str, the tag to add. + """ + if "tags" in attrs and attrs["tags"] != None: + tags = attrs["tags"] + + # Preserve the input type: this allows a test verifying the underlying + # rule can accept the tuple for the tags argument. + if types.is_tuple(tags): + attrs["tags"] = tags + (tag,) + else: + # List concatenation is necessary because the original value + # may be a frozen list. + attrs["tags"] = tags + [tag] + else: + attrs["tags"] = [tag] diff --git a/sphinxdocs/sphinxdocs/private/visibility.bzl b/sphinxdocs/sphinxdocs/private/visibility.bzl new file mode 100644 index 0000000000..706be45a3b --- /dev/null +++ b/sphinxdocs/sphinxdocs/private/visibility.bzl @@ -0,0 +1,7 @@ +"""Shared code for use with visibility specs.""" + +# Use when a target isn't actually public, but needs public +# visibility to keep Bazel happy. +# Such cases are typically for defaults of rule attributes or macro args that +# get used outside of sphinxdocs itself. +NOT_ACTUALLY_PUBLIC = ["//visibility:public"] diff --git a/sphinxdocs/readthedocs.bzl b/sphinxdocs/sphinxdocs/readthedocs.bzl similarity index 100% rename from sphinxdocs/readthedocs.bzl rename to sphinxdocs/sphinxdocs/readthedocs.bzl diff --git a/sphinxdocs/sphinx.bzl b/sphinxdocs/sphinxdocs/sphinx.bzl similarity index 100% rename from sphinxdocs/sphinx.bzl rename to sphinxdocs/sphinxdocs/sphinx.bzl diff --git a/sphinxdocs/sphinx_docs_library.bzl b/sphinxdocs/sphinxdocs/sphinx_docs_library.bzl similarity index 100% rename from sphinxdocs/sphinx_docs_library.bzl rename to sphinxdocs/sphinxdocs/sphinx_docs_library.bzl diff --git a/sphinxdocs/sphinx_stardoc.bzl b/sphinxdocs/sphinxdocs/sphinx_stardoc.bzl similarity index 100% rename from sphinxdocs/sphinx_stardoc.bzl rename to sphinxdocs/sphinxdocs/sphinx_stardoc.bzl diff --git a/sphinxdocs/src/sphinx_bzl/BUILD.bazel b/sphinxdocs/sphinxdocs/src/sphinx_bzl/BUILD.bazel similarity index 69% rename from sphinxdocs/src/sphinx_bzl/BUILD.bazel rename to sphinxdocs/sphinxdocs/src/sphinx_bzl/BUILD.bazel index 8830315bc3..2dd25e09b3 100644 --- a/sphinxdocs/src/sphinx_bzl/BUILD.bazel +++ b/sphinxdocs/sphinxdocs/src/sphinx_bzl/BUILD.bazel @@ -1,7 +1,7 @@ -load("//python:py_library.bzl", "py_library") +load("@rules_python//python:py_library.bzl", "py_library") package( - default_visibility = ["//:__subpackages__"], + default_visibility = ["//sphinxdocs:__subpackages__"], ) # NOTE: This provides the library on its own, not its dependencies. diff --git a/sphinxdocs/sphinxdocs/src/sphinx_bzl/__init__.py b/sphinxdocs/sphinxdocs/src/sphinx_bzl/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sphinxdocs/src/sphinx_bzl/bzl.py b/sphinxdocs/sphinxdocs/src/sphinx_bzl/bzl.py similarity index 95% rename from sphinxdocs/src/sphinx_bzl/bzl.py rename to sphinxdocs/sphinxdocs/src/sphinx_bzl/bzl.py index 8303b4d2a5..c115737ba5 100644 --- a/sphinxdocs/src/sphinx_bzl/bzl.py +++ b/sphinxdocs/sphinxdocs/src/sphinx_bzl/bzl.py @@ -22,18 +22,25 @@ from typing import Callable, Iterable, TypeVar from docutils import nodes as docutils_nodes -from docutils.parsers.rst import directives as docutils_directives -from docutils.parsers.rst import states -from sphinx import addnodes, builders -from sphinx import directives as sphinx_directives -from sphinx import domains, environment, roles +from docutils.parsers.rst import directives as docutils_directives, states +from sphinx import ( + addnodes, + builders, + directives as sphinx_directives, + domains, + environment, + roles, +) from sphinx.highlighting import lexer_classes from sphinx.locale import _ -from sphinx.util import docfields -from sphinx.util import docutils as sphinx_docutils -from sphinx.util import inspect, logging -from sphinx.util import nodes as sphinx_nodes -from sphinx.util import typing as sphinx_typing +from sphinx.util import ( + docfields, + docutils as sphinx_docutils, + inspect, + logging, + nodes as sphinx_nodes, + typing as sphinx_typing, +) from typing_extensions import TypeAlias, override _logger = logging.getLogger(__name__) @@ -57,7 +64,7 @@ def _log_debug(message, *args): # NOTE: Non-warning log messages go to stdout and are only # visible when -q isn't passed to Sphinx. Note that the sphinx_docs build - # rule passes -q by default; use --//sphinxdocs:quiet=false to disable it. + # rule passes -q by default; use --//:quiet=false to disable it. _logger.debug("%s" + message, _LOG_PREFIX, *args) @@ -455,7 +462,7 @@ def make_field( field_text = item[1][0].astext() parts = [p.strip() for p in field_text.split(",")] field_body = docutils_nodes.field_body() - for _, is_last, part in _position_iter(parts): + for _, is_last, part in _position_iter(parts): # noqa: F402 node = self.make_xref( self.bodyrolename, self._body_domain or domain, @@ -608,8 +615,10 @@ def first_child_with_class_name( root, class_name ) -> typing.Union[None, docutils_nodes.Element]: matches = root.findall( - lambda node: isinstance(node, docutils_nodes.Element) - and class_name in node["classes"] + lambda node: ( + isinstance(node, docutils_nodes.Element) + and class_name in node["classes"] + ) ) found = next(matches, None) return found @@ -766,7 +775,7 @@ def make_xref(name, title=None): return obj_id def _signature_add_object_type(self, sig_node: addnodes.desc_signature): - if sig_object_type := self._get_signature_object_type(): + if sig_object_type := self._get_signature_object_type(): # noqa: F841 sig_node += addnodes.desc_annotation("", self._get_signature_object_type()) sig_node += addnodes.desc_sig_space() @@ -1713,6 +1722,12 @@ def add_object(self, entry: _ObjectEntry, alt_names=None) -> None: ) if entry.full_id in self.data["objects"]: existing = self.data["objects"][entry.full_id] + # If the objects are identical, then it's fine. This can happen + # when a doc is re-parsed (e.g. during a query) or if a symbol + # is documented multiple times in the same file. + if existing.to_get_objects_tuple() == entry.to_get_objects_tuple(): + return + raise Exception( f"Object {entry.full_id} already registered: " + f"existing={existing}, incoming={entry}" @@ -1740,6 +1755,27 @@ def add_object(self, entry: _ObjectEntry, alt_names=None) -> None: self.data["doc_names"].setdefault(docname, {}) self.data["doc_names"][docname][base_name] = entry + @override + def clear_doc(self, docname: str) -> None: + if docname not in self.data["doc_names"]: + return + for base_name, entry in self.data["doc_names"][docname].items(): + if entry.full_id in self.data["objects"]: + del self.data["objects"][entry.full_id] + + if entry.full_id in self.data["objects_by_type"].get(entry.object_type, {}): + del self.data["objects_by_type"][entry.object_type][entry.full_id] + + # We can't easily reverse the mapping for alt_names, so we have + # to iterate over all of them. This is potentially slow, but + # clear_doc isn't called often. + for alt_name, entries in list(self.data["alt_names"].items()): + if entry.full_id in entries: + del entries[entry.full_id] + if not entries: + del self.data["alt_names"][alt_name] + del self.data["doc_names"][docname] + def merge_domaindata( self, docnames: list[str], otherdata: dict[str, typing.Any] ) -> None: @@ -1758,19 +1794,44 @@ def merge_domaindata( def _on_missing_reference(app, env: environment.BuildEnvironment, node, contnode): + """Handle missing references + + There are two main cases this is designed to handle: + 1. Psuedo-types, like None + 2. External references that aren't exact matches, e.g. using + `--stamp=true` to refer to the Bazel builtin stamp flag. + """ + target = node["reftarget"] if node["refdomain"] != "bzl": return None - if node["reftype"] != "type": + if node["reftype"] not in ("type", "flag", "obj"): return None # There's no Bazel docs for None, so prevent missing xrefs warning - if node["reftarget"] == "None": + if target == "None": return contnode - # Any and object are just conventions from Python, but useful for - # indicating what something is in Starlark, so treat them specially. - if node["reftarget"] in ("Any", "object"): + # Useful psuedo-types + if target in ("Any", "object", "collection"): return contnode + + original_target = target + new_target = target + + # Normalize --foo flag references + new_target = new_target.lstrip("-") + # Allow foo() style references + new_target, _, _ = new_target.partition("(") + # Allow --foo=bar references + new_target, _, _ = new_target.partition("=") + + if new_target != original_target: + # Access the intersphinx extension's internal mapping + # we try to resolve the reference again with the stripped name + from sphinx.ext.intersphinx import missing_reference + + node["reftarget"] = new_target + return missing_reference(app, env, node, contnode) return None diff --git a/sphinxdocs/tests/__init__.py b/sphinxdocs/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sphinxdocs/tests/proto_to_markdown/BUILD.bazel b/sphinxdocs/tests/proto_to_markdown/BUILD.bazel index 09f537472c..632d6d946f 100644 --- a/sphinxdocs/tests/proto_to_markdown/BUILD.bazel +++ b/sphinxdocs/tests/proto_to_markdown/BUILD.bazel @@ -12,13 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -load("//python:py_test.bzl", "py_test") -load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER") # buildifier: disable=bzl-visibility +load("@rules_python//python:py_test.bzl", "py_test") py_test( name = "proto_to_markdown_test", srcs = ["proto_to_markdown_test.py"], - target_compatible_with = [] if IS_BAZEL_7_OR_HIGHER else ["@platforms//:incompatible"], deps = [ "//sphinxdocs/private:proto_to_markdown_lib", "@dev_pip//absl_py", diff --git a/sphinxdocs/tests/proto_to_markdown/proto_to_markdown_test.py b/sphinxdocs/tests/proto_to_markdown/proto_to_markdown_test.py index da6edb21d4..d88d2bf127 100644 --- a/sphinxdocs/tests/proto_to_markdown/proto_to_markdown_test.py +++ b/sphinxdocs/tests/proto_to_markdown/proto_to_markdown_test.py @@ -13,13 +13,11 @@ # limitations under the License. import io -import re from absl.testing import absltest from google.protobuf import text_format -from stardoc.proto import stardoc_output_pb2 - from sphinxdocs.private import proto_to_markdown +from stardoc.proto import stardoc_output_pb2 _EVERYTHING_MODULE = """\ module_docstring: "MODULE_DOC_STRING" diff --git a/sphinxdocs/tests/sphinx_docs/BUILD.bazel b/sphinxdocs/tests/sphinx_docs/BUILD.bazel index f9c82967c1..33b98ec585 100644 --- a/sphinxdocs/tests/sphinx_docs/BUILD.bazel +++ b/sphinxdocs/tests/sphinx_docs/BUILD.bazel @@ -1,5 +1,4 @@ load("@bazel_skylib//rules:build_test.bzl", "build_test") -load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER") # buildifier: disable=bzl-visibility load("//sphinxdocs:sphinx.bzl", "sphinx_build_binary", "sphinx_docs") load(":defs.bzl", "gen_directory") @@ -13,7 +12,7 @@ _TARGET_COMPATIBLE_WITH = select({ "@platforms//os:linux": [], "@platforms//os:macos": [], "//conditions:default": ["@platforms//:incompatible"], -}) if IS_BAZEL_7_OR_HIGHER else ["@platforms//:incompatible"] +}) sphinx_docs( name = "docs", diff --git a/sphinxdocs/tests/sphinx_stardoc/BUILD.bazel b/sphinxdocs/tests/sphinx_stardoc/BUILD.bazel index e3a68ea225..2cbc773f77 100644 --- a/sphinxdocs/tests/sphinx_stardoc/BUILD.bazel +++ b/sphinxdocs/tests/sphinx_stardoc/BUILD.bazel @@ -1,7 +1,6 @@ load("@bazel_skylib//:bzl_library.bzl", "bzl_library") load("@bazel_skylib//rules:build_test.bzl", "build_test") -load("//python:py_test.bzl", "py_test") -load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER") # buildifier: disable=bzl-visibility +load("@rules_python//python:py_test.bzl", "py_test") load("//sphinxdocs:sphinx.bzl", "sphinx_build_binary", "sphinx_docs") load("//sphinxdocs:sphinx_stardoc.bzl", "sphinx_stardoc", "sphinx_stardocs") @@ -15,7 +14,7 @@ _TARGET_COMPATIBLE_WITH = select({ "@platforms//os:linux": [], "@platforms//os:macos": [], "//conditions:default": ["@platforms//:incompatible"], -}) if IS_BAZEL_7_OR_HIGHER else ["@platforms//:incompatible"] +}) sphinx_docs( name = "docs", @@ -70,7 +69,6 @@ sphinx_stardoc( deps = [":func_and_providers_bzl"], ) -# A bzl_library with multiple sources bzl_library( name = "func_and_providers_bzl", srcs = [ @@ -90,6 +88,7 @@ bzl_library( srcs = ["bzl_typedef.bzl"], ) +# A bzl_library with multiple sources sphinx_build_binary( name = "sphinx-build", tags = ["manual"], # Only needed as part of sphinx doc building diff --git a/sphinxdocs/tests/sphinx_stardoc/__init__.py b/sphinxdocs/tests/sphinx_stardoc/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sphinxdocs/tests/sphinx_stardoc/index.md b/sphinxdocs/tests/sphinx_stardoc/index.md index 43ef14f55a..51a7a671ec 100644 --- a/sphinxdocs/tests/sphinx_stardoc/index.md +++ b/sphinxdocs/tests/sphinx_stardoc/index.md @@ -5,7 +5,7 @@ This is a set of documents to test the sphinx_stardoc extension. To build and view these docs, run: ``` -bazel run //sphinxdocs/tests/sphinx_stardoc:docs.serve +bazel run //tests/sphinx_stardoc:docs.serve ``` This will build the docs and start an HTTP server where they can be viewed. @@ -14,7 +14,7 @@ To aid the edit/debug cycle, `ibazel` can be used to automatically rebuild the HTML: ``` -ibazel build //sphinxdocs/tests/sphinx_stardoc:docs +ibazel build //tests/sphinx_stardoc:docs ``` :::{toctree} diff --git a/sphinxdocs/tests/sphinx_stardoc/sphinx_output_test.py b/sphinxdocs/tests/sphinx_stardoc/sphinx_output_test.py index c78089ac14..650e0134d3 100644 --- a/sphinxdocs/tests/sphinx_stardoc/sphinx_output_test.py +++ b/sphinxdocs/tests/sphinx_stardoc/sphinx_output_test.py @@ -3,7 +3,7 @@ from absl.testing import absltest, parameterized -from sphinxdocs.tests import sphinx_stardoc +import tests.sphinx_stardoc as sphinx_stardoc class SphinxOutputTest(parameterized.TestCase): @@ -50,21 +50,65 @@ def _doc_element(self, doc): ("short_provider", "LangInfo", "provider.html#LangInfo"), ("short_tag_class", "myext.mytag", "module_extension.html#myext.mytag"), ("full_norepo_func", "//lang:function.bzl%myfunc", "function.html#myfunc"), - ("full_norepo_func_arg", "//lang:function.bzl%myfunc.arg1", "function.html#myfunc.arg1"), + ( + "full_norepo_func_arg", + "//lang:function.bzl%myfunc.arg1", + "function.html#myfunc.arg1", + ), ("full_norepo_rule", "//lang:rule.bzl%my_rule", "rule.html#my_rule"), - ("full_norepo_rule_attr", "//lang:rule.bzl%my_rule.ra1", "rule.html#my_rule.ra1"), - ("full_norepo_provider", "//lang:provider.bzl%LangInfo", "provider.html#LangInfo"), + ( + "full_norepo_rule_attr", + "//lang:rule.bzl%my_rule.ra1", + "rule.html#my_rule.ra1", + ), + ( + "full_norepo_provider", + "//lang:provider.bzl%LangInfo", + "provider.html#LangInfo", + ), ("full_norepo_aspect", "//lang:aspect.bzl%myaspect", "aspect.html#myaspect"), ("full_norepo_target", "//lang:relativetarget", "target.html#relativetarget"), - ("full_repo_func", "@testrepo//lang:function.bzl%myfunc", "function.html#myfunc"), - ("full_repo_func_arg", "@testrepo//lang:function.bzl%myfunc.arg1", "function.html#myfunc.arg1"), + ( + "full_repo_func", + "@testrepo//lang:function.bzl%myfunc", + "function.html#myfunc", + ), + ( + "full_repo_func_arg", + "@testrepo//lang:function.bzl%myfunc.arg1", + "function.html#myfunc.arg1", + ), ("full_repo_rule", "@testrepo//lang:rule.bzl%my_rule", "rule.html#my_rule"), - ("full_repo_rule_attr", "@testrepo//lang:rule.bzl%my_rule.ra1", "rule.html#my_rule.ra1"), - ("full_repo_provider", "@testrepo//lang:provider.bzl%LangInfo", "provider.html#LangInfo"), - ("full_repo_aspect", "@testrepo//lang:aspect.bzl%myaspect", "aspect.html#myaspect"), - ("full_repo_target", "@testrepo//lang:relativetarget", "target.html#relativetarget"), - ("tag_class_attr_using_attr_role", "myext.mytag.ta1", "module_extension.html#myext.mytag.ta1"), - ("tag_class_attr_using_attr_role_just_attr_name", "ta1", "module_extension.html#myext.mytag.ta1"), + ( + "full_repo_rule_attr", + "@testrepo//lang:rule.bzl%my_rule.ra1", + "rule.html#my_rule.ra1", + ), + ( + "full_repo_provider", + "@testrepo//lang:provider.bzl%LangInfo", + "provider.html#LangInfo", + ), + ( + "full_repo_aspect", + "@testrepo//lang:aspect.bzl%myaspect", + "aspect.html#myaspect", + ), + ( + "full_repo_target", + "@testrepo//lang:relativetarget", + "target.html#relativetarget", + ), + ( + "tag_class_attr_using_attr_role", + "myext.mytag.ta1", + "module_extension.html#myext.mytag.ta1", + ), + ( + "tag_class_attr_using_attr_role_just_attr_name", + "ta1", + "module_extension.html#myext.mytag.ta1", + ), ("file_without_repo", "//lang:rule.bzl", "rule.html"), ("file_with_repo", "@testrepo//lang:rule.bzl", "rule.html"), ("package_absolute", "//lang", "target.html"), diff --git a/sphinxdocs/tests/sphinx_stardoc/xrefs.md b/sphinxdocs/tests/sphinx_stardoc/xrefs.md index bbd415ce19..9893c32023 100644 --- a/sphinxdocs/tests/sphinx_stardoc/xrefs.md +++ b/sphinxdocs/tests/sphinx_stardoc/xrefs.md @@ -36,7 +36,7 @@ Various tests of cross referencing support ## Using origin keys -* provider using `{type}`: {type}`"@rules_python//sphinxdocs/tests/sphinx_stardoc:bzl_rule.bzl%GenericInfo"` +* provider using `{type}`: {type}`"//tests/sphinx_stardoc:bzl_rule.bzl%GenericInfo"` ## Any xref diff --git a/tests/BUILD.bazel b/tests/BUILD.bazel index 0fb8e88135..e7dbef65d8 100644 --- a/tests/BUILD.bazel +++ b/tests/BUILD.bazel @@ -1,6 +1,4 @@ load("@bazel_skylib//rules:build_test.bzl", "build_test") -load("@rules_shell//shell:sh_test.bzl", "sh_test") -load("//:version.bzl", "BAZEL_VERSION") package(default_visibility = ["//visibility:public"]) @@ -27,29 +25,3 @@ build_test( "//python/entry_points:py_console_script_binary_bzl", ], ) - -genrule( - name = "assert_bazelversion", - srcs = ["//:.bazelversion"], - outs = ["assert_bazelversion_test.sh"], - cmd = """\ -set -o errexit -o nounset -o pipefail -current=$$(cat "$(execpath //:.bazelversion)") -cat > "$@" <&2 echo "ERROR: current bazel version '$${{current}}' is not the expected '{expected}'" - exit 1 -fi -EOF -""".format( - expected = BAZEL_VERSION, - ), - executable = True, -) - -sh_test( - name = "assert_bazelversion_test", - srcs = [":assert_bazelversion_test.sh"], -) diff --git a/tests/api/py_common/py_common_tests.bzl b/tests/api/py_common/py_common_tests.bzl index 572028b2a6..028da6cc37 100644 --- a/tests/api/py_common/py_common_tests.bzl +++ b/tests/api/py_common/py_common_tests.bzl @@ -13,7 +13,6 @@ # limitations under the License. """py_common tests.""" -load("@rules_python_internal//:rules_python_config.bzl", "config") load("@rules_testing//lib:analysis_test.bzl", "analysis_test") load("@rules_testing//lib:test_suite.bzl", "test_suite") load("@rules_testing//lib:util.bzl", rt_util = "util") @@ -41,13 +40,11 @@ def _test_merge_py_infos_impl(env, target): py_common = _py_common.get(env.ctx) py1 = py_common.PyInfoBuilder() - if config.enable_pystar: - py1.direct_pyc_files.add(f1_pyc) + py1.direct_pyc_files.add(f1_pyc) py1.transitive_sources.add(f1_py) py2 = py_common.PyInfoBuilder() - if config.enable_pystar: - py1.direct_pyc_files.add(f2_pyc) + py1.direct_pyc_files.add(f2_pyc) py2.transitive_sources.add(f2_py) actual = py_info_subject( @@ -56,8 +53,7 @@ def _test_merge_py_infos_impl(env, target): ) actual.transitive_sources().contains_exactly([f1_py.path, f2_py.path]) - if config.enable_pystar: - actual.direct_pyc_files().contains_exactly([f1_pyc.path, f2_pyc.path]) + actual.direct_pyc_files().contains_exactly([f1_pyc.path, f2_pyc.path]) _tests.append(_test_merge_py_infos) diff --git a/tests/base_rules/precompile/precompile_tests.bzl b/tests/base_rules/precompile/precompile_tests.bzl index fe5c165648..bff994aa1a 100644 --- a/tests/base_rules/precompile/precompile_tests.bzl +++ b/tests/base_rules/precompile/precompile_tests.bzl @@ -14,7 +14,6 @@ """Tests for precompiling behavior.""" -load("@rules_python_internal//:rules_python_config.bzl", rp_config = "config") load("@rules_testing//lib:analysis_test.bzl", "analysis_test") load("@rules_testing//lib:test_suite.bzl", "test_suite") load("@rules_testing//lib:truth.bzl", "matching") @@ -42,9 +41,6 @@ _COMMON_CONFIG_SETTINGS = { _tests = [] def _test_executable_precompile_attr_enabled_setup(name, py_rule, **kwargs): - if not rp_config.enable_pystar: - rt_util.skip_test(name = name) - return rt_util.helper_target( py_rule, name = name + "_subject", @@ -112,9 +108,6 @@ def _test_precompile_enabled_py_test(name): _tests.append(_test_precompile_enabled_py_test) def _test_precompile_enabled_py_library_setup(name, impl, config_settings): - if not rp_config.enable_pystar: - rt_util.skip_test(name = name) - return rt_util.helper_target( py_library, name = name + "_subject", @@ -178,9 +171,6 @@ def _test_precompile_enabled_py_library_add_to_runfiles_enabled_impl(env, target _tests.append(_test_precompile_enabled_py_library_add_to_runfiles_enabled) def _test_pyc_only(name): - if not rp_config.enable_pystar: - rt_util.skip_test(name = name) - return rt_util.helper_target( py_binary, name = name + "_subject", @@ -231,9 +221,6 @@ def _test_pyc_only_impl(env, target): ) def _test_precompiler_action(name): - if not rp_config.enable_pystar: - rt_util.skip_test(name = name) - return rt_util.helper_target( py_binary, name = name + "_subject", @@ -325,9 +312,6 @@ def _verify_runfiles(contains_patterns, not_contains_patterns): return _verify_runfiles_impl def _test_precompile_flag_enabled_pyc_collection_attr_include_pyc(name): - if not rp_config.enable_pystar: - rt_util.skip_test(name = name) - return _setup_precompile_flag_pyc_collection_attr_interaction( name = name, precompile_flag = "enabled", @@ -351,9 +335,6 @@ def _test_precompile_flag_enabled_pyc_collection_attr_disabled(name): """Verify that a binary can opt-out of using implicit pycs even when precompiling is enabled by default. """ - if not rp_config.enable_pystar: - rt_util.skip_test(name = name) - return _setup_precompile_flag_pyc_collection_attr_interaction( name = name, precompile_flag = "enabled", @@ -376,9 +357,6 @@ _tests.append(_test_precompile_flag_enabled_pyc_collection_attr_disabled) def _test_precompile_flag_disabled_pyc_collection_attr_include_pyc(name): """Verify that a binary can opt-in to using pycs even when precompiling is disabled by default.""" - if not rp_config.enable_pystar: - rt_util.skip_test(name = name) - return _setup_precompile_flag_pyc_collection_attr_interaction( name = name, precompile_flag = "disabled", @@ -398,9 +376,6 @@ def _test_precompile_flag_disabled_pyc_collection_attr_include_pyc(name): _tests.append(_test_precompile_flag_disabled_pyc_collection_attr_include_pyc) def _test_precompile_flag_disabled_pyc_collection_attr_disabled(name): - if not rp_config.enable_pystar: - rt_util.skip_test(name = name) - return _setup_precompile_flag_pyc_collection_attr_interaction( name = name, precompile_flag = "disabled", @@ -424,9 +399,6 @@ def _test_pyc_collection_disabled_library_omit_source(name): """Verify that, when a binary doesn't include implicit pyc files, libraries that set omit_source still have the py source file included. """ - if not rp_config.enable_pystar: - rt_util.skip_test(name = name) - return rt_util.helper_target( py_binary, name = name + "_subject", @@ -469,9 +441,6 @@ def _test_pyc_collection_disabled_library_omit_source_impl(env, target): _tests.append(_test_pyc_collection_disabled_library_omit_source) def _test_pyc_collection_include_dep_omit_source(name): - if not rp_config.enable_pystar: - rt_util.skip_test(name = name) - return rt_util.helper_target( py_binary, name = name + "_subject", @@ -513,9 +482,6 @@ def _test_pyc_collection_include_dep_omit_source_impl(env, target): _tests.append(_test_pyc_collection_include_dep_omit_source) def _test_precompile_attr_inherit_pyc_collection_disabled_precompile_flag_enabled(name): - if not rp_config.enable_pystar: - rt_util.skip_test(name = name) - return rt_util.helper_target( py_binary, name = name + "_subject", diff --git a/tests/base_rules/py_executable_base_tests.bzl b/tests/base_rules/py_executable_base_tests.bzl index 4e451289dc..f6b3c9bb60 100644 --- a/tests/base_rules/py_executable_base_tests.bzl +++ b/tests/base_rules/py_executable_base_tests.bzl @@ -19,21 +19,20 @@ load("@rules_testing//lib:analysis_test.bzl", "analysis_test") load("@rules_testing//lib:truth.bzl", "matching") load("@rules_testing//lib:util.bzl", rt_util = "util") load("//python:py_executable_info.bzl", "PyExecutableInfo") +load("//python:py_info.bzl", "PyInfo") +load("//python:py_library.bzl", "py_library") +load("//python/private:common.bzl", "maybe_builtin_build_python_zip") # buildifier: disable=bzl-visibility load("//python/private:common_labels.bzl", "labels") # buildifier: disable=bzl-visibility load("//python/private:reexports.bzl", "BuiltinPyRuntimeInfo") # buildifier: disable=bzl-visibility -load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER") # buildifier: disable=bzl-visibility load("//tests/base_rules:base_tests.bzl", "create_base_tests") load("//tests/base_rules:util.bzl", "WINDOWS_ATTR", pt_util = "util") load("//tests/support:py_executable_info_subject.bzl", "PyExecutableInfoSubject") -load("//tests/support:support.bzl", "CC_TOOLCHAIN", "CROSSTOOL_TOP", "LINUX_X86_64", "WINDOWS_X86_64") +load("//tests/support:support.bzl", "CC_TOOLCHAIN", "CROSSTOOL_TOP") +load("//tests/support/platforms:platforms.bzl", "platform_targets") _tests = [] def _test_basic_windows(name, config): - if rp_config.enable_pystar: - target_compatible_with = [] - else: - target_compatible_with = ["@platforms//:incompatible"] rt_util.helper_target( config.rule, name = name + "_subject", @@ -45,25 +44,20 @@ def _test_basic_windows(name, config): impl = _test_basic_windows_impl, target = name + "_subject", config_settings = { - # NOTE: The default for this flag is based on the Bazel host OS, not - # the target platform. For windows, it defaults to true, so force - # it to that to match behavior when this test runs on other - # platforms. - "//command_line_option:build_python_zip": "true", "//command_line_option:cpu": "windows_x86_64", "//command_line_option:crosstool_top": CROSSTOOL_TOP, - "//command_line_option:extra_execution_platforms": [WINDOWS_X86_64], + "//command_line_option:extra_execution_platforms": [platform_targets.WINDOWS_X86_64], "//command_line_option:extra_toolchains": [CC_TOOLCHAIN], - "//command_line_option:platforms": [WINDOWS_X86_64], + "//command_line_option:platforms": [platform_targets.WINDOWS_X86_64], }, - attr_values = {"target_compatible_with": target_compatible_with}, + attr_values = {}, ) def _test_basic_windows_impl(env, target): target = env.expect.that_target(target) target.executable().path().contains(".exe") target.runfiles().contains_predicate(matching.str_endswith( - target.meta.format_str("/{name}.zip"), + target.meta.format_str("/{name}"), )) target.runfiles().contains_predicate(matching.str_endswith( target.meta.format_str("/{name}.exe"), @@ -72,14 +66,11 @@ def _test_basic_windows_impl(env, target): _tests.append(_test_basic_windows) def _test_basic_zip(name, config): - if rp_config.enable_pystar: - target_compatible_with = select({ - # Disable the new test on windows because we have _test_basic_windows. - "@platforms//os:windows": ["@platforms//:incompatible"], - "//conditions:default": [], - }) - else: - target_compatible_with = ["@platforms//:incompatible"] + target_compatible_with = select({ + # Disable the new test on windows because we have _test_basic_windows. + "@platforms//os:windows": ["@platforms//:incompatible"], + "//conditions:default": [], + }) rt_util.helper_target( config.rule, name = name + "_subject", @@ -95,13 +86,15 @@ def _test_basic_zip(name, config): # the target platform. For windows, it defaults to true, so force # it to that to match behavior when this test runs on other # platforms. - "//command_line_option:build_python_zip": "true", + # Pass value to both native and starlark versions of the flag until + # the native one is removed. + labels.BUILD_PYTHON_ZIP: True, "//command_line_option:cpu": "linux_x86_64", "//command_line_option:crosstool_top": CROSSTOOL_TOP, - "//command_line_option:extra_execution_platforms": [LINUX_X86_64], + "//command_line_option:extra_execution_platforms": [platform_targets.LINUX_X86_64], "//command_line_option:extra_toolchains": [CC_TOOLCHAIN], - "//command_line_option:platforms": [LINUX_X86_64], - }, + "//command_line_option:platforms": [platform_targets.LINUX_X86_64], + } | maybe_builtin_build_python_zip("true"), attr_values = {"target_compatible_with": target_compatible_with}, ) @@ -116,6 +109,52 @@ def _test_basic_zip_impl(env, target): _tests.append(_test_basic_zip) +def _test_config_settings_extra_toolchains(name, config): + rt_util.helper_target( + config.rule, + name = name + "_subject", + srcs = ["main.py"], + main = "main.py", + config_settings = { + "//command_line_option:extra_toolchains": "{tc},{tc}".format(tc = CC_TOOLCHAIN), + }, + ) + analysis_test( + name = name, + impl = _test_config_settings_extra_toolchains_impl, + target = name + "_subject", + ) + +def _test_config_settings_extra_toolchains_impl(env, target): + # If we got here, it means analysis succeeded, which implies the transition + # successfully parsed the CSV string into a list. + env.expect.that_target(target).has_provider(PyInfo) + +_tests.append(_test_config_settings_extra_toolchains) + +def _test_cross_compile_to_unix(name, config): + rt_util.helper_target( + config.rule, + name = name + "_subject", + main_module = "dummy", + ) + analysis_test( + name = name, + impl = _test_cross_compile_to_unix_impl, + target = name + "_subject", + # Cross-compilation of py_test fails since the default test toolchain + # requires an execution platform that matches the target platform. + config_settings = { + "//command_line_option:platforms": [platform_targets.EXOTIC_UNIX], + } if rp_config.bazel_9_or_later and not "py_test" in str(config.rule) else {}, + expect_failure = True, + ) + +def _test_cross_compile_to_unix_impl(_env, _target): + pass + +_tests.append(_test_cross_compile_to_unix) + def _test_executable_in_runfiles(name, config): rt_util.helper_target( config.rule, @@ -140,14 +179,99 @@ def _test_executable_in_runfiles_impl(env, target): "{workspace}/{package}/{test_name}_subject" + exe, ]) - if rp_config.enable_pystar: - py_exec_info = env.expect.that_target(target).provider(PyExecutableInfo, factory = PyExecutableInfoSubject.new) - py_exec_info.main().path().contains("_subject.py") - py_exec_info.interpreter_path().contains("python") - py_exec_info.runfiles_without_exe().contains_none_of([ - "{workspace}/{package}/{test_name}_subject" + exe, - "{workspace}/{package}/{test_name}_subject", - ]) + py_exec_info = env.expect.that_target(target).provider(PyExecutableInfo, factory = PyExecutableInfoSubject.new) + py_exec_info.main().path().contains("_subject.py") + py_exec_info.interpreter_path().contains("python") + py_exec_info.runfiles_without_exe().contains_none_of([ + "{workspace}/{package}/{test_name}_subject" + exe, + "{workspace}/{package}/{test_name}_subject", + ]) + +def _test_debugger(name, config): + # Using imports + rt_util.helper_target( + py_library, + name = name + "_debugger", + imports = ["."], + srcs = [rt_util.empty_file(name + "_debugger.py")], + ) + + rt_util.helper_target( + config.rule, + name = name + "_subject", + srcs = [rt_util.empty_file(name + "_subject.py")], + config_settings = { + # config_settings requires a fully qualified label + labels.DEBUGGER: "//{}:{}_debugger".format(native.package_name(), name), + }, + ) + + # Using venv + rt_util.helper_target( + py_library, + name = name + "_debugger_venv", + imports = ["site-packages"], + experimental_venvs_site_packages = "@rules_python//python/config_settings:venvs_site_packages", + srcs = [rt_util.empty_file("site-packages/" + name + "_debugger_venv.py")], + ) + + rt_util.helper_target( + config.rule, + name = name + "_subject_venv", + srcs = [rt_util.empty_file(name + "_subject_venv.py")], + config_settings = { + # config_settings requires a fully qualified label + labels.DEBUGGER: "//{}:{}_debugger_venv".format(native.package_name(), name), + }, + ) + + analysis_test( + name = name, + impl = _test_debugger_impl, + targets = { + "exec_target": name + "_subject", + "target": name + "_subject", + "target_venv": name + "_subject_venv", + }, + attrs = { + "exec_target": attr.label(cfg = "exec"), + }, + config_settings = { + labels.VENVS_SITE_PACKAGES: "yes", + labels.PYTHON_VERSION: "3.13", + }, + ) + +_tests.append(_test_debugger) + +def _test_debugger_impl(env, targets): + # 1. Subject + + # Check the file from debugger dep is injected. + env.expect.that_target(targets.target).runfiles().contains_at_least([ + "{workspace}/{package}/{test_name}_debugger.py", + ]) + + # #3481: Ensure imports are setup correcty. + meta = env.expect.meta.derive(format_str_kwargs = {"package": targets.target.label.package}) + env.expect.that_target(targets.target).has_provider(PyInfo) + imports = targets.target[PyInfo].imports.to_list() + env.expect.that_collection(imports).contains(meta.format_str("{workspace}/{package}")) + + # 2. Subject venv + + # #3481: Ensure that venv site-packages is setup correctly, if the dependency is coming + # from pip integration. + env.expect.that_target(targets.target_venv).runfiles().contains_predicate( + matching.str_endswith("site-packages/test_debugger_debugger_venv.py"), + ) + + # 3. Subject exec + + # Ensure that tools don't inherit debugger. + env.expect.that_target(targets.exec_target).runfiles().not_contains( + "{workspace}/{package}/{test_name}_debugger.py", + ) def _test_default_main_can_be_generated(name, config): rt_util.helper_target( @@ -188,11 +312,6 @@ def _test_default_main_can_have_multiple_path_segments_impl(env, target): ) def _test_default_main_must_be_in_srcs(name, config): - # Bazel 5 will crash with a Java stacktrace when the native Python - # rules have an error. - if not pt_util.is_bazel_6_or_higher(): - rt_util.skip_test(name = name) - return rt_util.helper_target( config.rule, name = name + "_subject", @@ -213,11 +332,6 @@ def _test_default_main_must_be_in_srcs_impl(env, target): ) def _test_default_main_cannot_be_ambiguous(name, config): - # Bazel 5 will crash with a Java stacktrace when the native Python - # rules have an error. - if not pt_util.is_bazel_6_or_higher(): - rt_util.skip_test(name = name) - return rt_util.helper_target( config.rule, name = name + "_subject", @@ -260,11 +374,6 @@ def _test_explicit_main_impl(env, target): ) def _test_explicit_main_cannot_be_ambiguous(name, config): - # Bazel 5 will crash with a Java stacktrace when the native Python - # rules have an error. - if not pt_util.is_bazel_6_or_higher(): - rt_util.skip_test(name = name) - return rt_util.helper_target( config.rule, name = name + "_subject", @@ -285,7 +394,7 @@ def _test_explicit_main_cannot_be_ambiguous_impl(env, target): matching.str_matches("foo.py*matches multiple"), ) -def _test_files_to_build(name, config): +def _test_default_outputs(name, config): rt_util.helper_target( config.rule, name = name + "_subject", @@ -293,14 +402,14 @@ def _test_files_to_build(name, config): ) analysis_test( name = name, - impl = _test_files_to_build_impl, + impl = _test_default_outputs_impl, target = name + "_subject", attrs = WINDOWS_ATTR, ) -_tests.append(_test_files_to_build) +_tests.append(_test_default_outputs) -def _test_files_to_build_impl(env, target): +def _test_default_outputs_impl(env, target): default_outputs = env.expect.that_target(target).default_outputs() if pt_util.is_windows(env): default_outputs.contains("{package}/{test_name}_subject.exe") @@ -310,22 +419,16 @@ def _test_files_to_build_impl(env, target): "{package}/{test_name}_subject.py", ]) - if IS_BAZEL_7_OR_HIGHER: - # As of Bazel 7, the first default output is the executable, so - # verify that is the case. rules_testing - # DepsetFileSubject.contains_exactly doesn't provide an in_order() - # call, nor access to the underlying depset, so we have to do things - # manually. - first_default_output = target[DefaultInfo].files.to_list()[0] - executable = target[DefaultInfo].files_to_run.executable - env.expect.that_file(first_default_output).equals(executable) + # As of Bazel 7, the first default output is the executable, so + # verify that is the case. rules_testing + # DepsetFileSubject.contains_exactly doesn't provide an in_order() + # call, nor access to the underlying depset, so we have to do things + # manually. + first_default_output = target[DefaultInfo].files.to_list()[0] + executable = target[DefaultInfo].files_to_run.executable + env.expect.that_file(first_default_output).equals(executable) def _test_name_cannot_end_in_py(name, config): - # Bazel 5 will crash with a Java stacktrace when the native Python - # rules have an error. - if not pt_util.is_bazel_6_or_higher(): - rt_util.skip_test(name = name) - return rt_util.helper_target( config.rule, name = name + "_subject.py", @@ -357,8 +460,8 @@ def _test_main_module_bootstrap_system_python(name, config): target = name + "_subject", config_settings = { labels.BOOTSTRAP_IMPL: "system_python", - "//command_line_option:extra_execution_platforms": ["@bazel_tools//tools:host_platform", LINUX_X86_64], - "//command_line_option:platforms": [LINUX_X86_64], + "//command_line_option:extra_execution_platforms": ["@bazel_tools//tools:host_platform", platform_targets.LINUX_X86_64], + "//command_line_option:platforms": [platform_targets.LINUX_X86_64], }, ) @@ -381,8 +484,8 @@ def _test_main_module_bootstrap_script(name, config): target = name + "_subject", config_settings = { labels.BOOTSTRAP_IMPL: "script", - "//command_line_option:extra_execution_platforms": ["@bazel_tools//tools:host_platform", LINUX_X86_64], - "//command_line_option:platforms": [LINUX_X86_64], + "//command_line_option:extra_execution_platforms": ["@bazel_tools//tools:host_platform", platform_targets.LINUX_X86_64], + "//command_line_option:platforms": [platform_targets.LINUX_X86_64], }, ) @@ -416,6 +519,74 @@ def _test_py_runtime_info_provided_impl(env, target): _tests.append(_test_py_runtime_info_provided) +def _test_venv_output_prefix_with_path_separators(name, config): + rt_util.helper_target( + config.rule, + name = name + "/foo/tool", + srcs = ["main.py"], + main = "main.py", + ) + rt_util.helper_target( + config.rule, + name = name + "/bar/tool", + srcs = ["main.py"], + main = "main.py", + ) + rt_util.helper_target( + config.rule, + name = name + "/foo_tool", + srcs = ["main.py"], + main = "main.py", + ) + analysis_test( + name = name, + impl = _test_venv_output_prefix_with_path_separators_impl, + targets = { + "bar": name + "/bar/tool", + "foo": name + "/foo/tool", + "foo_underscore": name + "/foo_tool", + }, + ) + +def _test_venv_output_prefix_with_path_separators_impl(env, targets): + for target in [targets.foo, targets.bar, targets.foo_underscore]: + target = env.expect.that_target(target) + venv_file = "*/_{}.venv/*site-packages/bazel.pth".format( + target.meta.format_str("{name}"), + ) + target.runfiles().contains_predicate(matching.str_matches(venv_file)) + +_tests.append(_test_venv_output_prefix_with_path_separators) + +def _test_windows_target_with_path_separators(name, config): + rt_util.helper_target( + config.rule, + name = name + "/nested_subject", + srcs = ["main.py"], + main = "main.py", + ) + analysis_test( + name = name, + impl = _test_windows_target_with_path_separators_impl, + target = name + "/nested_subject", + config_settings = { + "//command_line_option:cpu": "windows_x86_64", + "//command_line_option:crosstool_top": CROSSTOOL_TOP, + "//command_line_option:extra_execution_platforms": [platform_targets.WINDOWS_X86_64], + "//command_line_option:extra_toolchains": [CC_TOOLCHAIN], + "//command_line_option:platforms": [platform_targets.WINDOWS_X86_64], + }, + attr_values = {}, + ) + +def _test_windows_target_with_path_separators_impl(env, target): + target = env.expect.that_target(target) + target.runfiles().contains_predicate(matching.str_endswith( + target.meta.format_str("/{name}"), + )) + +_tests.append(_test_windows_target_with_path_separators) + # ===== # You were gonna add a test at the end, weren't you? # Nope. Please keep them sorted; put it in its alphabetical location. diff --git a/tests/base_rules/py_info/py_info_tests.bzl b/tests/base_rules/py_info/py_info_tests.bzl index aa252a2937..0e1a96548c 100644 --- a/tests/base_rules/py_info/py_info_tests.bzl +++ b/tests/base_rules/py_info/py_info_tests.bzl @@ -13,11 +13,10 @@ # limitations under the License. """Tests for py_info.""" -load("@rules_python_internal//:rules_python_config.bzl", "config") load("@rules_testing//lib:analysis_test.bzl", "analysis_test") load("@rules_testing//lib:test_suite.bzl", "test_suite") load("@rules_testing//lib:util.bzl", rt_util = "util") -load("//python:py_info.bzl", "PyInfo") +load("//python:py_info.bzl", "PyInfo", "VenvSymlinkKind") load("//python/private:py_info.bzl", "PyInfoBuilder") # buildifier: disable=bzl-visibility load("//python/private:reexports.bzl", "BuiltinPyInfo") # buildifier: disable=bzl-visibility load("//tests/support:py_info_subject.bzl", "py_info_subject") @@ -36,14 +35,13 @@ def _provide_py_info_impl(ctx): if ctx.attr.has_py2_only_sources != -1: kwargs["has_py2_only_sources"] = bool(ctx.attr.has_py2_only_sources) if ctx.attr.has_py3_only_sources != -1: - kwargs["has_py2_only_sources"] = bool(ctx.attr.has_py2_only_sources) + kwargs["has_py3_only_sources"] = bool(ctx.attr.has_py3_only_sources) providers = [] - if config.enable_pystar: - providers.append(PyInfo(**kwargs)) + providers.append(PyInfo(**kwargs)) # Handle Bazel 6 or if Bazel autoloading is enabled - if not config.enable_pystar or (BuiltinPyInfo and PyInfo != BuiltinPyInfo): + if BuiltinPyInfo and PyInfo != BuiltinPyInfo: providers.append(BuiltinPyInfo(**{ k: kwargs[k] for k in ( @@ -95,10 +93,8 @@ def _test_py_info_create_impl(env, target): imports = depset(["import-path"]), transitive_sources = depset([trans_py]), uses_shared_libraries = True, - **(dict( - direct_pyc_files = depset([direct_pyc]), - transitive_pyc_files = depset([trans_pyc]), - ) if config.enable_pystar else {}) + direct_pyc_files = depset([direct_pyc]), + transitive_pyc_files = depset([trans_pyc]), ) subject = py_info_subject(actual, meta = env.expect.meta) @@ -107,9 +103,8 @@ def _test_py_info_create_impl(env, target): subject.has_py3_only_sources().equals(True) subject.transitive_sources().contains_exactly(["tests/base_rules/py_info/trans.py"]) subject.imports().contains_exactly(["import-path"]) - if config.enable_pystar: - subject.direct_pyc_files().contains_exactly(["tests/base_rules/py_info/direct.pyc"]) - subject.transitive_pyc_files().contains_exactly(["tests/base_rules/py_info/trans.pyc"]) + subject.direct_pyc_files().contains_exactly(["tests/base_rules/py_info/direct.pyc"]) + subject.transitive_pyc_files().contains_exactly(["tests/base_rules/py_info/trans.pyc"]) _tests.append(_test_py_info_create) @@ -175,6 +170,12 @@ def _test_py_info_builder_impl(env, targets): builder.transitive_sources.add(trans) builder.merge_uses_shared_libraries(True) + symlink = builder.add_venv_symlink() + symlink.set_kind(VenvSymlinkKind.LIB) + symlink.set_venv_path("test_path") + symlink.set_link_to_path("test_target") + symlink.files.add(trans) + builder.merge_target(targets.py1) builder.merge_targets([targets.py2]) @@ -252,6 +253,21 @@ def _test_py_info_builder_impl(env, targets): "tests/base_rules/py_info/py6-trans.pyi", ]) + if hasattr(actual, "venv_symlinks"): + entries = actual.venv_symlinks.to_list() + env.expect.that_int(len(entries)).equals(1) + entry = entries[0] + env.expect.that_str(entry.venv_path).equals("test_path") + env.expect.that_str(entry.link_to_path).equals("test_target") + env.expect.that_str(entry.kind).equals(VenvSymlinkKind.LIB) + env.expect.that_collection(entry.files.to_list()).contains_exactly([trans]) + env.expect.that_bool(entry.package == None).equals(True) + env.expect.that_bool(entry.version == None).equals(True) + env.expect.that_bool(entry.link_to_file == None).equals(True) + + check(builder.build()) + + # Call build() again to verify it doesn't duplicate/leak state check(builder.build()) if BuiltinPyInfo != None: check(builder.build_builtin_py_info()) diff --git a/tests/base_rules/py_library/py_library_tests.bzl b/tests/base_rules/py_library/py_library_tests.bzl index 9b585b17ef..3726ff1f41 100644 --- a/tests/base_rules/py_library/py_library_tests.bzl +++ b/tests/base_rules/py_library/py_library_tests.bzl @@ -27,7 +27,7 @@ def _test_py_runtime_info_not_present_impl(env, target): _tests.append(_test_py_runtime_info_not_present) -def _test_files_to_build(name, config): +def _test_default_outputs(name, config): rt_util.helper_target( config.rule, name = name + "_subject", @@ -36,15 +36,15 @@ def _test_files_to_build(name, config): analysis_test( name = name, target = name + "_subject", - impl = _test_files_to_build_impl, + impl = _test_default_outputs_impl, ) -def _test_files_to_build_impl(env, target): +def _test_default_outputs_impl(env, target): env.expect.that_target(target).default_outputs().contains_exactly([ "{package}/lib.py", ]) -_tests.append(_test_files_to_build) +_tests.append(_test_default_outputs) def _test_srcs_can_contain_rule_generating_py_and_nonpy_files(name, config): rt_util.helper_target( diff --git a/tests/base_rules/py_test/py_test_tests.bzl b/tests/base_rules/py_test/py_test_tests.bzl index 1ec1dc428f..e16204e9f2 100644 --- a/tests/base_rules/py_test/py_test_tests.bzl +++ b/tests/base_rules/py_test/py_test_tests.bzl @@ -16,12 +16,14 @@ load("@rules_testing//lib:analysis_test.bzl", "analysis_test") load("@rules_testing//lib:util.bzl", rt_util = "util") load("//python:py_test.bzl", "py_test") +load("//python/private:common_labels.bzl", "labels") # buildifier: disable=bzl-visibility load( "//tests/base_rules:py_executable_base_tests.bzl", "create_executable_tests", ) load("//tests/base_rules:util.bzl", pt_util = "util") -load("//tests/support:support.bzl", "CC_TOOLCHAIN", "CROSSTOOL_TOP", "LINUX_X86_64", "MAC_X86_64") +load("//tests/support:support.bzl", "CC_TOOLCHAIN", "CROSSTOOL_TOP", "PY_TOOLCHAINS") +load("//tests/support/platforms:platforms.bzl", "platform_targets") # The Windows CI currently runs as root, which breaks when # the analysis tests try to install (but not use, because @@ -39,14 +41,6 @@ _SKIP_WINDOWS = { _tests = [] def _test_mac_requires_darwin_for_execution(name, config): - # Bazel 5.4 has a bug where every access of testing.ExecutionInfo is - # a different object that isn't equal to any other, which prevents - # rules_testing from detecting it properly and fails with an error. - # This is fixed in Bazel 6+. - if not pt_util.is_bazel_6_or_higher(): - rt_util.skip_test(name = name) - return - rt_util.helper_target( config.rule, name = name + "_subject", @@ -59,9 +53,9 @@ def _test_mac_requires_darwin_for_execution(name, config): config_settings = { "//command_line_option:cpu": "darwin_x86_64", "//command_line_option:crosstool_top": CROSSTOOL_TOP, - "//command_line_option:extra_execution_platforms": [MAC_X86_64], + "//command_line_option:extra_execution_platforms": [platform_targets.MAC_X86_64], "//command_line_option:extra_toolchains": [CC_TOOLCHAIN], - "//command_line_option:platforms": [MAC_X86_64], + "//command_line_option:platforms": [platform_targets.MAC_X86_64], }, attr_values = _SKIP_WINDOWS, ) @@ -74,13 +68,6 @@ def _test_mac_requires_darwin_for_execution_impl(env, target): _tests.append(_test_mac_requires_darwin_for_execution) def _test_non_mac_doesnt_require_darwin_for_execution(name, config): - # Bazel 5.4 has a bug where every access of testing.ExecutionInfo is - # a different object that isn't equal to any other, which prevents - # rules_testing from detecting it properly and fails with an error. - # This is fixed in Bazel 6+. - if not pt_util.is_bazel_6_or_higher(): - rt_util.skip_test(name = name) - return rt_util.helper_target( config.rule, name = name + "_subject", @@ -93,9 +80,9 @@ def _test_non_mac_doesnt_require_darwin_for_execution(name, config): config_settings = { "//command_line_option:cpu": "k8", "//command_line_option:crosstool_top": CROSSTOOL_TOP, - "//command_line_option:extra_execution_platforms": [LINUX_X86_64], + "//command_line_option:extra_execution_platforms": [platform_targets.LINUX_X86_64], "//command_line_option:extra_toolchains": [CC_TOOLCHAIN], - "//command_line_option:platforms": [LINUX_X86_64], + "//command_line_option:platforms": [platform_targets.LINUX_X86_64], }, attr_values = _SKIP_WINDOWS, ) @@ -110,6 +97,61 @@ def _test_non_mac_doesnt_require_darwin_for_execution_impl(env, target): _tests.append(_test_non_mac_doesnt_require_darwin_for_execution) +_VALIDATE_TEST_MAIN_CONFIG_SETTINGS = { + "//command_line_option:extra_toolchains": [PY_TOOLCHAINS, CC_TOOLCHAIN], + labels.EXEC_TOOLS_TOOLCHAIN: "enabled", +} + +def _test_validate_test_main_enabled(name, config): + rt_util.helper_target( + config.rule, + name = name + "_subject", + srcs = [name + "_subject.py"], + ) + analysis_test( + name = name, + impl = _test_validate_test_main_enabled_impl, + target = name + "_subject", + config_settings = _VALIDATE_TEST_MAIN_CONFIG_SETTINGS | { + labels.VALIDATE_TEST_MAIN: "enabled", + }, + attr_values = _SKIP_WINDOWS, + ) + +def _test_validate_test_main_enabled_impl(env, target): + mnemonics = [a.mnemonic for a in target.actions] + env.expect.that_collection(mnemonics).contains("PyValidateTestMain") + env.expect.that_bool( + hasattr(target[OutputGroupInfo], "_validation"), + ).equals(True) + +_tests.append(_test_validate_test_main_enabled) + +def _test_validate_test_main_disabled(name, config): + rt_util.helper_target( + config.rule, + name = name + "_subject", + srcs = [name + "_subject.py"], + ) + analysis_test( + name = name, + impl = _test_validate_test_main_disabled_impl, + target = name + "_subject", + config_settings = _VALIDATE_TEST_MAIN_CONFIG_SETTINGS | { + labels.VALIDATE_TEST_MAIN: "disabled", + }, + attr_values = _SKIP_WINDOWS, + ) + +def _test_validate_test_main_disabled_impl(env, target): + mnemonics = [a.mnemonic for a in target.actions] + env.expect.that_collection(mnemonics).not_contains("PyValidateTestMain") + env.expect.that_bool( + hasattr(target[OutputGroupInfo], "_validation"), + ).equals(False) + +_tests.append(_test_validate_test_main_disabled) + def py_test_test_suite(name): config = struct(rule = py_test) native.test_suite( diff --git a/tests/base_rules/util.bzl b/tests/base_rules/util.bzl index a02cafa992..9fb66d7eb3 100644 --- a/tests/base_rules/util.bzl +++ b/tests/base_rules/util.bzl @@ -14,7 +14,6 @@ """Helpers and utilities multiple tests re-use.""" load("@bazel_skylib//lib:structs.bzl", "structs") -load("//python/private:util.bzl", "IS_BAZEL_6_OR_HIGHER") # buildifier: disable=bzl-visibility # Use this with is_windows() WINDOWS_ATTR = {"windows": attr.label(default = "@platforms//os:windows")} @@ -53,9 +52,6 @@ def _struct_with(s, **kwargs): struct_dict.update(kwargs) return struct(**struct_dict) -def _is_bazel_6_or_higher(): - return IS_BAZEL_6_OR_HIGHER - def _is_windows(env): """Tell if the target platform is windows. @@ -72,6 +68,5 @@ def _is_windows(env): util = struct( create_tests = _create_tests, struct_with = _struct_with, - is_bazel_6_or_higher = _is_bazel_6_or_higher, is_windows = _is_windows, ) diff --git a/tests/bootstrap_impls/BUILD.bazel b/tests/bootstrap_impls/BUILD.bazel index c3d44df240..89cd682a6a 100644 --- a/tests/bootstrap_impls/BUILD.bazel +++ b/tests/bootstrap_impls/BUILD.bazel @@ -13,6 +13,7 @@ # limitations under the License. load("@rules_pkg//pkg:tar.bzl", "pkg_tar") load("@rules_shell//shell:sh_test.bzl", "sh_test") +load("//python:py_test.bzl", "py_test") load("//tests/support:py_reconfig.bzl", "py_reconfig_binary", "py_reconfig_test") load("//tests/support:sh_py_run_test.bzl", "sh_py_run_test") load("//tests/support:support.bzl", "SUPPORTS_BOOTSTRAP_SCRIPT") @@ -23,7 +24,7 @@ py_reconfig_binary( srcs = ["bin.py"], bootstrap_impl = "script", # Force it to not be self-executable - build_python_zip = "no", + build_python_zip = False, main = "bin.py", target_compatible_with = SUPPORTS_BOOTSTRAP_SCRIPT, ) @@ -50,14 +51,14 @@ sh_test( sh_py_run_test( name = "run_binary_zip_no_test", - build_python_zip = "no", + build_python_zip = False, py_src = "bin.py", sh_src = "run_binary_zip_no_test.sh", ) sh_py_run_test( name = "run_binary_zip_yes_test", - build_python_zip = "yes", + build_python_zip = True, py_src = "bin.py", sh_src = "run_binary_zip_yes_test.sh", ) @@ -81,7 +82,7 @@ sh_py_run_test( sh_py_run_test( name = "run_binary_bootstrap_script_zip_yes_test", bootstrap_impl = "script", - build_python_zip = "yes", + build_python_zip = True, py_src = "bin.py", sh_src = "run_binary_zip_yes_test.sh", target_compatible_with = SUPPORTS_BOOTSTRAP_SCRIPT, @@ -90,7 +91,7 @@ sh_py_run_test( sh_py_run_test( name = "run_binary_bootstrap_script_zip_no_test", bootstrap_impl = "script", - build_python_zip = "no", + build_python_zip = False, py_src = "bin.py", sh_src = "run_binary_zip_no_test.sh", target_compatible_with = SUPPORTS_BOOTSTRAP_SCRIPT, @@ -104,6 +105,18 @@ sh_py_run_test( target_compatible_with = SUPPORTS_BOOTSTRAP_SCRIPT, ) +py_reconfig_test( + name = "bazel_tools_importable_system_python_test", + srcs = ["bazel_tools_importable_test.py"], + bootstrap_impl = "system_python", + # Necessary because bazel_tools doesn't have __init__.py files. + legacy_create_init = True, + main = "bazel_tools_importable_test.py", + deps = [ + "@bazel_tools//tools/python/runfiles", + ], +) + py_reconfig_test( name = "sys_path_order_bootstrap_script_test", srcs = ["sys_path_order_test.py"], @@ -123,6 +136,32 @@ py_reconfig_test( main = "sys_path_order_test.py", ) +genrule( + name = "stdlib_shadowing_outputs", + outs = [ + "shutil.py", + "types.py", + ], + cmd = """ +cat > $(@D)/shutil.py <<'PY' +raise RuntimeError("target output shutil.py shadowed the stdlib shutil module") +PY +cat > $(@D)/types.py <<'PY' +raise RuntimeError("target output types.py shadowed the stdlib types module") +PY +""", +) + +py_reconfig_test( + name = "stdlib_shadowing_system_python_test", + srcs = [ + "stdlib_shadowing_test.py", + ":stdlib_shadowing_outputs", + ], + bootstrap_impl = "system_python", + main = "stdlib_shadowing_test.py", +) + py_reconfig_test( name = "main_module_test", srcs = ["main_module.py"], @@ -178,4 +217,12 @@ sh_test( }), ) +py_test( + name = "system_python_nodeps_test", + srcs = ["system_python_nodeps_test.py"], + config_settings = { + "//python/config_settings:bootstrap_impl": "system_python", + }, +) + relative_path_test_suite(name = "relative_path_tests") diff --git a/tests/bootstrap_impls/a/b/c/BUILD.bazel b/tests/bootstrap_impls/a/b/c/BUILD.bazel index 1659ef25bc..1c4b1e7b6b 100644 --- a/tests/bootstrap_impls/a/b/c/BUILD.bazel +++ b/tests/bootstrap_impls/a/b/c/BUILD.bazel @@ -1,10 +1,9 @@ -load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER") # buildifier: disable=bzl-visibility load("//tests/support:py_reconfig.bzl", "py_reconfig_test") _SUPPORTS_BOOTSTRAP_SCRIPT = select({ "@platforms//os:windows": ["@platforms//:incompatible"], "//conditions:default": [], -}) if IS_BAZEL_7_OR_HIGHER else ["@platforms//:incompatible"] +}) py_reconfig_test( name = "nested_dir_test", diff --git a/tests/bootstrap_impls/bazel_tools_importable_test.py b/tests/bootstrap_impls/bazel_tools_importable_test.py new file mode 100644 index 0000000000..c374dd5dcf --- /dev/null +++ b/tests/bootstrap_impls/bazel_tools_importable_test.py @@ -0,0 +1,20 @@ +import sys +import unittest + + +class BazelToolsImportableTest(unittest.TestCase): + def test_bazel_tools_importable(self): + try: + import bazel_tools + import bazel_tools.tools.python + import bazel_tools.tools.python.runfiles # noqa: F401 + except ImportError as exc: + raise AssertionError( + "Failed to import bazel_tools.python.runfiles\n" + + "sys.path:\n" + + "\n".join(f"{i}: {v}" for i, v in enumerate(sys.path)) + ) from exc + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/bootstrap_impls/bin_calls_bin/BUILD.bazel b/tests/bootstrap_impls/bin_calls_bin/BUILD.bazel index 02835fb77b..b5ba68f987 100644 --- a/tests/bootstrap_impls/bin_calls_bin/BUILD.bazel +++ b/tests/bootstrap_impls/bin_calls_bin/BUILD.bazel @@ -1,7 +1,14 @@ load("@rules_shell//shell:sh_test.bzl", "sh_test") +load("//python:py_library.bzl", "py_library") load("//tests/support:py_reconfig.bzl", "py_reconfig_binary") load("//tests/support:support.bzl", "NOT_WINDOWS", "SUPPORTS_BOOTSTRAP_SCRIPT") +py_library( + name = "inner_lib", + srcs = ["inner_lib.py"], + tags = ["manual"], +) + # ===== # bootstrap_impl=system_python testing # ===== @@ -19,12 +26,13 @@ py_reconfig_binary( bootstrap_impl = "system_python", main = "inner.py", tags = ["manual"], + deps = [":inner_lib"], ) genrule( name = "outer_calls_inner_system_python", outs = ["outer_calls_inner_system_python.out"], - cmd = "RULES_PYTHON_TESTING_TELL_MODULE_SPACE=1 $(location :outer_bootstrap_system_python) $(location :inner_bootstrap_system_python) > $@", + cmd = "RULES_PYTHON_TESTING_TELL_RUNFILES_ROOT=1 $(location :outer_bootstrap_system_python) $(location :inner_bootstrap_system_python) > $@", tags = ["manual"], tools = [ ":inner_bootstrap_system_python", @@ -54,6 +62,7 @@ py_reconfig_binary( bootstrap_impl = "script", main = "inner.py", tags = ["manual"], + deps = [":inner_lib"], ) py_reconfig_binary( @@ -67,7 +76,7 @@ py_reconfig_binary( genrule( name = "outer_calls_inner_script_python", outs = ["outer_calls_inner_script_python.out"], - cmd = "RULES_PYTHON_TESTING_TELL_MODULE_SPACE=1 $(location :outer_bootstrap_script) $(location :inner_bootstrap_script) > $@", + cmd = "RULES_PYTHON_TESTING_TELL_RUNFILES_ROOT=1 $(location :outer_bootstrap_script) $(location :inner_bootstrap_script) > $@", tags = ["manual"], tools = [ ":inner_bootstrap_script", diff --git a/tests/bootstrap_impls/bin_calls_bin/inner.py b/tests/bootstrap_impls/bin_calls_bin/inner.py index e67b31dda3..74a70b5f0d 100644 --- a/tests/bootstrap_impls/bin_calls_bin/inner.py +++ b/tests/bootstrap_impls/bin_calls_bin/inner.py @@ -1,4 +1,15 @@ import os -module_space = os.environ.get("RULES_PYTHON_TESTING_MODULE_SPACE") -print(f"inner: RULES_PYTHON_TESTING_MODULE_SPACE='{module_space}'") +runfiles_root = os.environ.get("RULES_PYTHON_TESTING_RUNFILES_ROOT") +runfiles_dir = os.environ.get("RUNFILES_DIR") +runfiles_manifest_file = os.environ.get("RUNFILES_MANIFEST_FILE") +print(f"inner: RULES_PYTHON_TESTING_RUNFILES_ROOT='{runfiles_root}'") +print(f"inner: RUNFILES_DIR='{runfiles_dir}'") +print(f"inner: RUNFILES_MANIFEST_FILE='{runfiles_manifest_file}'") + +try: + import tests.bootstrap_impls.bin_calls_bin.inner_lib as inner_lib + + print(f"inner: import_result='{inner_lib.confirm()}'") +except ImportError as e: + print(f"inner: import_result='{e}'") diff --git a/tests/bootstrap_impls/bin_calls_bin/inner_lib.py b/tests/bootstrap_impls/bin_calls_bin/inner_lib.py new file mode 100644 index 0000000000..5815b4f41b --- /dev/null +++ b/tests/bootstrap_impls/bin_calls_bin/inner_lib.py @@ -0,0 +1,3 @@ +# Rather than having a completely empty file... +def confirm(): + return "success" diff --git a/tests/bootstrap_impls/bin_calls_bin/outer.py b/tests/bootstrap_impls/bin_calls_bin/outer.py index 19dac06eb7..a4432dff59 100644 --- a/tests/bootstrap_impls/bin_calls_bin/outer.py +++ b/tests/bootstrap_impls/bin_calls_bin/outer.py @@ -3,16 +3,16 @@ import sys if __name__ == "__main__": - module_space = os.environ.get("RULES_PYTHON_TESTING_MODULE_SPACE") - print(f"outer: RULES_PYTHON_TESTING_MODULE_SPACE='{module_space}'") + runfiles_root = os.environ.get("RULES_PYTHON_TESTING_RUNFILES_ROOT") + print(f"outer: RULES_PYTHON_TESTING_RUNFILES_ROOT='{runfiles_root}'") inner_binary_path = sys.argv[1] result = subprocess.run( [inner_binary_path], capture_output=True, text=True, - check=True, ) print(result.stdout, end="") if result.stderr: print(result.stderr, end="", file=sys.stderr) + sys.exit(result.returncode) diff --git a/tests/bootstrap_impls/bin_calls_bin/verify.sh b/tests/bootstrap_impls/bin_calls_bin/verify.sh index 433704e9ab..bbe4252cc1 100755 --- a/tests/bootstrap_impls/bin_calls_bin/verify.sh +++ b/tests/bootstrap_impls/bin_calls_bin/verify.sh @@ -4,23 +4,35 @@ set -euo pipefail verify_output() { local OUTPUT_FILE=$1 - # Extract the RULES_PYTHON_TESTING_MODULE_SPACE values - local OUTER_MODULE_SPACE=$(grep "outer: RULES_PYTHON_TESTING_MODULE_SPACE" "$OUTPUT_FILE" | sed "s/outer: RULES_PYTHON_TESTING_MODULE_SPACE='\(.*\)'/\1/") - local INNER_MODULE_SPACE=$(grep "inner: RULES_PYTHON_TESTING_MODULE_SPACE" "$OUTPUT_FILE" | sed "s/inner: RULES_PYTHON_TESTING_MODULE_SPACE='\(.*\)'/\1/") + # Extract the RULES_PYTHON_TESTING_RUNFILES_ROOT values + local OUTER_RUNFILES_ROOT=$(grep "outer: RULES_PYTHON_TESTING_RUNFILES_ROOT" "$OUTPUT_FILE" | sed "s/outer: RULES_PYTHON_TESTING_RUNFILES_ROOT='\(.*\)'/\1/") + local INNER_RUNFILES_ROOT=$(grep "inner: RULES_PYTHON_TESTING_RUNFILES_ROOT" "$OUTPUT_FILE" | sed "s/inner: RULES_PYTHON_TESTING_RUNFILES_ROOT='\(.*\)'/\1/") + + echo "Outer runfiles root: $OUTER_RUNFILES_ROOT" + echo "Inner runfiles root: $INNER_RUNFILES_ROOT" + + # Extract the inner runfiles values + local INNER_RUNFILES_DIR=$(grep "inner: RUNFILES_DIR" "$OUTPUT_FILE" | sed "s/inner: RUNFILES_DIR='\(.*\)'/\1/") + local INNER_RUNFILES_MANIFEST_FILE=$(grep "inner: RUNFILES_MANIFEST_FILE" "$OUTPUT_FILE" | sed "s/inner: RUNFILES_MANIFEST_FILE='\(.*\)'/\1/") + + echo "Inner runfiles dir: $INNER_RUNFILES_DIR" + echo "Inner runfiles manifest file: $INNER_RUNFILES_MANIFEST_FILE" + + # Extract the inner lib import result + local INNER_LIB_IMPORT=$(grep "inner: import_result" "$OUTPUT_FILE" | sed "s/inner: import_result='\(.*\)'/\1/") + echo "Inner lib import result: $INNER_LIB_IMPORT" - echo "Outer module space: $OUTER_MODULE_SPACE" - echo "Inner module space: $INNER_MODULE_SPACE" # Check 1: The two values are different - if [ "$OUTER_MODULE_SPACE" == "$INNER_MODULE_SPACE" ]; then - echo "Error: Outer and Inner module spaces are the same." + if [ "$OUTER_RUNFILES_ROOT" == "$INNER_RUNFILES_ROOT" ]; then + echo "Error: Outer and Inner runfiles roots are the same." exit 1 fi # Check 2: Inner is not a subdirectory of Outer - case "$INNER_MODULE_SPACE" in - "$OUTER_MODULE_SPACE"/*) - echo "Error: Inner module space is a subdirectory of Outer's." + case "$INNER_RUNFILES_ROOT" in + "$OUTER_RUNFILES_ROOT"/*) + echo "Error: Inner runfiles root is a subdirectory of Outer's." exit 1 ;; *) @@ -28,5 +40,11 @@ verify_output() { ;; esac + # Check 3: inner_lib was imported + if [ "$INNER_LIB_IMPORT" != "success" ]; then + echo "Error: Inner lib was not successfully imported." + exit 1 + fi + echo "Verification successful." } diff --git a/tests/bootstrap_impls/call_sys_exe.py b/tests/bootstrap_impls/call_sys_exe.py index 0c6157048c..c431145386 100644 --- a/tests/bootstrap_impls/call_sys_exe.py +++ b/tests/bootstrap_impls/call_sys_exe.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os import subprocess import sys diff --git a/tests/bootstrap_impls/run_binary_zip_yes_test.sh b/tests/bootstrap_impls/run_binary_zip_yes_test.sh index ca278083dd..77fe4d3609 100755 --- a/tests/bootstrap_impls/run_binary_zip_yes_test.sh +++ b/tests/bootstrap_impls/run_binary_zip_yes_test.sh @@ -34,8 +34,8 @@ actual=$($bin) # How we detect if a zip file was executed from depends on which bootstrap # is used. # bootstrap_impl=script outputs RULES_PYTHON_ZIP_DIR: -# bootstrap_impl=system_python outputs file:.*Bazel.runfiles -expected_pattern="RULES_PYTHON_ZIP_DIR:/\|file:.*Bazel.runfiles" +# bootstrap_impl=system_python outputs file:.*Bazel.runfiles (or .exe.runfiles on Windows) +expected_pattern="RULES_PYTHON_ZIP_DIR:/\|file:.*Bazel.runfiles\|file:.*\.exe\.runfiles" if ! (echo "$actual" | grep "$expected_pattern" ) >/dev/null; then echo "expected output to match: $expected_pattern" echo "but got: $actual" diff --git a/tests/bootstrap_impls/stdlib_shadowing_test.py b/tests/bootstrap_impls/stdlib_shadowing_test.py new file mode 100644 index 0000000000..fc913857db --- /dev/null +++ b/tests/bootstrap_impls/stdlib_shadowing_test.py @@ -0,0 +1,20 @@ +# Copyright 2026 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. +"""Verifies stage 1 bootstrap stdlib imports cannot be shadowed.""" + + +def test_bootstrap_reached_main(): + # If stage 1 imports target outputs such as shutil.py instead of stdlib + # modules, the process fails before this test module is executed. + pass diff --git a/tests/bootstrap_impls/sys_path_order_test.py b/tests/bootstrap_impls/sys_path_order_test.py index 9ae03bb129..a9018c39ce 100644 --- a/tests/bootstrap_impls/sys_path_order_test.py +++ b/tests/bootstrap_impls/sys_path_order_test.py @@ -33,9 +33,13 @@ def test_sys_path_order(self): # error messages are more informative. categorized_paths = [] for i, value in enumerate(sys.path): - # The runtime's root repo may be added to sys.path, but it - # counts as a user directory, not stdlib directory. - if value in (sys.prefix, sys.base_prefix): + # On Windows, the `pythonXY.zip` entry shows up as `$venv/Scripts/pythonXY.zip` + # While it's technically part of the venv, it's considered the stdlib. + if os.name == "nt" and re.search("python.*[.]zip$", value): + category = "stdlib" + elif value in (sys.prefix, sys.base_prefix): + # The runtime's root repo may be added to sys.path, but it + # counts as a user directory, not stdlib directory. category = "user" elif value.startswith(sys.base_prefix): # The runtime's site-package directory might be called diff --git a/tests/bootstrap_impls/system_python_nodeps_test.py b/tests/bootstrap_impls/system_python_nodeps_test.py new file mode 100644 index 0000000000..1affaab497 --- /dev/null +++ b/tests/bootstrap_impls/system_python_nodeps_test.py @@ -0,0 +1,11 @@ +print("Hello, world") + +# Verify py code from the stdlib can be imported. +import pathlib # noqa: E402 + +print(pathlib) + +# Verify a C-implemented module can be imported. +# Socket isn't implement in C, but requires `_socket`, +# which is implemented in C +import socket # noqa: E402, F401 diff --git a/tests/build_data/BUILD.bazel b/tests/build_data/BUILD.bazel new file mode 100644 index 0000000000..64db005f51 --- /dev/null +++ b/tests/build_data/BUILD.bazel @@ -0,0 +1,25 @@ +load("//python:py_binary.bzl", "py_binary") +load("//python:py_test.bzl", "py_test") + +py_test( + name = "build_data_test", + srcs = ["build_data_test.py"], + data = [ + ":tool_build_data.txt", + ], + stamp = 1, + deps = ["//python/runfiles"], +) + +py_binary( + name = "print_build_data", + srcs = ["print_build_data.py"], + deps = ["//python/runfiles"], +) + +genrule( + name = "tool_build_data", + outs = ["tool_build_data.txt"], + cmd = "$(location :print_build_data) > $(OUTS)", + tools = [":print_build_data"], +) diff --git a/tests/build_data/build_data_test.py b/tests/build_data/build_data_test.py new file mode 100644 index 0000000000..6be4e52c84 --- /dev/null +++ b/tests/build_data/build_data_test.py @@ -0,0 +1,29 @@ +import unittest + +from python.runfiles import runfiles + + +class BuildDataTest(unittest.TestCase): + def test_target_build_data(self): + import bazel_binary_info + + self.assertIn("build_data.txt", bazel_binary_info.BUILD_DATA_FILE) + + build_data = bazel_binary_info.get_build_data() + self.assertIn("TARGET ", build_data) + self.assertIn("BUILD_HOST ", build_data) + self.assertIn("BUILD_USER ", build_data) + self.assertIn("BUILD_TIMESTAMP ", build_data) + self.assertIn("FORMATTED_DATE ", build_data) + self.assertIn("STAMPED TRUE", build_data) + + def test_tool_build_data(self): + rf = runfiles.Create() + path = rf.Rlocation("rules_python/tests/build_data/tool_build_data.txt") + with open(path) as fp: + build_data = fp.read() + + self.assertIn("STAMPED FALSE", build_data) + + +unittest.main() diff --git a/tests/build_data/print_build_data.py b/tests/build_data/print_build_data.py new file mode 100644 index 0000000000..0af77d72be --- /dev/null +++ b/tests/build_data/print_build_data.py @@ -0,0 +1,3 @@ +import bazel_binary_info + +print(bazel_binary_info.get_build_data()) diff --git a/tests/cc/current_py_cc_headers/abi3_headers_linkage_test.py b/tests/cc/current_py_cc_headers/abi3_headers_linkage_test.py index 6c337653b1..2d64828278 100644 --- a/tests/cc/current_py_cc_headers/abi3_headers_linkage_test.py +++ b/tests/cc/current_py_cc_headers/abi3_headers_linkage_test.py @@ -1,5 +1,3 @@ -import os.path -import pathlib import sys import unittest @@ -12,7 +10,9 @@ class CheckLinkageTest(unittest.TestCase): @unittest.skipUnless(sys.platform.startswith("win"), "requires windows") def test_linkage_windows(self): rf = runfiles.Create() - dll_path = rf.Rlocation("rules_python/tests/cc/current_py_cc_headers/bin_abi3.dll") + dll_path = rf.Rlocation( + "rules_python/tests/cc/current_py_cc_headers/bin_abi3.dll" + ) pe = pefile.PE(dll_path) if not hasattr(pe, "DIRECTORY_ENTRY_IMPORT"): self.fail("No import directory found.") diff --git a/tests/cc/py_extension/BUILD.bazel b/tests/cc/py_extension/BUILD.bazel new file mode 100644 index 0000000000..5ba8ff8aca --- /dev/null +++ b/tests/cc/py_extension/BUILD.bazel @@ -0,0 +1,181 @@ +load("@rules_cc//cc:cc_library.bzl", "cc_library") +load("@rules_cc//cc:cc_shared_library.bzl", "cc_shared_library") +load("//python:py_test.bzl", "py_test") + +# buildifier: disable=bzl-visibility +load("//python/cc:py_extension.bzl", "py_extension") +load(":dependency_graph_tests.bzl", "dependency_graph_test_suite") +load(":py_extension_tests.bzl", "py_extension_analysis_test_suite") +load(":py_limited_api_tests.bzl", "py_limited_api_test_suite") + +package( + default_testonly = True, + default_visibility = ["//visibility:private"], +) + +licenses(["notice"]) + +##### + +py_extension( + # An extension defined solely by source files, with no deps + name = "ext_source", + srcs = ["ext_source.c"], +) + +##### + +py_extension( + # A python extension that gets its code from a statically-linked library + name = "ext_static", + deps = [":static_dep"], +) + +cc_library( + name = "static_dep", + srcs = ["static_dep.c"], + hdrs = ["static_dep.h"], +) + +##### + +py_extension( + # An extension that also depends on a data file + name = "ext_with_data", + data = ["some_data.txt"], + deps = [":static_dep"], +) + +##### + +py_extension( + # A python extension that dynamically links to another shared library + name = "ext_shared", + imports = ["."], + dynamic_deps = [ + ":add_one_shared", + ], + deps = [ + # + ":ext_shared_impl", + ], +) + +cc_library( + name = "ext_shared_impl", + srcs = ["ext_shared.c"], + copts = [ + # Gemini says PIC is needed + "-fPIC", + "-fvisibility=hidden", + ], + deps = [ + #":add_one_headers", + # todo: if we put this here, we statically link add_one into the + # extension. + ":add_one_impl", + "@rules_python//python/cc:current_py_cc_headers", + ], +) + +cc_shared_library( + name = "add_one_shared", + deps = [":add_one_impl"], +) + +cc_library( + name = "add_one_headers", + hdrs = ["add_one.h"], +) + +cc_library( + name = "add_one_impl", + srcs = ["add_one.c"], + deps = [ + ":add_one_headers", + ":add_one_helper", + ], +) + +cc_library( + name = "add_one_helper", + srcs = ["add_one_helper.c"], + hdrs = ["add_one_helper.h"], +) + +##### + +py_extension( + # An extension that uses the Python limited API + name = "ext_limited", + py_limited_api = "3.8", + deps = [":ext_limited_impl"], +) + +cc_library( + name = "ext_limited_impl", + srcs = ["ext_limited.c"], + copts = [ + "-fPIC", + "-fvisibility=hidden", + ], + defines = ["Py_LIMITED_API=0x3080000"], + deps = [ + "@rules_python//python/cc:current_py_cc_headers", + ], +) + +##### + +py_extension( + # An extension with its PyInit_* function in a static dependency + name = "ext_init_in_dep", + deps = [":ext_init_in_dep_impl"], +) + +cc_library( + name = "ext_init_in_dep_impl", + srcs = ["ext_init_in_dep.c"], + deps = [ + "@rules_python//python/cc:current_py_cc_headers", + ], +) + +##### + +py_test( + name = "py_extension_test", + srcs = ["py_extension_test.py"], + deps = [ + ":ext_shared", + "@dev_pip//pyelftools", + "@rules_python//python/runfiles", + ], +) + +##### + +py_extension( + name = "ext_pkg_test", + srcs = ["ext_pkg_test.c"], +) + +py_test( + name = "py_extension_pkg_test", + srcs = ["py_extension_pkg_test.py"], + deps = [ + ":ext_pkg_test", + ], +) + +py_extension_analysis_test_suite( + name = "py_extension_analysis_tests", +) + +py_limited_api_test_suite( + name = "py_limited_api_tests", +) + +dependency_graph_test_suite( + name = "dependency_graph_tests", +) diff --git a/tests/cc/py_extension/add_one.c b/tests/cc/py_extension/add_one.c new file mode 100644 index 0000000000..7da8f6796e --- /dev/null +++ b/tests/cc/py_extension/add_one.c @@ -0,0 +1,7 @@ + +#include "add_one_helper.h" + +int add_one(int x) { + x = add_one_helper(x); + return x + 1; +} diff --git a/tests/cc/py_extension/add_one.h b/tests/cc/py_extension/add_one.h new file mode 100644 index 0000000000..eda0abf7e5 --- /dev/null +++ b/tests/cc/py_extension/add_one.h @@ -0,0 +1,6 @@ +#ifndef TESTS_CC_PY_EXTENSION_DYN_DEP_A_H_ +#define TESTS_CC_PY_EXTENSION_DYN_DEP_A_H_ + +int add_one(int x); + +#endif // TESTS_CC_PY_EXTENSION_DYN_DEP_A_H_ diff --git a/tests/cc/py_extension/add_one_helper.c b/tests/cc/py_extension/add_one_helper.c new file mode 100644 index 0000000000..21d19a5823 --- /dev/null +++ b/tests/cc/py_extension/add_one_helper.c @@ -0,0 +1,7 @@ + + +#include "add_one_helper.h" + +int add_one_helper(int i) { + return i + 1; +} diff --git a/tests/cc/py_extension/add_one_helper.h b/tests/cc/py_extension/add_one_helper.h new file mode 100644 index 0000000000..5524d3077f --- /dev/null +++ b/tests/cc/py_extension/add_one_helper.h @@ -0,0 +1,2 @@ + +int add_one_helper(int i); diff --git a/tests/cc/py_extension/dependency_graph_tests.bzl b/tests/cc/py_extension/dependency_graph_tests.bzl new file mode 100644 index 0000000000..497571e128 --- /dev/null +++ b/tests/cc/py_extension/dependency_graph_tests.bzl @@ -0,0 +1,236 @@ +# 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. + +"""Parity tests comparing cc_shared_library and py_extension behavior.""" + +load("@rules_cc//cc:cc_library.bzl", "cc_library") +load("@rules_cc//cc:cc_shared_library.bzl", "cc_shared_library") +load("@rules_cc//cc/common:cc_shared_library_info.bzl", "CcSharedLibraryInfo") +load("@rules_testing//lib:analysis_test.bzl", "analysis_test", "test_suite") +load("@rules_testing//lib:util.bzl", "util") + +# For tests 1 and 2 +def _create_dynamic_deps_helpers(name): + util.helper_target( + cc_library, + name = name + "_libC", + srcs = ["test_lib_c.c"], + hdrs = ["test_symbols.h"], + copts = ["-fPIC"], + ) + util.helper_target( + cc_shared_library, + name = name + "_cslC", + deps = [":" + name + "_libC"], + ) + util.helper_target( + cc_library, + name = name + "_libB", + srcs = ["test_lib_b.c"], + hdrs = ["test_symbols.h"], + copts = ["-fPIC"], + deps = [":" + name + "_libC"], + ) + util.helper_target( + cc_shared_library, + name = name + "_cslB", + deps = [":" + name + "_libB"], + dynamic_deps = [":" + name + "_cslC"], + ) + util.helper_target( + cc_library, + name = name + "_libA", + srcs = ["test_lib_a.c"], + hdrs = ["test_symbols.h"], + copts = ["-fPIC"], + deps = [":" + name + "_libB", ":" + name + "_libC"], + ) + +# Test 1: CSL A -> CSL B -> CSL C (Dynamic deps) +def _test_csl_dynamic_deps_top(name): + _create_dynamic_deps_helpers(name) + util.helper_target( + cc_shared_library, + name = name + "_cslA", + deps = [":" + name + "_libA"], + dynamic_deps = [":" + name + "_cslB", ":" + name + "_cslC"], + ) + analysis_test( + name = name, + target = name + "_cslA", + impl = _csl_dynamic_deps_test_impl, + ) + +def _csl_dynamic_deps_test_impl(env, target): + env.expect.that_target(target).has_provider(CcSharedLibraryInfo) + csl_info = target[CcSharedLibraryInfo] + + # Derive labels + test_name = target.label.name[:-5] # remove "_cslA" + lib_a_label = target.label.same_package_label(test_name + "_libA") + lib_b_label = target.label.same_package_label(test_name + "_libB") + lib_c_label = target.label.same_package_label(test_name + "_libC") + + env.expect.that_collection([str(e) for e in csl_info.exports]).contains_exactly([str(lib_a_label)]) + if hasattr(csl_info, "link_once_static_libs"): + static_libs = [str(lbl) for lbl in csl_info.link_once_static_libs] + env.expect.that_collection(static_libs).contains(str(lib_a_label)) + env.expect.that_collection(static_libs).contains_none_of([str(lib_b_label), str(lib_c_label)]) + +# Test 2: py_extension A -> CSL B -> CSL C (Dynamic deps) +def _test_pyext_dynamic_deps_cslB(name): + _create_dynamic_deps_helpers(name) + analysis_test( + name = name, + target = name + "_cslB", + impl = _cslB_deps_test_impl, + ) + +def _test_pyext_dynamic_deps_cslC(name): + _create_dynamic_deps_helpers(name) + analysis_test( + name = name, + target = name + "_cslC", + impl = _cslC_deps_test_impl, + ) + +def _cslC_deps_test_impl(env, target): + env.expect.that_target(target).has_provider(CcSharedLibraryInfo) + csl_info = target[CcSharedLibraryInfo] + + # Derive labels + test_name = target.label.name[:-5] # remove "_cslC" + lib_c_label = target.label.same_package_label(test_name + "_libC") + + env.expect.that_collection([str(e) for e in csl_info.exports]).contains_exactly([str(lib_c_label)]) + if hasattr(csl_info, "link_once_static_libs"): + env.expect.that_collection([str(lbl) for lbl in csl_info.link_once_static_libs]).contains_exactly([str(lib_c_label)]) + +def _cslB_deps_test_impl(env, target): + env.expect.that_target(target).has_provider(CcSharedLibraryInfo) + csl_info = target[CcSharedLibraryInfo] + + # Derive labels + test_name = target.label.name[:-5] # remove "_cslB" + lib_b_label = target.label.same_package_label(test_name + "_libB") + lib_c_label = target.label.same_package_label(test_name + "_libC") + csl_c_label = target.label.same_package_label(test_name + "_cslC") + + env.expect.that_collection([str(e) for e in csl_info.exports]).contains_exactly([str(lib_b_label)]) + if hasattr(csl_info, "link_once_static_libs"): + static_libs = [str(lbl) for lbl in csl_info.link_once_static_libs] + env.expect.that_collection(static_libs).contains(str(lib_b_label)) + env.expect.that_collection(static_libs).contains_none_of([str(lib_c_label)]) + + if hasattr(csl_info, "dynamic_deps"): + dynamic_deps = [str(d.linker_input.owner) for d in csl_info.dynamic_deps.to_list()] + env.expect.that_collection(dynamic_deps).contains(str(csl_c_label)) + +# For tests 3 and 4 +def _create_static_sharing_helpers(name): + util.helper_target( + cc_library, + name = name + "_libC", + srcs = ["test_lib_c.c"], + hdrs = ["test_symbols.h"], + copts = ["-fPIC"], + ) + util.helper_target( + cc_library, + name = name + "_libB", + srcs = ["test_lib_b.c"], + hdrs = ["test_symbols.h"], + copts = ["-fPIC"], + deps = [":" + name + "_libC"], + ) + util.helper_target( + cc_shared_library, + name = name + "_cslB", + deps = [":" + name + "_libB", ":" + name + "_libC"], + ) + util.helper_target( + cc_library, + name = name + "_libA", + srcs = ["test_lib_a.c"], + hdrs = ["test_symbols.h"], + copts = ["-fPIC"], + deps = [":" + name + "_libB", ":" + name + "_libC"], + ) + +# Test 3: CSL A -> CSL B, CL C (Static sharing) +def _test_csl_static_sharing_top(name): + _create_static_sharing_helpers(name) + util.helper_target( + cc_shared_library, + name = name + "_cslA", + deps = [":" + name + "_libA"], + dynamic_deps = [":" + name + "_cslB"], + ) + analysis_test( + name = name, + target = name + "_cslA", + impl = _csl_static_sharing_test_impl, + ) + +def _csl_static_sharing_test_impl(env, target): + env.expect.that_target(target).has_provider(CcSharedLibraryInfo) + csl_info = target[CcSharedLibraryInfo] + + # Derive labels + test_name = target.label.name[:-5] # remove "_cslA" + lib_a_label = target.label.same_package_label(test_name + "_libA") + lib_b_label = target.label.same_package_label(test_name + "_libB") + lib_c_label = target.label.same_package_label(test_name + "_libC") + + env.expect.that_collection([str(e) for e in csl_info.exports]).contains_exactly([str(lib_a_label)]) + if hasattr(csl_info, "link_once_static_libs"): + static_libs = [str(lbl) for lbl in csl_info.link_once_static_libs] + env.expect.that_collection(static_libs).contains(str(lib_a_label)) + env.expect.that_collection(static_libs).contains_none_of([str(lib_b_label), str(lib_c_label)]) + +# Test 4: Same as 3, but A is py_extension + +def _test_pyext_static_sharing_cslB(name): + _create_static_sharing_helpers(name) + analysis_test( + name = name, + target = name + "_cslB", + impl = _cslB_static_sharing_test_impl, + ) + +def _cslB_static_sharing_test_impl(env, target): + env.expect.that_target(target).has_provider(CcSharedLibraryInfo) + csl_info = target[CcSharedLibraryInfo] + + # Derive labels + test_name = target.label.name[:-5] # remove "_cslB" + lib_b_label = target.label.same_package_label(test_name + "_libB") + lib_c_label = target.label.same_package_label(test_name + "_libC") + + env.expect.that_collection([str(e) for e in csl_info.exports]).contains_exactly([str(lib_b_label), str(lib_c_label)]) + if hasattr(csl_info, "link_once_static_libs"): + static_libs = [str(lbl) for lbl in csl_info.link_once_static_libs] + env.expect.that_collection(static_libs).contains_exactly([str(lib_b_label), str(lib_c_label)]) + +def dependency_graph_test_suite(name): + test_suite( + name = name, + tests = [ + _test_csl_dynamic_deps_top, + _test_pyext_dynamic_deps_cslB, + _test_pyext_dynamic_deps_cslC, + _test_csl_static_sharing_top, + _test_pyext_static_sharing_cslB, + ], + ) diff --git a/tests/cc/py_extension/ext_init_in_dep.c b/tests/cc/py_extension/ext_init_in_dep.c new file mode 100644 index 0000000000..a6d32ba0b2 --- /dev/null +++ b/tests/cc/py_extension/ext_init_in_dep.c @@ -0,0 +1,20 @@ + +#include + +// No methods defined; we're just testing the init function. +static PyMethodDef ModuleMethods[] = { + {NULL, NULL, 0, NULL} /* Sentinel */ +}; + +static struct PyModuleDef ext_init_in_dep_module = { + PyModuleDef_HEAD_INIT, + "ext_init_in_dep", /* name of module */ + NULL, /* module documentation, may be NULL */ + -1, /* size of per-interpreter state of the module, + or -1 if the module keeps state in global variables. */ + ModuleMethods +}; + +PyMODINIT_FUNC PyInit_ext_init_in_dep(void) { + return PyModule_Create(&ext_init_in_dep_module); +} diff --git a/tests/cc/py_extension/ext_limited.c b/tests/cc/py_extension/ext_limited.c new file mode 100644 index 0000000000..f3622c5824 --- /dev/null +++ b/tests/cc/py_extension/ext_limited.c @@ -0,0 +1,22 @@ +#include + +static PyObject* get_limited_api_version(PyObject* self, PyObject* args) { + return PyUnicode_FromFormat("0x%08x", Py_LIMITED_API); +} + +static PyMethodDef ModuleMethods[] = { + {"get_limited_api_version", get_limited_api_version, METH_NOARGS, "Get the version of the limited API this extension was compiled against."}, + {NULL, NULL, 0, NULL} +}; + +static struct PyModuleDef ext_limited_module = { + PyModuleDef_HEAD_INIT, + "ext_limited", + NULL, + -1, + ModuleMethods +}; + +PyMODINIT_FUNC PyInit_ext_limited(void) { + return PyModule_Create(&ext_limited_module); +} diff --git a/tests/cc/py_extension/ext_pkg_test.c b/tests/cc/py_extension/ext_pkg_test.c new file mode 100644 index 0000000000..c151f0162b --- /dev/null +++ b/tests/cc/py_extension/ext_pkg_test.c @@ -0,0 +1,22 @@ +#include + +static PyObject* get_magic_number(PyObject* self, PyObject* args) { + return PyLong_FromLong(42); +} + +static PyMethodDef ModuleMethods[] = { + {"get_magic_number", get_magic_number, METH_NOARGS, "Returns 42."}, + {NULL, NULL, 0, NULL} +}; + +static struct PyModuleDef ext_pkg_test_module = { + PyModuleDef_HEAD_INIT, + "ext_pkg_test", + NULL, + -1, + ModuleMethods +}; + +PyMODINIT_FUNC PyInit_ext_pkg_test(void) { + return PyModule_Create(&ext_pkg_test_module); +} diff --git a/tests/cc/py_extension/ext_shared.c b/tests/cc/py_extension/ext_shared.c new file mode 100644 index 0000000000..4b79a6da2f --- /dev/null +++ b/tests/cc/py_extension/ext_shared.c @@ -0,0 +1,33 @@ +#include + +#include "tests/cc/py_extension/add_one.h" + +// A simple function that returns a Python integer. +static PyObject* do_alpha(PyObject* self, PyObject* args) { + return PyLong_FromLong(add_one(41)); +} + +// Method definition object for this extension, these are the functions +// that will be available in the module. +static PyMethodDef ModuleMethods[] = { + {"do_alpha", do_alpha, METH_NOARGS, "A simple C function."}, + {NULL, NULL, 0, NULL} /* Sentinel */ +}; + +// Module definition +// The arguments of this structure tell Python what to call your extension, +// what its methods are and where to look for its method definitions. +static struct PyModuleDef ext_shared_module = { + PyModuleDef_HEAD_INIT, + "ext_shared", /* name of module */ + NULL, /* module documentation, may be NULL */ + -1, /* size of per-interpreter state of the module, + or -1 if the module keeps state in global variables. */ + ModuleMethods +}; + +// The module init function. This must be exported and retained in the +// shared library output. +PyMODINIT_FUNC PyInit_ext_shared(void) { + return PyModule_Create(&ext_shared_module); +} diff --git a/tests/cc/py_extension/ext_source.c b/tests/cc/py_extension/ext_source.c new file mode 100644 index 0000000000..df0239df3a --- /dev/null +++ b/tests/cc/py_extension/ext_source.c @@ -0,0 +1,33 @@ + +#include + + +static PyObject* calc_one_plus_two(PyObject* self, PyObject* args) { + return PyLong_FromLong(1 + 2); +} + + +// Method definition object for this extension, these are the functions +// that will be available in the module. +static PyMethodDef ModuleMethods[] = { + {"calc_one_plus_two", calc_one_plus_two, METH_NOARGS, "A simple C function."}, + {NULL, NULL, 0, NULL} /* Sentinel */ +}; + +// Module definition +// The arguments of this structure tell Python what to call your extension, +// what its methods are and where to look for its method definitions. +static struct PyModuleDef ext_source_module = { + PyModuleDef_HEAD_INIT, + "ext_source", /* name of module */ + NULL, /* module documentation, may be NULL */ + -1, /* size of per-interpreter state of the module, + or -1 if the module keeps state in global variables. */ + ModuleMethods +}; + +// The module init function. This must be exported and retained in the +// shared library output. +PyMODINIT_FUNC PyInit_ext_source(void) { + return PyModule_Create(&ext_source_module); +} diff --git a/tests/cc/py_extension/ext_static.c b/tests/cc/py_extension/ext_static.c new file mode 100644 index 0000000000..500c52db00 --- /dev/null +++ b/tests/cc/py_extension/ext_static.c @@ -0,0 +1 @@ +/* A no-op C extension for static linking tests. */ diff --git a/tests/cc/py_extension/py_extension_pkg_test.py b/tests/cc/py_extension/py_extension_pkg_test.py new file mode 100644 index 0000000000..e9aac21dcd --- /dev/null +++ b/tests/cc/py_extension/py_extension_pkg_test.py @@ -0,0 +1,17 @@ +import unittest + +from tests.cc.py_extension import ext_pkg_test + + +class PyExtensionPkgTest(unittest.TestCase): + + def test_import_via_package(self): + self.assertEqual(ext_pkg_test.get_magic_number(), 42) + + def test_direct_import(self): + with self.assertRaises(ModuleNotFoundError): + import ext_pkg_test # buildifier: disable=g-import-not-at-top # noqa: F401 + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cc/py_extension/py_extension_test.py b/tests/cc/py_extension/py_extension_test.py new file mode 100644 index 0000000000..52d5502ea7 --- /dev/null +++ b/tests/cc/py_extension/py_extension_test.py @@ -0,0 +1,49 @@ +import os +import unittest + +import ext_shared +from elftools.elf.dynamic import DynamicSection +from elftools.elf.elffile import ELFFile + +from python.runfiles import runfiles + + +class PyExtensionTest(unittest.TestCase): + def test_inspect_elf(self): + r = runfiles.Create() + ext_path = r.Rlocation( + "rules_python/tests/cc/py_extension/" + + "ext_shared.cpython-311-x86_64-linux-gnu.so" + ) + self.assertTrue( + os.path.exists(ext_path), f"Could not find ext_shared.so at {ext_path}" + ) + + with open(ext_path, "rb") as f: + elf = ELFFile(f) + + # Check for DT_NEEDED entry for the dynamic library + dynamic_section = elf.get_section_by_name(".dynamic") + self.assertIsNotNone(dynamic_section) + self.assertTrue(isinstance(dynamic_section, DynamicSection)) + + needed_libs = [ + tag.needed + for tag in dynamic_section.iter_tags() + if tag.entry.d_tag == "DT_NEEDED" + ] + self.assertIn("libadd_one_shared.so", needed_libs) + + # Check for the PyInit symbol + dynsym_section = elf.get_section_by_name(".dynsym") + self.assertIsNotNone(dynsym_section) + + symbols = [s.name for s in dynsym_section.iter_symbols()] + self.assertIn("PyInit_ext_shared", symbols) + + def test_import_and_call(self): + self.assertEqual(ext_shared.do_alpha(), 43) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cc/py_extension/py_extension_tests.bzl b/tests/cc/py_extension/py_extension_tests.bzl new file mode 100644 index 0000000000..0d66f07eec --- /dev/null +++ b/tests/cc/py_extension/py_extension_tests.bzl @@ -0,0 +1,102 @@ +# 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. + +"""Tests for py_extension.""" + +load("@rules_testing//lib:analysis_test.bzl", "analysis_test", "test_suite") +load("@rules_testing//lib:truth.bzl", "matching") +load("//python/private:py_info.bzl", "PyInfo") # buildifier: disable=bzl-visibility + +_tests = [] + +def _test_static_deps_impl(env, target): + env.expect.that_target(target).has_provider(PyInfo) + py_info = target[PyInfo] + + # The .so should be in PyInfo + env.expect.that_collection(py_info.transitive_sources.to_list()).has_size(1) + env.expect.that_depset_of_files(py_info.transitive_sources).contains_predicate( + matching.file_basename_equals("ext_static.cpython-311-x86_64-linux-gnu.so"), + ) + +def _test_static_deps(name): + analysis_test( + name = name, + impl = _test_static_deps_impl, + target = "//tests/cc/py_extension:ext_static", + ) + +_tests.append(_test_static_deps) + +def _test_data_deps_impl(env, target): + env.expect.that_target(target).has_provider(PyInfo) + + # Check that data file is in runfiles + default_info = target[DefaultInfo] + env.expect.that_depset_of_files(default_info.default_runfiles.files).contains_predicate( + matching.file_basename_equals("some_data.txt"), + ) + +def _test_data_deps(name): + analysis_test( + name = name, + impl = _test_data_deps_impl, + target = "//tests/cc/py_extension:ext_with_data", + ) + +_tests.append(_test_data_deps) + +def _test_dynamic_deps_impl(env, target): + env.expect.that_target(target).has_provider(PyInfo) + py_info = target[PyInfo] + + # The .so should be in PyInfo + env.expect.that_collection(py_info.transitive_sources.to_list()).has_size(1) + env.expect.that_depset_of_files(py_info.transitive_sources).contains_predicate( + matching.file_basename_equals("ext_shared.cpython-311-x86_64-linux-gnu.so"), + ) + +def _test_dynamic_deps(name): + analysis_test( + name = name, + impl = _test_dynamic_deps_impl, + target = "//tests/cc/py_extension:ext_shared", + ) + +_tests.append(_test_dynamic_deps) + +def _test_musl_platform_impl(env, target): + env.expect.that_target(target).has_provider(PyInfo) + py_info = target[PyInfo] + env.expect.that_depset_of_files(py_info.transitive_sources).contains_predicate( + matching.file_basename_equals("ext_static.cpython-311-x86_64-linux-musl.so"), + ) + +def _test_musl_platform(name): + analysis_test( + name = name, + impl = _test_musl_platform_impl, + target = "//tests/cc/py_extension:ext_static", + config_settings = { + str(Label("//python/config_settings:py_linux_libc")): "musl", + }, + ) + +_tests.append(_test_musl_platform) + +def py_extension_analysis_test_suite(name): + test_suite( + name = name, + tests = _tests, + ) diff --git a/tests/cc/py_extension/py_limited_api_tests.bzl b/tests/cc/py_extension/py_limited_api_tests.bzl new file mode 100644 index 0000000000..a59628c19c --- /dev/null +++ b/tests/cc/py_extension/py_limited_api_tests.bzl @@ -0,0 +1,139 @@ +# 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. + +"""Tests for the py_limited_api attribute for py_extension.""" + +load("@rules_cc//cc:cc_library.bzl", "cc_library") +load("@rules_testing//lib:analysis_test.bzl", "analysis_test", "test_suite") +load("@rules_testing//lib:util.bzl", "util") +load("//python/cc:py_extension.bzl", "py_extension") + +def _test_limited_pass_impl(env, target): + env.expect.that_target(target).default_outputs().contains( + "tests/cc/py_extension/{}.abi3.so".format(target.label.name), + ) + +def _test_limited_same_version(name): + util.helper_target( + cc_library, + name = name + "_csl", + defines = ["Py_LIMITED_API=0x03080000"], + deps = [ + "@rules_python//python/cc:current_py_cc_headers", + ], + ) + py_extension( + name = name + "_pyext", + deps = [":" + name + "_csl"], + py_limited_api = "3.8", + ) + analysis_test( + name = name, + target = name + "_pyext", + impl = _test_limited_pass_impl, + ) + +def _test_limited_older_dep(name): + util.helper_target( + cc_library, + name = name + "_csl", + defines = ["Py_LIMITED_API=0x03080000"], # 3.8 + deps = [ + "@rules_python//python/cc:current_py_cc_headers", + ], + ) + py_extension( + name = name + "_pyext", + deps = [":" + name + "_csl"], + py_limited_api = "3.9", # 3.9 + ) + analysis_test( + name = name, + target = name + "_pyext", + impl = _test_limited_pass_impl, + ) + +def _test_no_limited_api(name): + util.helper_target( + cc_library, + name = name + "_csl", + deps = [ + "@rules_python//python/cc:current_py_cc_headers", + ], + ) + py_extension( + name = name + "_pyext", + deps = [":" + name + "_csl"], + ) + analysis_test( + name = name, + target = name + "_pyext", + impl = _test_no_limited_api_impl, + ) + +def _test_no_limited_api_impl(env, target): + # Should pass, nothing to assert on filename since it is platform-specific + _ = env # @unused + _ = target # @unused + +def _test_no_limited_api_dep_has_limited(name): + util.helper_target( + cc_library, + name = name + "_csl", + defines = ["Py_LIMITED_API=0x03080000"], + deps = [ + "@rules_python//python/cc:current_py_cc_headers", + ], + ) + py_extension( + name = name + "_pyext", + deps = [":" + name + "_csl"], + ) + analysis_test( + name = name, + target = name + "_pyext", + impl = _test_no_limited_api_dep_has_limited_impl, + ) + +def _test_no_limited_api_dep_has_limited_impl(env, target): + _ = env # @unused + _ = target # @unused + +def _test_limited_api_dep_has_no_python(name): + util.helper_target( + cc_library, + name = name + "_csl", + ) + py_extension( + name = name + "_pyext", + deps = [":" + name + "_csl"], + py_limited_api = "3.8", + ) + analysis_test( + name = name, + target = name + "_pyext", + impl = _test_limited_pass_impl, + ) + +def py_limited_api_test_suite(name): + test_suite( + name = name, + tests = [ + _test_limited_same_version, + _test_limited_older_dep, + _test_no_limited_api, + _test_no_limited_api_dep_has_limited, + _test_limited_api_dep_has_no_python, + ], + ) diff --git a/tests/cc/py_extension/some_data.txt b/tests/cc/py_extension/some_data.txt new file mode 100644 index 0000000000..4b5dc1d64c --- /dev/null +++ b/tests/cc/py_extension/some_data.txt @@ -0,0 +1 @@ +This is a data file diff --git a/tests/cc/py_extension/static_dep.c b/tests/cc/py_extension/static_dep.c new file mode 100644 index 0000000000..fa95e4dacc --- /dev/null +++ b/tests/cc/py_extension/static_dep.c @@ -0,0 +1,5 @@ +#include "static_dep.h" + +int my_lib_func() { + return 42; +} diff --git a/tests/cc/py_extension/static_dep.h b/tests/cc/py_extension/static_dep.h new file mode 100644 index 0000000000..d0f272abd7 --- /dev/null +++ b/tests/cc/py_extension/static_dep.h @@ -0,0 +1 @@ +int my_lib_func(); diff --git a/tests/cc/py_extension/test_lib_a.c b/tests/cc/py_extension/test_lib_a.c new file mode 100644 index 0000000000..19e2f489bf --- /dev/null +++ b/tests/cc/py_extension/test_lib_a.c @@ -0,0 +1,6 @@ +#include "test_symbols.h" + +void fnA() { + fnB(); + fnC(); +} diff --git a/tests/cc/py_extension/test_lib_b.c b/tests/cc/py_extension/test_lib_b.c new file mode 100644 index 0000000000..3621587c1c --- /dev/null +++ b/tests/cc/py_extension/test_lib_b.c @@ -0,0 +1,5 @@ +#include "test_symbols.h" + +void fnB() { + fnC(); +} diff --git a/tests/cc/py_extension/test_lib_c.c b/tests/cc/py_extension/test_lib_c.c new file mode 100644 index 0000000000..99941576dd --- /dev/null +++ b/tests/cc/py_extension/test_lib_c.c @@ -0,0 +1,6 @@ +#include "test_symbols.h" +#include + +void fnC() { + printf("fnC\n"); +} diff --git a/tests/cc/py_extension/test_symbols.h b/tests/cc/py_extension/test_symbols.h new file mode 100644 index 0000000000..59ab3b02e7 --- /dev/null +++ b/tests/cc/py_extension/test_symbols.h @@ -0,0 +1,8 @@ +#ifndef TEST_SYMBOLS_H +#define TEST_SYMBOLS_H + +void fnC(); +void fnB(); +void fnA(); + +#endif // TEST_SYMBOLS_H diff --git a/tests/config_settings/transition/BUILD.bazel b/tests/config_settings/transition/BUILD.bazel index 19d4958669..093de50959 100644 --- a/tests/config_settings/transition/BUILD.bazel +++ b/tests/config_settings/transition/BUILD.bazel @@ -1,6 +1,3 @@ load(":multi_version_tests.bzl", "multi_version_test_suite") -load(":py_args_tests.bzl", "py_args_test_suite") - -py_args_test_suite(name = "py_args_tests") multi_version_test_suite(name = "multi_version_tests") diff --git a/tests/config_settings/transition/multi_version_tests.bzl b/tests/config_settings/transition/multi_version_tests.bzl index 93f6efd728..2b4f73a225 100644 --- a/tests/config_settings/transition/multi_version_tests.bzl +++ b/tests/config_settings/transition/multi_version_tests.bzl @@ -16,13 +16,15 @@ load("@pythons_hub//:versions.bzl", "DEFAULT_PYTHON_VERSION") load("@rules_testing//lib:analysis_test.bzl", "analysis_test") load("@rules_testing//lib:test_suite.bzl", "test_suite") -load("@rules_testing//lib:util.bzl", "TestingAspectInfo", rt_util = "util") +load("@rules_testing//lib:util.bzl", rt_util = "util") load("//python:py_binary.bzl", "py_binary") load("//python:py_info.bzl", "PyInfo") load("//python:py_test.bzl", "py_test") +load("//python/private:common.bzl", "maybe_builtin_build_python_zip") # buildifier: disable=bzl-visibility +load("//python/private:common_labels.bzl", "labels") # buildifier: disable=bzl-visibility load("//python/private:reexports.bzl", "BuiltinPyInfo") # buildifier: disable=bzl-visibility -load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER") # buildifier: disable=bzl-visibility load("//tests/support:support.bzl", "CC_TOOLCHAIN") +load("//tests/support/platforms:platforms.bzl", "platform_targets") # NOTE @aignas 2024-06-04: we are using here something that is registered in the MODULE.Bazel # and if you find tests failing, it could be because of the toolchain resolution issues here. @@ -91,64 +93,32 @@ def _setup_py_binary_windows(name, *, impl, build_python_zip): target = name + "_subject", impl = impl, config_settings = { - "//command_line_option:build_python_zip": build_python_zip, + labels.BUILD_PYTHON_ZIP: build_python_zip, "//command_line_option:extra_toolchains": CC_TOOLCHAIN, - "//command_line_option:platforms": str(Label("//tests/support:windows_x86_64")), - }, + "//command_line_option:platforms": str(platform_targets.WINDOWS_X86_64), + } | maybe_builtin_build_python_zip(str(build_python_zip)), ) def _test_py_binary_windows_build_python_zip_false(name): _setup_py_binary_windows( name, - build_python_zip = "false", + build_python_zip = False, impl = _test_py_binary_windows_build_python_zip_false_impl, ) def _test_py_binary_windows_build_python_zip_false_impl(env, target): default_outputs = env.expect.that_target(target).default_outputs() - if IS_BAZEL_7_OR_HIGHER: - # TODO: These outputs aren't correct. The outputs shouldn't - # have the "_" prefix on them (those are coming from the underlying - # wrapped binary). - env.expect.that_target(target).default_outputs().contains_exactly([ - "{package}/{test_name}_subject.exe", - "{package}/{test_name}_subject", - "{package}/{test_name}_subject.py", - ]) - else: - inner_exe = target[TestingAspectInfo].attrs.target[DefaultInfo].files_to_run.executable - default_outputs.contains_at_least([ - inner_exe.short_path, - ]) -_tests.append(_test_py_binary_windows_build_python_zip_false) + # TODO: These outputs aren't correct. The outputs shouldn't + # have the "_" prefix on them (those are coming from the underlying + # wrapped binary). + default_outputs.contains_exactly([ + "{package}/{test_name}_subject.exe", + "{package}/{test_name}_subject", + "{package}/{test_name}_subject.py", + ]) -def _test_py_binary_windows_build_python_zip_true(name): - _setup_py_binary_windows( - name, - build_python_zip = "true", - impl = _test_py_binary_windows_build_python_zip_true_impl, - ) - -def _test_py_binary_windows_build_python_zip_true_impl(env, target): - default_outputs = env.expect.that_target(target).default_outputs() - if IS_BAZEL_7_OR_HIGHER: - # TODO: These outputs aren't correct. The outputs shouldn't - # have the "_" prefix on them (those are coming from the underlying - # wrapped binary). - default_outputs.contains_exactly([ - "{package}/{test_name}_subject.exe", - "{package}/{test_name}_subject.py", - "{package}/{test_name}_subject.zip", - ]) - else: - inner_exe = target[TestingAspectInfo].attrs.target[DefaultInfo].files_to_run.executable - default_outputs.contains_at_least([ - "{package}/{test_name}_subject.zip", - inner_exe.short_path, - ]) - -_tests.append(_test_py_binary_windows_build_python_zip_true) +_tests.append(_test_py_binary_windows_build_python_zip_false) def multi_version_test_suite(name): test_suite( diff --git a/tests/config_settings/transition/py_args_tests.bzl b/tests/config_settings/transition/py_args_tests.bzl deleted file mode 100644 index 4538c88a5c..0000000000 --- a/tests/config_settings/transition/py_args_tests.bzl +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright 2023 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("@rules_testing//lib:test_suite.bzl", "test_suite") -load("//python/config_settings/private:py_args.bzl", "py_args") # buildifier: disable=bzl-visibility - -_tests = [] - -def _test_py_args_default(env): - actual = py_args("foo", {}) - - want = { - "args": None, - "data": None, - "deps": None, - "env": None, - "main": "foo.py", - "srcs": None, - } - env.expect.that_dict(actual).contains_exactly(want) - -_tests.append(_test_py_args_default) - -def _test_kwargs_get_consumed(env): - kwargs = { - "args": ["some", "args"], - "data": ["data"], - "deps": ["deps"], - "env": {"key": "value"}, - "main": "__main__.py", - "srcs": ["__main__.py"], - "visibility": ["//visibility:public"], - } - actual = py_args("bar_bin", kwargs) - - want = { - "args": ["some", "args"], - "data": ["data"], - "deps": ["deps"], - "env": {"key": "value"}, - "main": "__main__.py", - "srcs": ["__main__.py"], - } - env.expect.that_dict(actual).contains_exactly(want) - env.expect.that_dict(kwargs).keys().contains_exactly(["visibility"]) - -_tests.append(_test_kwargs_get_consumed) - -def py_args_test_suite(name): - """Create the test suite. - - Args: - name: the name of the test suite - """ - test_suite(name = name, basic_tests = _tests) diff --git a/tests/integration/ignore_root_user_error/foo_test.py b/tests/coverage_deps/BUILD.bazel similarity index 69% rename from tests/integration/ignore_root_user_error/foo_test.py rename to tests/coverage_deps/BUILD.bazel index 724cdcb69a..8ec6025902 100644 --- a/tests/integration/ignore_root_user_error/foo_test.py +++ b/tests/coverage_deps/BUILD.bazel @@ -1,13 +1,17 @@ -# Copyright 2019 The Bazel Authors. All rights reserved. +# Copyright 2026 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 +# 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(":coverage_deps_test.bzl", "coverage_deps_test_suite") + +coverage_deps_test_suite(name = "coverage_deps_tests") diff --git a/tests/coverage_deps/coverage_deps_test.bzl b/tests/coverage_deps/coverage_deps_test.bzl new file mode 100644 index 0000000000..12351affde --- /dev/null +++ b/tests/coverage_deps/coverage_deps_test.bzl @@ -0,0 +1,95 @@ +# Copyright 2026 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. + +"Tests for the warning emitted by coverage_dep when no wheel is available." + +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("//python/private:coverage_deps.bzl", "coverage_dep") # buildifier: disable=bzl-visibility +load("//python/private:repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "REPO_VERBOSITY_ENV_VAR", "repo_utils") # buildifier: disable=bzl-visibility + +_tests = [] + +def _capturing_logger(): + """Build a (logger, captured_messages_list) pair. + + The logger has its verbosity set to INFO so WARN messages are captured but + nothing noisier than necessary is emitted. The printer collects the second + positional argument from each printer invocation (the formatted message). + """ + captured = [] + logger = repo_utils.logger( + struct( + getenv = { + REPO_DEBUG_ENV_VAR: None, + REPO_VERBOSITY_ENV_VAR: "INFO", + }.get, + ), + name = "unit-test", + printer = lambda _key, message: captured.append(message), + ) + return logger, captured + +def _test_unsupported_python_version_warns(env): + # cp37 is not in the bundled wheel set; coverage_dep should return None + # and emit a warning describing the misconfiguration. + logger, captured = _capturing_logger() + result = coverage_dep( + name = "unused_for_test", + python_version = "3.7", + platform = "aarch64-apple-darwin", + visibility = ["//visibility:public"], + logger = logger, + ) + env.expect.that_bool(result == None).equals(True) + env.expect.that_int(len(captured)).equals(1) + env.expect.that_str(captured[0]).contains("no wheel for") + env.expect.that_str(captured[0]).contains("python_version=3.7") + env.expect.that_str(captured[0]).contains("platform=aarch64-apple-darwin") + +_tests.append(_test_unsupported_python_version_warns) + +def _test_windows_platform_is_silent(env): + # Windows is intentionally unsupported and not actionable; coverage_dep + # must return None without logging anything. + logger, captured = _capturing_logger() + result = coverage_dep( + name = "unused_for_test", + python_version = "3.10", + platform = "x86_64-pc-windows-msvc", + visibility = ["//visibility:public"], + logger = logger, + ) + env.expect.that_bool(result == None).equals(True) + env.expect.that_int(len(captured)).equals(0) + +_tests.append(_test_windows_platform_is_silent) + +# NOTE: there is intentionally no unit test for the supported-wheel path +# (where coverage_dep returns a non-None label and emits no warning). +# That path calls `maybe(http_archive, ...)`, which calls +# `native.existing_rule()`. `native.existing_rule()` is only valid during +# BUILD file, legacy macro, or rule finalizer evaluation -- not during +# rule analysis, which is the phase rules_testing analysis tests run in. +# Calling coverage_dep with supported args from here therefore fails with +# "existing_rule() can only be used while evaluating a BUILD file, ...". +# The supported-wheel path is exercised end-to-end by `bazel coverage` +# against a real py_test target during ordinary use of the toolchain. + +def coverage_deps_test_suite(name): + """Create the test suite. + + Args: + name: the name of the test suite. + """ + test_suite(name = name, basic_tests = _tests) diff --git a/tests/docs/BUILD.bazel b/tests/docs/BUILD.bazel new file mode 100644 index 0000000000..bdc99a290c --- /dev/null +++ b/tests/docs/BUILD.bazel @@ -0,0 +1,10 @@ +load("@bazel_skylib//rules:build_test.bzl", "build_test") + +licenses(["notice"]) + +build_test( + name = "docs_build_test", + targets = [ + "//docs:docs", + ], +) diff --git a/tests/entry_points/py_console_script_gen_test.py b/tests/entry_points/py_console_script_gen_test.py index 1bbf5fbf25..92fa42f167 100644 --- a/tests/entry_points/py_console_script_gen_test.py +++ b/tests/entry_points/py_console_script_gen_test.py @@ -162,7 +162,7 @@ def test_a_single_entry_point(self): raise if __name__ == "__main__": - sys.exit(baz()) + sys.exit(baz()) # type: ignore """ ) self.assertEqual(want, got) @@ -194,8 +194,8 @@ def test_a_second_entry_point_class_method(self): got = out.read_text() - self.assertRegex(got, "from foo\.baz import Bar") - self.assertRegex(got, "sys\.exit\(Bar\.baz\(\)\)") + self.assertRegex(got, r"from foo\.baz import Bar") + self.assertRegex(got, r"sys\.exit\(Bar\.baz\(\)\)") def test_shebang_included(self): with tempfile.TemporaryDirectory() as tmpdir: diff --git a/tests/exec_toolchain_matching/exec_toolchain_matching_tests.bzl b/tests/exec_toolchain_matching/exec_toolchain_matching_tests.bzl index 43a9717314..a26e4f5f6e 100644 --- a/tests/exec_toolchain_matching/exec_toolchain_matching_tests.bzl +++ b/tests/exec_toolchain_matching/exec_toolchain_matching_tests.bzl @@ -20,8 +20,7 @@ load("//python:py_runtime.bzl", "py_runtime") load("//python:py_runtime_pair.bzl", "py_runtime_pair") load("//python/private:common_labels.bzl", "labels") # buildifier: disable=bzl-visibility load("//python/private:toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE", "TARGET_TOOLCHAIN_TYPE") # buildifier: disable=bzl-visibility -load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER") # buildifier: disable=bzl-visibility -load("//tests/support:support.bzl", "LINUX", "MAC") +load("//tests/support/platforms:platforms.bzl", "platform_targets") _LookupInfo = provider() # buildifier: disable=provider-params @@ -127,9 +126,9 @@ def _test_exec_matches_target_python_version(name): target = name + "_subject", impl = _test_exec_matches_target_python_version_impl, config_settings = { - "//command_line_option:extra_execution_platforms": [str(MAC)], + "//command_line_option:extra_execution_platforms": [str(platform_targets.MAC)], "//command_line_option:extra_toolchains": ["//tests/exec_toolchain_matching:all"], - "//command_line_option:platforms": [str(LINUX)], + "//command_line_option:platforms": [str(platform_targets.LINUX)], labels.PYTHON_VERSION: "3.12", }, ) @@ -143,11 +142,10 @@ def _test_exec_matches_target_python_version_impl(env, target): env.expect.that_str(target_runtime.interpreter_path).equals("/linux/python3.12") env.expect.that_str(exec_runtime.interpreter_path).equals("/mac/python3.12") - if IS_BAZEL_7_OR_HIGHER: - target_version = target_runtime.interpreter_version_info - exec_version = exec_runtime.interpreter_version_info + target_version = target_runtime.interpreter_version_info + exec_version = exec_runtime.interpreter_version_info - env.expect.that_bool(target_version == exec_version) + env.expect.that_bool(target_version == exec_version) def exec_toolchain_matching_test_suite(name): test_suite(name = name, tests = _tests) diff --git a/python/private/register_extension_info.bzl b/tests/get_release_info/BUILD.bazel similarity index 65% rename from python/private/register_extension_info.bzl rename to tests/get_release_info/BUILD.bazel index 408df6261e..26517e6dec 100644 --- a/python/private/register_extension_info.bzl +++ b/tests/get_release_info/BUILD.bazel @@ -1,4 +1,4 @@ -# Copyright 2023 The Bazel Authors. All rights reserved. +# Copyright 2024 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. @@ -11,8 +11,14 @@ # 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. -"""Stub implementation to make patching easier.""" -# buildifier: disable=unused-variable -def register_extension_info(**kwargs): - """A no-op stub to make Google patching easier.""" +load(":get_release_info_tests.bzl", "get_release_info_test_suite") + +package( + default_testonly = True, + default_visibility = ["//:__subpackages__"], +) + +licenses(["notice"]) + +get_release_info_test_suite(name = "get_release_info") diff --git a/tests/get_release_info/get_release_info_tests.bzl b/tests/get_release_info/get_release_info_tests.bzl new file mode 100644 index 0000000000..0b1b60adc3 --- /dev/null +++ b/tests/get_release_info/get_release_info_tests.bzl @@ -0,0 +1,112 @@ +# Copyright 2024 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. + +"""Tests for get_release_info.""" + +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("//python:versions.bzl", "get_release_info") # buildifier: disable=bzl-visibility + +_tests = [] + +def _test_file_url(env): + """Tests that a file:/// url is handled correctly.""" + tool_versions = { + "3.11.5": { + "sha256": { + "x86_64-unknown-linux-gnu": "fbed6f7694b2faae5d7c401a856219c945397f772eea5ca50c6eb825cbc9d1e1", + }, + "strip_prefix": "python", + "url": "file:///tmp/cpython-3.11.5.tar.gz", + }, + } + + expected_url = "file:///tmp/cpython-3.11.5.tar.gz" + expected_filename = "file:///tmp/cpython-3.11.5.tar.gz" + + filename, urls, strip_prefix, patches, patch_strip = get_release_info( + platform = "x86_64-unknown-linux-gnu", + python_version = "3.11.5", + tool_versions = tool_versions, + ) + + env.expect.that_str(filename).equals(expected_filename) + env.expect.that_collection(urls).contains_exactly([expected_url]) + env.expect.that_str(strip_prefix).equals("python") + env.expect.that_collection(patches).has_size(0) + env.expect.that_bool(patch_strip == None).equals(True) + +_tests.append(_test_file_url) + +def _test_astral_mirror(env): + """Tests that the releases.astral.sh mirror is added as a secondary URL.""" + tool_versions = { + "3.11.5": { + "sha256": { + "x86_64-unknown-linux-gnu": "fbed6f7694b2faae5d7c401a856219c945397f772eea5ca50c6eb825cbc9d1e1", + }, + "strip_prefix": "python", + "url": "20230826/cpython-{python_version}+20230826-{platform}-{build}.tar.gz", + }, + } + + expected_urls = [ + "https://github.com/astral-sh/python-build-standalone/releases/download/20230826/cpython-3.11.5+20230826-x86_64-unknown-linux-gnu-install_only.tar.gz", + "https://releases.astral.sh/github/python-build-standalone/releases/download/20230826/cpython-3.11.5+20230826-x86_64-unknown-linux-gnu-install_only.tar.gz", + "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5+20230826-x86_64-unknown-linux-gnu-install_only.tar.gz", + ] + + _, urls, _, _, _ = get_release_info( + platform = "x86_64-unknown-linux-gnu", + python_version = "3.11.5", + tool_versions = tool_versions, + ) + + env.expect.that_collection(urls).contains_exactly(expected_urls) + +_tests.append(_test_astral_mirror) + +def _test_astral_mirror_legacy(env): + """Tests that the releases.astral.sh mirror is added for legacy indygreg URLs.""" + tool_versions = { + "3.11.5": { + "sha256": { + "x86_64-unknown-linux-gnu": "fbed6f7694b2faae5d7c401a856219c945397f772eea5ca50c6eb825cbc9d1e1", + }, + "strip_prefix": "python", + "url": "20230826/cpython-{python_version}+20230826-{platform}-{build}.tar.gz", + }, + } + + expected_urls = [ + "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5+20230826-x86_64-unknown-linux-gnu-install_only.tar.gz", + "https://releases.astral.sh/github/python-build-standalone/releases/download/20230826/cpython-3.11.5+20230826-x86_64-unknown-linux-gnu-install_only.tar.gz", + ] + + _, urls, _, _, _ = get_release_info( + platform = "x86_64-unknown-linux-gnu", + python_version = "3.11.5", + base_urls = ["https://github.com/indygreg/python-build-standalone/releases/download"], + tool_versions = tool_versions, + ) + + env.expect.that_collection(urls).contains_exactly(expected_urls) + +_tests.append(_test_astral_mirror_legacy) + +def get_release_info_test_suite(name): + """Defines the test suite for get_release_info.""" + test_suite( + name = name, + basic_tests = _tests, + ) diff --git a/tests/implicit_namespace_packages/BUILD.bazel b/tests/implicit_namespace_packages/BUILD.bazel index 42aca9b97f..b544c4d118 100644 --- a/tests/implicit_namespace_packages/BUILD.bazel +++ b/tests/implicit_namespace_packages/BUILD.bazel @@ -1,10 +1,10 @@ load("//python:py_test.bzl", "py_test") -load("//tests/support:support.bzl", "SUPPORTS_BZLMOD_UNIXY") +load("//tests/support:support.bzl", "SUPPORTS_BZLMOD") py_test( name = "namespace_packages_test", srcs = ["namespace_packages_test.py"], - target_compatible_with = SUPPORTS_BZLMOD_UNIXY, + target_compatible_with = SUPPORTS_BZLMOD, deps = [ "@implicit_namespace_ns_sub1//:pkg", "@implicit_namespace_ns_sub2//:pkg", diff --git a/tests/implicit_namespace_packages/namespace_packages_test.py b/tests/implicit_namespace_packages/namespace_packages_test.py index ea47c08fd2..a1ee27d71b 100644 --- a/tests/implicit_namespace_packages/namespace_packages_test.py +++ b/tests/implicit_namespace_packages/namespace_packages_test.py @@ -2,7 +2,6 @@ class NamespacePackagesTest(unittest.TestCase): - def test_both_importable(self): import nspkg import nspkg.subpkg1 diff --git a/tests/implicit_namespace_packages/testdata/ns-sub1/ns-sub1-1.0.dist-info/METADATA b/tests/implicit_namespace_packages/testdata/ns-sub1/ns-sub1-1.0.dist-info/METADATA index e69de29bb2..ecec6086ba 100644 --- a/tests/implicit_namespace_packages/testdata/ns-sub1/ns-sub1-1.0.dist-info/METADATA +++ b/tests/implicit_namespace_packages/testdata/ns-sub1/ns-sub1-1.0.dist-info/METADATA @@ -0,0 +1,2 @@ +Name: ns-sub1 +Version: 1.0 diff --git a/tests/implicit_namespace_packages/testdata/ns-sub2/ns_sub2-1.0.dist-info/METADATA b/tests/implicit_namespace_packages/testdata/ns-sub2/ns_sub2-1.0.dist-info/METADATA index e69de29bb2..92cbb8ec2e 100644 --- a/tests/implicit_namespace_packages/testdata/ns-sub2/ns_sub2-1.0.dist-info/METADATA +++ b/tests/implicit_namespace_packages/testdata/ns-sub2/ns_sub2-1.0.dist-info/METADATA @@ -0,0 +1,2 @@ +Name: ns-sub2 +Version: 1.0 diff --git a/tests/integration/BUILD.bazel b/tests/integration/BUILD.bazel index df7fe15444..abdb37be57 100644 --- a/tests/integration/BUILD.bazel +++ b/tests/integration/BUILD.bazel @@ -12,9 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -load("@bazel_binaries//:defs.bzl", "bazel_binaries") 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"]) @@ -24,14 +25,6 @@ _WORKSPACE_FLAGS = [ "--enable_workspace", ] -_WORKSPACE_GAZELLE_PLUGIN_FLAGS = [ - "--override_repository=rules_python_gazelle_plugin=../../../rules_python_gazelle_plugin", -] - -_GAZELLE_PLUGIN_FLAGS = [ - "--override_module=rules_python_gazelle_plugin=../../../rules_python_gazelle_plugin", -] - default_test_runner( name = "workspace_test_runner", bazel_cmds = [ @@ -42,30 +35,25 @@ default_test_runner( ) default_test_runner( - name = "workspace_test_runner_gazelle_plugin", - bazel_cmds = [ - "info {}".format(" ".join(_WORKSPACE_FLAGS + _WORKSPACE_GAZELLE_PLUGIN_FLAGS)), - "test {} //...".format(" ".join(_WORKSPACE_FLAGS + _WORKSPACE_GAZELLE_PLUGIN_FLAGS)), - ], + name = "test_runner", visibility = ["//visibility:public"], ) -default_test_runner( - name = "test_runner", - visibility = ["//visibility:public"], +rules_python_integration_test( + name = "bzlmod_lockfile_test", + bazel_versions = ["9.1.0"], ) -default_test_runner( - name = "test_runner_gazelle_plugin", - bazel_cmds = [ - "info {}".format(" ".join(_GAZELLE_PLUGIN_FLAGS)), - "test {} //...".format(" ".join(_GAZELLE_PLUGIN_FLAGS)), +test_suite( + name = "subset", + tags = ["manual"], + tests = [ + "bzlmod_lockfile_test_bazel_9.1.0", + "local_toolchains_test_bazel_self", + "uv_lock_test_bazel_self", ], - visibility = ["//visibility:public"], ) -# TODO: add compile_pip_requirements_test_from_external_repo - rules_python_integration_test( name = "compile_pip_requirements_test", ) @@ -76,33 +64,19 @@ rules_python_integration_test( workspace_path = "compile_pip_requirements", ) -rules_python_integration_test( - name = "ignore_root_user_error_test", -) - -rules_python_integration_test( - name = "ignore_root_user_error_workspace_test", - bzlmod = False, - workspace_path = "ignore_root_user_error", -) - rules_python_integration_test( name = "local_toolchains_test", - bazel_versions = [ - version - for version in bazel_binaries.versions.all - if not version.startswith("6.") - ], + env = { + "RULES_PYTHON_BZLMOD_DEBUG": "1", + }, ) rules_python_integration_test( name = "local_toolchains_workspace_test", - bazel_versions = [ - version - for version in bazel_binaries.versions.all - if not version.startswith("6.") - ], bzlmod = False, + env = { + "RULES_PYTHON_BZLMOD_DEBUG": "1", + }, workspace_path = "local_toolchains", ) @@ -110,6 +84,10 @@ rules_python_integration_test( name = "pip_parse_test", ) +rules_python_integration_test( + name = "pip_parse_isolated_test", +) + rules_python_integration_test( name = "pip_parse_workspace_test", bzlmod = False, @@ -126,13 +104,65 @@ rules_python_integration_test( workspace_path = "py_cc_toolchain_registered", ) +rules_python_integration_test( + name = "runtime_manifests_test", +) + rules_python_integration_test( name = "custom_commands_test", py_main = "custom_commands_test.py", ) +rules_python_integration_test( + name = "toolchain_target_settings_test", + py_main = "toolchain_target_settings_test.py", +) + +rules_python_integration_test( + name = "validate_test_main_test", + py_main = "validate_test_main_test.py", +) + +rules_python_integration_test( + name = "unified_pypi_test", + py_main = "unified_pypi_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"], +) diff --git a/tests/integration/bzlmod_lockfile/.bazelrc b/tests/integration/bzlmod_lockfile/.bazelrc new file mode 100644 index 0000000000..3687a11fae --- /dev/null +++ b/tests/integration/bzlmod_lockfile/.bazelrc @@ -0,0 +1,9 @@ +# Bazel configuration flags + +build --enable_runfiles +common --lockfile_mode=error + +common --experimental_isolated_extension_usages + +# https://docs.bazel.build/versions/main/best-practices.html#using-the-bazelrc-file +try-import %workspace%/user.bazelrc diff --git a/tests/integration/bzlmod_lockfile/.bazelversion b/tests/integration/bzlmod_lockfile/.bazelversion new file mode 100644 index 0000000000..47da986f86 --- /dev/null +++ b/tests/integration/bzlmod_lockfile/.bazelversion @@ -0,0 +1 @@ +9.1.0 diff --git a/tests/integration/bzlmod_lockfile/BUILD.bazel b/tests/integration/bzlmod_lockfile/BUILD.bazel new file mode 100644 index 0000000000..7e99a242ae --- /dev/null +++ b/tests/integration/bzlmod_lockfile/BUILD.bazel @@ -0,0 +1,10 @@ +load("@rules_python//python:py_test.bzl", "py_test") + +py_test( + name = "test_dummy", + srcs = ["test_dummy.py"], + deps = [ + "@pypi//pycparser", + "@pypi//six", + ], +) diff --git a/tests/integration/bzlmod_lockfile/MODULE.bazel b/tests/integration/bzlmod_lockfile/MODULE.bazel new file mode 100644 index 0000000000..6c074bcdd4 --- /dev/null +++ b/tests/integration/bzlmod_lockfile/MODULE.bazel @@ -0,0 +1,19 @@ +module(name = "bzlmod_lockfile") + +bazel_dep(name = "rules_python") +local_path_override( + module_name = "rules_python", + path = "../../..", +) + +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.toolchain(python_version = "3.13") + +# TODO: This test module should also verify that isolate = True works, will do in a followup PR. +pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") +pip.parse( + hub_name = "pypi", + python_version = "3.13", + requirements_lock = "//:requirements_lock.txt", +) +use_repo(pip, "pypi") diff --git a/tests/integration/bzlmod_lockfile/MODULE.bazel.lock b/tests/integration/bzlmod_lockfile/MODULE.bazel.lock new file mode 100644 index 0000000000..fd161a3965 --- /dev/null +++ b/tests/integration/bzlmod_lockfile/MODULE.bazel.lock @@ -0,0 +1,649 @@ +{ + "lockFileVersion": 26, + "registryFileHashes": { + "https://bcr.bazel.build/bazel_registry.json": "8a28e4aff06ee60aed2a8c281907fb8bcbf3b753c91fb5a5c57da3215d5b3497", + "https://bcr.bazel.build/modules/abseil-cpp/20210324.2/MODULE.bazel": "7cd0312e064fde87c8d1cd79ba06c876bd23630c83466e9500321be55c96ace2", + "https://bcr.bazel.build/modules/abseil-cpp/20211102.0/MODULE.bazel": "70390338f7a5106231d20620712f7cccb659cd0e9d073d1991c038eb9fc57589", + "https://bcr.bazel.build/modules/abseil-cpp/20230125.1/MODULE.bazel": "89047429cb0207707b2dface14ba7f8df85273d484c2572755be4bab7ce9c3a0", + "https://bcr.bazel.build/modules/abseil-cpp/20230802.0.bcr.1/MODULE.bazel": "1c8cec495288dccd14fdae6e3f95f772c1c91857047a098fad772034264cc8cb", + "https://bcr.bazel.build/modules/abseil-cpp/20230802.0/MODULE.bazel": "d253ae36a8bd9ee3c5955384096ccb6baf16a1b1e93e858370da0a3b94f77c16", + "https://bcr.bazel.build/modules/abseil-cpp/20230802.1/MODULE.bazel": "fa92e2eb41a04df73cdabeec37107316f7e5272650f81d6cc096418fe647b915", + "https://bcr.bazel.build/modules/abseil-cpp/20240116.1/MODULE.bazel": "37bcdb4440fbb61df6a1c296ae01b327f19e9bb521f9b8e26ec854b6f97309ed", + "https://bcr.bazel.build/modules/abseil-cpp/20240116.2/MODULE.bazel": "73939767a4686cd9a520d16af5ab440071ed75cec1a876bf2fcfaf1f71987a16", + "https://bcr.bazel.build/modules/abseil-cpp/20250127.1/MODULE.bazel": "c4a89e7ceb9bf1e25cf84a9f830ff6b817b72874088bf5141b314726e46a57c1", + "https://bcr.bazel.build/modules/abseil-cpp/20250512.1/MODULE.bazel": "d209fdb6f36ffaf61c509fcc81b19e81b411a999a934a032e10cd009a0226215", + "https://bcr.bazel.build/modules/abseil-cpp/20250814.1/MODULE.bazel": "51f2312901470cdab0dbdf3b88c40cd21c62a7ed58a3de45b365ddc5b11bcab2", + "https://bcr.bazel.build/modules/abseil-cpp/20250814.1/source.json": "cea3901d7e299da7320700abbaafe57a65d039f10d0d7ea601c4a66938ea4b0c", + "https://bcr.bazel.build/modules/apple_support/1.11.1/MODULE.bazel": "1843d7cd8a58369a444fc6000e7304425fba600ff641592161d9f15b179fb896", + "https://bcr.bazel.build/modules/apple_support/1.15.1/MODULE.bazel": "a0556fefca0b1bb2de8567b8827518f94db6a6e7e7d632b4c48dc5f865bc7c85", + "https://bcr.bazel.build/modules/apple_support/1.21.0/MODULE.bazel": "ac1824ed5edf17dee2fdd4927ada30c9f8c3b520be1b5fd02a5da15bc10bff3e", + "https://bcr.bazel.build/modules/apple_support/1.21.1/MODULE.bazel": "5809fa3efab15d1f3c3c635af6974044bac8a4919c62238cce06acee8a8c11f1", + "https://bcr.bazel.build/modules/apple_support/1.24.2/MODULE.bazel": "0e62471818affb9f0b26f128831d5c40b074d32e6dda5a0d3852847215a41ca4", + "https://bcr.bazel.build/modules/apple_support/1.24.2/source.json": "2c22c9827093250406c5568da6c54e6fdf0ef06238def3d99c71b12feb057a8d", + "https://bcr.bazel.build/modules/bazel_features/1.10.0/MODULE.bazel": "f75e8807570484a99be90abcd52b5e1f390362c258bcb73106f4544957a48101", + "https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8", + "https://bcr.bazel.build/modules/bazel_features/1.15.0/MODULE.bazel": "d38ff6e517149dc509406aca0db3ad1efdd890a85e049585b7234d04238e2a4d", + "https://bcr.bazel.build/modules/bazel_features/1.17.0/MODULE.bazel": "039de32d21b816b47bd42c778e0454217e9c9caac4a3cf8e15c7231ee3ddee4d", + "https://bcr.bazel.build/modules/bazel_features/1.18.0/MODULE.bazel": "1be0ae2557ab3a72a57aeb31b29be347bcdc5d2b1eb1e70f39e3851a7e97041a", + "https://bcr.bazel.build/modules/bazel_features/1.19.0/MODULE.bazel": "59adcdf28230d220f0067b1f435b8537dd033bfff8db21335ef9217919c7fb58", + "https://bcr.bazel.build/modules/bazel_features/1.21.0/MODULE.bazel": "675642261665d8eea09989aa3b8afb5c37627f1be178382c320d1b46afba5e3b", + "https://bcr.bazel.build/modules/bazel_features/1.23.0/MODULE.bazel": "fd1ac84bc4e97a5a0816b7fd7d4d4f6d837b0047cf4cbd81652d616af3a6591a", + "https://bcr.bazel.build/modules/bazel_features/1.27.0/MODULE.bazel": "621eeee06c4458a9121d1f104efb80f39d34deff4984e778359c60eaf1a8cb65", + "https://bcr.bazel.build/modules/bazel_features/1.28.0/MODULE.bazel": "4b4200e6cbf8fa335b2c3f43e1d6ef3e240319c33d43d60cc0fbd4b87ece299d", + "https://bcr.bazel.build/modules/bazel_features/1.3.0/MODULE.bazel": "cdcafe83ec318cda34e02948e81d790aab8df7a929cec6f6969f13a489ccecd9", + "https://bcr.bazel.build/modules/bazel_features/1.30.0/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87", + "https://bcr.bazel.build/modules/bazel_features/1.33.0/MODULE.bazel": "8b8dc9d2a4c88609409c3191165bccec0e4cb044cd7a72ccbe826583303459f6", + "https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7", + "https://bcr.bazel.build/modules/bazel_features/1.42.1/MODULE.bazel": "275a59b5406ff18c01739860aa70ad7ccb3cfb474579411decca11c93b951080", + "https://bcr.bazel.build/modules/bazel_features/1.42.1/source.json": "fcd4396b2df85f64f2b3bb436ad870793ecf39180f1d796f913cc9276d355309", + "https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8", + "https://bcr.bazel.build/modules/bazel_skylib/1.1.1/MODULE.bazel": "1add3e7d93ff2e6998f9e118022c84d163917d912f5afafb3058e3d2f1545b5e", + "https://bcr.bazel.build/modules/bazel_skylib/1.2.0/MODULE.bazel": "44fe84260e454ed94ad326352a698422dbe372b21a1ac9f3eab76eb531223686", + "https://bcr.bazel.build/modules/bazel_skylib/1.2.1/MODULE.bazel": "f35baf9da0efe45fa3da1696ae906eea3d615ad41e2e3def4aeb4e8bc0ef9a7a", + "https://bcr.bazel.build/modules/bazel_skylib/1.3.0/MODULE.bazel": "20228b92868bf5cfc41bda7afc8a8ba2a543201851de39d990ec957b513579c5", + "https://bcr.bazel.build/modules/bazel_skylib/1.4.1/MODULE.bazel": "a0dcb779424be33100dcae821e9e27e4f2901d9dfd5333efe5ac6a8d7ab75e1d", + "https://bcr.bazel.build/modules/bazel_skylib/1.4.2/MODULE.bazel": "3bd40978e7a1fac911d5989e6b09d8f64921865a45822d8b09e815eaa726a651", + "https://bcr.bazel.build/modules/bazel_skylib/1.5.0/MODULE.bazel": "32880f5e2945ce6a03d1fbd588e9198c0a959bb42297b2cfaf1685b7bc32e138", + "https://bcr.bazel.build/modules/bazel_skylib/1.6.1/MODULE.bazel": "8fdee2dbaace6c252131c00e1de4b165dc65af02ea278476187765e1a617b917", + "https://bcr.bazel.build/modules/bazel_skylib/1.7.0/MODULE.bazel": "0db596f4563de7938de764cc8deeabec291f55e8ec15299718b93c4423e9796d", + "https://bcr.bazel.build/modules/bazel_skylib/1.7.1/MODULE.bazel": "3120d80c5861aa616222ec015332e5f8d3171e062e3e804a2a0253e1be26e59b", + "https://bcr.bazel.build/modules/bazel_skylib/1.8.1/MODULE.bazel": "88ade7293becda963e0e3ea33e7d54d3425127e0a326e0d17da085a5f1f03ff6", + "https://bcr.bazel.build/modules/bazel_skylib/1.8.2/MODULE.bazel": "69ad6927098316848b34a9142bcc975e018ba27f08c4ff403f50c1b6e646ca67", + "https://bcr.bazel.build/modules/bazel_skylib/1.8.2/source.json": "34a3c8bcf233b835eb74be9d628899bb32999d3e0eadef1947a0a562a2b16ffb", + "https://bcr.bazel.build/modules/buildozer/8.5.1/MODULE.bazel": "a35d9561b3fc5b18797c330793e99e3b834a473d5fbd3d7d7634aafc9bdb6f8f", + "https://bcr.bazel.build/modules/buildozer/8.5.1/source.json": "e3386e6ff4529f2442800dee47ad28d3e6487f36a1f75ae39ae56c70f0cd2fbd", + "https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb", + "https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4", + "https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/MODULE.bazel": "22c31a561553727960057361aa33bf20fb2e98584bc4fec007906e27053f80c6", + "https://bcr.bazel.build/modules/googletest/1.14.0/MODULE.bazel": "cfbcbf3e6eac06ef9d85900f64424708cc08687d1b527f0ef65aa7517af8118f", + "https://bcr.bazel.build/modules/googletest/1.15.2/MODULE.bazel": "6de1edc1d26cafb0ea1a6ab3f4d4192d91a312fd2d360b63adaa213cd00b2108", + "https://bcr.bazel.build/modules/googletest/1.17.0/MODULE.bazel": "dbec758171594a705933a29fcf69293d2468c49ec1f2ebca65c36f504d72df46", + "https://bcr.bazel.build/modules/googletest/1.17.0/source.json": "38e4454b25fc30f15439c0378e57909ab1fd0a443158aa35aec685da727cd713", + "https://bcr.bazel.build/modules/jsoncpp/1.9.5/MODULE.bazel": "31271aedc59e815656f5736f282bb7509a97c7ecb43e927ac1a37966e0578075", + "https://bcr.bazel.build/modules/jsoncpp/1.9.6/MODULE.bazel": "2f8d20d3b7d54143213c4dfc3d98225c42de7d666011528dc8fe91591e2e17b0", + "https://bcr.bazel.build/modules/jsoncpp/1.9.6/source.json": "a04756d367a2126c3541682864ecec52f92cdee80a35735a3cb249ce015ca000", + "https://bcr.bazel.build/modules/libpfm/4.11.0/MODULE.bazel": "45061ff025b301940f1e30d2c16bea596c25b176c8b6b3087e92615adbd52902", + "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/MODULE.bazel": "6f7b417dcc794d9add9e556673ad25cb3ba835224290f4f848f8e2db1e1fca74", + "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/source.json": "f448c6e8963fdfa7eb831457df83ad63d3d6355018f6574fb017e8169deb43a9", + "https://bcr.bazel.build/modules/package_metadata/0.0.7/MODULE.bazel": "7adb03933fc8401f495800cf4eafcff0edc6da0ff55c7db223ef69d19f689486", + "https://bcr.bazel.build/modules/package_metadata/0.0.7/source.json": "50639625e937b56115012674c797cca7a05a96b4878c87d803c13dc2b31de8a0", + "https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5", + "https://bcr.bazel.build/modules/platforms/0.0.11/MODULE.bazel": "0daefc49732e227caa8bfa834d65dc52e8cc18a2faf80df25e8caea151a9413f", + "https://bcr.bazel.build/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee", + "https://bcr.bazel.build/modules/platforms/0.0.5/MODULE.bazel": "5733b54ea419d5eaf7997054bb55f6a1d0b5ff8aedf0176fef9eea44f3acda37", + "https://bcr.bazel.build/modules/platforms/0.0.6/MODULE.bazel": "ad6eeef431dc52aefd2d77ed20a4b353f8ebf0f4ecdd26a807d2da5aa8cd0615", + "https://bcr.bazel.build/modules/platforms/0.0.7/MODULE.bazel": "72fd4a0ede9ee5c021f6a8dd92b503e089f46c227ba2813ff183b71616034814", + "https://bcr.bazel.build/modules/platforms/0.0.8/MODULE.bazel": "9f142c03e348f6d263719f5074b21ef3adf0b139ee4c5133e2aa35664da9eb2d", + "https://bcr.bazel.build/modules/platforms/0.0.9/MODULE.bazel": "4a87a60c927b56ddd67db50c89acaa62f4ce2a1d2149ccb63ffd871d5ce29ebc", + "https://bcr.bazel.build/modules/platforms/1.0.0/MODULE.bazel": "f05feb42b48f1b3c225e4ccf351f367be0371411a803198ec34a389fb22aa580", + "https://bcr.bazel.build/modules/platforms/1.0.0/source.json": "f4ff1fd412e0246fd38c82328eb209130ead81d62dcd5a9e40910f867f733d96", + "https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel": "a5a29bb89544f9b97edce05642fac225a808b5b7be74038ea3640fae2f8e66a7", + "https://bcr.bazel.build/modules/protobuf/27.0/MODULE.bazel": "7873b60be88844a0a1d8f80b9d5d20cfbd8495a689b8763e76c6372998d3f64c", + "https://bcr.bazel.build/modules/protobuf/29.0-rc2/MODULE.bazel": "6241d35983510143049943fc0d57937937122baf1b287862f9dc8590fc4c37df", + "https://bcr.bazel.build/modules/protobuf/29.1/MODULE.bazel": "557c3457560ff49e122ed76c0bc3397a64af9574691cb8201b4e46d4ab2ecb95", + "https://bcr.bazel.build/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0", + "https://bcr.bazel.build/modules/protobuf/32.1/MODULE.bazel": "89cd2866a9cb07fee9ff74c41ceace11554f32e0d849de4e23ac55515cfada4d", + "https://bcr.bazel.build/modules/protobuf/33.4/MODULE.bazel": "114775b816b38b6d0ca620450d6b02550c60ceedfdc8d9a229833b34a223dc42", + "https://bcr.bazel.build/modules/protobuf/33.4/source.json": "555f8686b4c7d6b5ba731fbea13bf656b4bfd9a7ff629c1d9d3f6e1d6155de79", + "https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/MODULE.bazel": "88af1c246226d87e65be78ed49ecd1e6f5e98648558c14ce99176da041dc378e", + "https://bcr.bazel.build/modules/pybind11_bazel/2.12.0/MODULE.bazel": "e6f4c20442eaa7c90d7190d8dc539d0ab422f95c65a57cc59562170c58ae3d34", + "https://bcr.bazel.build/modules/pybind11_bazel/2.12.0/source.json": "6900fdc8a9e95866b8c0d4ad4aba4d4236317b5c1cd04c502df3f0d33afed680", + "https://bcr.bazel.build/modules/re2/2023-09-01/MODULE.bazel": "cb3d511531b16cfc78a225a9e2136007a48cf8a677e4264baeab57fe78a80206", + "https://bcr.bazel.build/modules/re2/2024-07-02.bcr.1/MODULE.bazel": "b4963dda9b31080be1905ef085ecd7dd6cd47c05c79b9cdf83ade83ab2ab271a", + "https://bcr.bazel.build/modules/re2/2024-07-02.bcr.1/source.json": "2ff292be6ef3340325ce8a045ecc326e92cbfab47c7cbab4bd85d28971b97ac4", + "https://bcr.bazel.build/modules/re2/2024-07-02/MODULE.bazel": "0eadc4395959969297cbcf31a249ff457f2f1d456228c67719480205aa306daa", + "https://bcr.bazel.build/modules/rules_android/0.1.1/MODULE.bazel": "48809ab0091b07ad0182defb787c4c5328bd3a278938415c00a7b69b50c4d3a8", + "https://bcr.bazel.build/modules/rules_android/0.1.1/source.json": "e6986b41626ee10bdc864937ffb6d6bf275bb5b9c65120e6137d56e6331f089e", + "https://bcr.bazel.build/modules/rules_apple/3.16.0/MODULE.bazel": "0d1caf0b8375942ce98ea944be754a18874041e4e0459401d925577624d3a54a", + "https://bcr.bazel.build/modules/rules_apple/4.1.0/MODULE.bazel": "76e10fd4a48038d3fc7c5dc6e63b7063bbf5304a2e3bd42edda6ec660eebea68", + "https://bcr.bazel.build/modules/rules_apple/4.1.0/source.json": "8ee81e1708756f81b343a5eb2b2f0b953f1d25c4ab3d4a68dc02754872e80715", + "https://bcr.bazel.build/modules/rules_cc/0.0.1/MODULE.bazel": "cb2aa0747f84c6c3a78dad4e2049c154f08ab9d166b1273835a8174940365647", + "https://bcr.bazel.build/modules/rules_cc/0.0.10/MODULE.bazel": "ec1705118f7eaedd6e118508d3d26deba2a4e76476ada7e0e3965211be012002", + "https://bcr.bazel.build/modules/rules_cc/0.0.13/MODULE.bazel": "0e8529ed7b323dad0775ff924d2ae5af7640b23553dfcd4d34344c7e7a867191", + "https://bcr.bazel.build/modules/rules_cc/0.0.15/MODULE.bazel": "6704c35f7b4a72502ee81f61bf88706b54f06b3cbe5558ac17e2e14666cd5dcc", + "https://bcr.bazel.build/modules/rules_cc/0.0.16/MODULE.bazel": "7661303b8fc1b4d7f532e54e9d6565771fea666fbdf839e0a86affcd02defe87", + "https://bcr.bazel.build/modules/rules_cc/0.0.17/MODULE.bazel": "2ae1d8f4238ec67d7185d8861cb0a2cdf4bc608697c331b95bf990e69b62e64a", + "https://bcr.bazel.build/modules/rules_cc/0.0.2/MODULE.bazel": "6915987c90970493ab97393024c156ea8fb9f3bea953b2f3ec05c34f19b5695c", + "https://bcr.bazel.build/modules/rules_cc/0.0.6/MODULE.bazel": "abf360251023dfe3efcef65ab9d56beefa8394d4176dd29529750e1c57eaa33f", + "https://bcr.bazel.build/modules/rules_cc/0.0.8/MODULE.bazel": "964c85c82cfeb6f3855e6a07054fdb159aced38e99a5eecf7bce9d53990afa3e", + "https://bcr.bazel.build/modules/rules_cc/0.0.9/MODULE.bazel": "836e76439f354b89afe6a911a7adf59a6b2518fafb174483ad78a2a2fde7b1c5", + "https://bcr.bazel.build/modules/rules_cc/0.1.1/MODULE.bazel": "2f0222a6f229f0bf44cd711dc13c858dad98c62d52bd51d8fc3a764a83125513", + "https://bcr.bazel.build/modules/rules_cc/0.1.2/MODULE.bazel": "557ddc3a96858ec0d465a87c0a931054d7dcfd6583af2c7ed3baf494407fd8d0", + "https://bcr.bazel.build/modules/rules_cc/0.2.0/MODULE.bazel": "b5c17f90458caae90d2ccd114c81970062946f49f355610ed89bebf954f5783c", + "https://bcr.bazel.build/modules/rules_cc/0.2.13/MODULE.bazel": "eecdd666eda6be16a8d9dc15e44b5c75133405e820f620a234acc4b1fdc5aa37", + "https://bcr.bazel.build/modules/rules_cc/0.2.17/MODULE.bazel": "1849602c86cb60da8613d2de887f9566a6d354a6df6d7009f9d04a14402f9a84", + "https://bcr.bazel.build/modules/rules_cc/0.2.17/source.json": "3832f45d145354049137c0090df04629d9c2b5493dc5c2bf46f1834040133a07", + "https://bcr.bazel.build/modules/rules_cc/0.2.8/MODULE.bazel": "f1df20f0bf22c28192a794f29b501ee2018fa37a3862a1a2132ae2940a23a642", + "https://bcr.bazel.build/modules/rules_foreign_cc/0.9.0/MODULE.bazel": "c9e8c682bf75b0e7c704166d79b599f93b72cfca5ad7477df596947891feeef6", + "https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/MODULE.bazel": "40c97d1144356f52905566c55811f13b299453a14ac7769dfba2ac38192337a8", + "https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74", + "https://bcr.bazel.build/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86", + "https://bcr.bazel.build/modules/rules_java/6.5.2/MODULE.bazel": "1d440d262d0e08453fa0c4d8f699ba81609ed0e9a9a0f02cd10b3e7942e61e31", + "https://bcr.bazel.build/modules/rules_java/7.10.0/MODULE.bazel": "530c3beb3067e870561739f1144329a21c851ff771cd752a49e06e3dc9c2e71a", + "https://bcr.bazel.build/modules/rules_java/7.12.2/MODULE.bazel": "579c505165ee757a4280ef83cda0150eea193eed3bef50b1004ba88b99da6de6", + "https://bcr.bazel.build/modules/rules_java/7.2.0/MODULE.bazel": "06c0334c9be61e6cef2c8c84a7800cef502063269a5af25ceb100b192453d4ab", + "https://bcr.bazel.build/modules/rules_java/7.6.1/MODULE.bazel": "2f14b7e8a1aa2f67ae92bc69d1ec0fa8d9f827c4e17ff5e5f02e91caa3b2d0fe", + "https://bcr.bazel.build/modules/rules_java/8.6.1/MODULE.bazel": "f4808e2ab5b0197f094cabce9f4b006a27766beb6a9975931da07099560ca9c2", + "https://bcr.bazel.build/modules/rules_java/9.1.0/MODULE.bazel": "ee63f27e36a3fada80342869361182f120a9819c74320e8e65b1e04ba0cd7a9d", + "https://bcr.bazel.build/modules/rules_java/9.1.0/source.json": "da589573c1dee2c9ac4a568b301269a2e8191110ff0345c1a959fa7ea6c4dfd6", + "https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7", + "https://bcr.bazel.build/modules/rules_jvm_external/5.1/MODULE.bazel": "33f6f999e03183f7d088c9be518a63467dfd0be94a11d0055fe2d210f89aa909", + "https://bcr.bazel.build/modules/rules_jvm_external/5.2/MODULE.bazel": "d9351ba35217ad0de03816ef3ed63f89d411349353077348a45348b096615036", + "https://bcr.bazel.build/modules/rules_jvm_external/6.3/MODULE.bazel": "c998e060b85f71e00de5ec552019347c8bca255062c990ac02d051bb80a38df0", + "https://bcr.bazel.build/modules/rules_jvm_external/6.7/MODULE.bazel": "e717beabc4d091ecb2c803c2d341b88590e9116b8bf7947915eeb33aab4f96dd", + "https://bcr.bazel.build/modules/rules_jvm_external/6.7/source.json": "5426f412d0a7fc6b611643376c7e4a82dec991491b9ce5cb1cfdd25fe2e92be4", + "https://bcr.bazel.build/modules/rules_kotlin/1.9.6/MODULE.bazel": "d269a01a18ee74d0335450b10f62c9ed81f2321d7958a2934e44272fe82dcef3", + "https://bcr.bazel.build/modules/rules_kotlin/1.9.6/source.json": "2faa4794364282db7c06600b7e5e34867a564ae91bda7cae7c29c64e9466b7d5", + "https://bcr.bazel.build/modules/rules_license/0.0.3/MODULE.bazel": "627e9ab0247f7d1e05736b59dbb1b6871373de5ad31c3011880b4133cafd4bd0", + "https://bcr.bazel.build/modules/rules_license/0.0.7/MODULE.bazel": "088fbeb0b6a419005b89cf93fe62d9517c0a2b8bb56af3244af65ecfe37e7d5d", + "https://bcr.bazel.build/modules/rules_license/1.0.0/MODULE.bazel": "a7fda60eefdf3d8c827262ba499957e4df06f659330bbe6cdbdb975b768bb65c", + "https://bcr.bazel.build/modules/rules_license/1.0.0/source.json": "a52c89e54cc311196e478f8382df91c15f7a2bfdf4c6cd0e2675cc2ff0b56efb", + "https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc", + "https://bcr.bazel.build/modules/rules_pkg/1.0.1/MODULE.bazel": "5b1df97dbc29623bccdf2b0dcd0f5cb08e2f2c9050aab1092fd39a41e82686ff", + "https://bcr.bazel.build/modules/rules_pkg/1.0.1/source.json": "bd82e5d7b9ce2d31e380dd9f50c111d678c3bdaca190cb76b0e1c71b05e1ba8a", + "https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06", + "https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7", + "https://bcr.bazel.build/modules/rules_proto/6.0.2/MODULE.bazel": "ce916b775a62b90b61888052a416ccdda405212b6aaeb39522f7dc53431a5e73", + "https://bcr.bazel.build/modules/rules_proto/7.1.0/MODULE.bazel": "002d62d9108f75bb807cd56245d45648f38275cb3a99dcd45dfb864c5d74cb96", + "https://bcr.bazel.build/modules/rules_proto/7.1.0/source.json": "39f89066c12c24097854e8f57ab8558929f9c8d474d34b2c00ac04630ad8940e", + "https://bcr.bazel.build/modules/rules_shell/0.2.0/MODULE.bazel": "fda8a652ab3c7d8fee214de05e7a9916d8b28082234e8d2c0094505c5268ed3c", + "https://bcr.bazel.build/modules/rules_shell/0.3.0/MODULE.bazel": "de4402cd12f4cc8fda2354fce179fdb068c0b9ca1ec2d2b17b3e21b24c1a937b", + "https://bcr.bazel.build/modules/rules_shell/0.6.1/MODULE.bazel": "72e76b0eea4e81611ef5452aa82b3da34caca0c8b7b5c0c9584338aa93bae26b", + "https://bcr.bazel.build/modules/rules_shell/0.6.1/source.json": "20ec05cd5e592055e214b2da8ccb283c7f2a421ea0dc2acbf1aa792e11c03d0c", + "https://bcr.bazel.build/modules/rules_swift/1.16.0/MODULE.bazel": "4a09f199545a60d09895e8281362b1ff3bb08bbde69c6fc87aff5b92fcc916ca", + "https://bcr.bazel.build/modules/rules_swift/2.1.1/MODULE.bazel": "494900a80f944fc7aa61500c2073d9729dff0b764f0e89b824eb746959bc1046", + "https://bcr.bazel.build/modules/rules_swift/2.4.0/MODULE.bazel": "1639617eb1ede28d774d967a738b4a68b0accb40650beadb57c21846beab5efd", + "https://bcr.bazel.build/modules/rules_swift/3.1.2/MODULE.bazel": "72c8f5cf9d26427cee6c76c8e3853eb46ce6b0412a081b2b6db6e8ad56267400", + "https://bcr.bazel.build/modules/rules_swift/3.1.2/source.json": "e85761f3098a6faf40b8187695e3de6d97944e98abd0d8ce579cb2daf6319a66", + "https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8", + "https://bcr.bazel.build/modules/stardoc/0.5.3/MODULE.bazel": "c7f6948dae6999bf0db32c1858ae345f112cacf98f174c7a8bb707e41b974f1c", + "https://bcr.bazel.build/modules/stardoc/0.7.0/MODULE.bazel": "05e3d6d30c099b6770e97da986c53bd31844d7f13d41412480ea265ac9e8079c", + "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/MODULE.bazel": "5e463fbfba7b1701d957555ed45097d7f984211330106ccd1352c6e0af0dcf91", + "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.2/MODULE.bazel": "75aab2373a4bbe2a1260b9bf2a1ebbdbf872d3bd36f80bff058dccd82e89422f", + "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.2/source.json": "5fba48bbe0ba48761f9e9f75f92876cafb5d07c0ce059cc7a8027416de94a05b", + "https://bcr.bazel.build/modules/toml.bzl/0.4.1/MODULE.bazel": "6bc0b938f03ade8d58c2fca0ad5c3fa12b4764e1e1927ad50b0c860286db2167", + "https://bcr.bazel.build/modules/toml.bzl/0.4.1/source.json": "86a90afd8b43c9b69ad31f5c03998c3adcf4b08175e621addb93e3a38eec538b", + "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43", + "https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0", + "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/MODULE.bazel": "eec517b5bbe5492629466e11dae908d043364302283de25581e3eb944326c4ca", + "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/source.json": "22bc55c47af97246cfc093d0acf683a7869377de362b5d1c552c2c2e16b7a806", + "https://bcr.bazel.build/modules/zlib/1.3.1/MODULE.bazel": "751c9940dcfe869f5f7274e1295422a34623555916eb98c174c1e945594bf198" + }, + "selectedYankedVersions": {}, + "moduleExtensions": { + "@@pybind11_bazel+//:internal_configure.bzl%internal_configure_extension": { + "general": { + "bzlTransitiveDigest": "b+RP7Sgl8KN0VHamrgTqzGLuYPcQ/Mo4ptNkkHUIIlA=", + "usagesDigest": "D1r3lfzMuUBFxgG8V6o0bQTLMk3GkaGOaPzw53wrwyw=", + "recordedInputs": [ + "REPO_MAPPING:pybind11_bazel+,bazel_tools bazel_tools", + "FILE:@@pybind11_bazel+//MODULE.bazel e6f4c20442eaa7c90d7190d8dc539d0ab422f95c65a57cc59562170c58ae3d34" + ], + "generatedRepoSpecs": { + "pybind11": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "build_file": "@@pybind11_bazel+//:pybind11-BUILD.bazel", + "strip_prefix": "pybind11-2.12.0", + "urls": [ + "https://github.com/pybind/pybind11/archive/v2.12.0.zip" + ] + } + } + } + } + }, + "@@rules_kotlin+//src/main/starlark/core/repositories:bzlmod_setup.bzl%rules_kotlin_extensions": { + "general": { + "bzlTransitiveDigest": "Ga4z8lQy1YQ5rAMy+dOl0dqcCEBnYNCXku8x3YQmDZI=", + "usagesDigest": "QI2z8ZUR+mqtbwsf2fLqYdJAkPOHdOV+tF2yVAUgRzw=", + "recordedInputs": [ + "REPO_MAPPING:rules_kotlin+,bazel_tools bazel_tools" + ], + "generatedRepoSpecs": { + "com_github_jetbrains_kotlin_git": { + "repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:compiler.bzl%kotlin_compiler_git_repository", + "attributes": { + "urls": [ + "https://github.com/JetBrains/kotlin/releases/download/v1.9.23/kotlin-compiler-1.9.23.zip" + ], + "sha256": "93137d3aab9afa9b27cb06a824c2324195c6b6f6179d8a8653f440f5bd58be88" + } + }, + "com_github_jetbrains_kotlin": { + "repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:compiler.bzl%kotlin_capabilities_repository", + "attributes": { + "git_repository_name": "com_github_jetbrains_kotlin_git", + "compiler_version": "1.9.23" + } + }, + "com_github_google_ksp": { + "repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:ksp.bzl%ksp_compiler_plugin_repository", + "attributes": { + "urls": [ + "https://github.com/google/ksp/releases/download/1.9.23-1.0.20/artifacts.zip" + ], + "sha256": "ee0618755913ef7fd6511288a232e8fad24838b9af6ea73972a76e81053c8c2d", + "strip_version": "1.9.23-1.0.20" + } + }, + "com_github_pinterest_ktlint": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_file", + "attributes": { + "sha256": "01b2e0ef893383a50dbeb13970fe7fa3be36ca3e83259e01649945b09d736985", + "urls": [ + "https://github.com/pinterest/ktlint/releases/download/1.3.0/ktlint" + ], + "executable": true + } + }, + "rules_android": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "sha256": "cd06d15dd8bb59926e4d65f9003bfc20f9da4b2519985c27e190cddc8b7a7806", + "strip_prefix": "rules_android-0.1.1", + "urls": [ + "https://github.com/bazelbuild/rules_android/archive/v0.1.1.zip" + ] + } + } + } + } + }, + "@@rules_python+//python/uv:uv.bzl%uv": { + "general": { + "bzlTransitiveDigest": "OC4ZhWl8jX9wvFicn83AioC9J7Dx6Us2/+EzoGDlPzU=", + "usagesDigest": "6yXGw7XDyXjOfqBL0SBu1YBEMMYPQzCE3jTzUCkxPgg=", + "recordedInputs": [ + "REPO_MAPPING:rules_python+,bazel_tools bazel_tools", + "REPO_MAPPING:rules_python+,platforms platforms" + ], + "generatedRepoSpecs": { + "uv": { + "repoRuleId": "@@rules_python+//python/uv/private:uv_toolchains_repo.bzl%uv_toolchains_repo", + "attributes": { + "toolchain_type": "'@@rules_python+//python/uv:uv_toolchain_type'", + "toolchain_names": [ + "none" + ], + "toolchain_implementations": { + "none": "'@@rules_python+//python:none'" + }, + "toolchain_compatible_with": { + "none": [ + "@platforms//:incompatible" + ] + }, + "toolchain_target_settings": {} + } + } + } + } + } + }, + "facts": { + "@@rules_python+//python/extensions:pip.bzl%pip": { + "dist_hashes": { + "https://pypi.org/simple": { + "backports-tarfile": { + "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz": "d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", + "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl": "77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34" + }, + "certifi": { + "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz": "47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", + "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl": "0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de" + }, + "cffi": { + "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", + "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl": "3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", + "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl": "5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", + "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl": "b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", + "https://files.pythonhosted.org/packages/15/12/a7a79bd0df4c3bff744b2d7e52cc1b68d5e7e427b384252c42366dc1ecbc/cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165", + "https://files.pythonhosted.org/packages/1f/74/cc4096ce66f5939042ae094e2e96f53426a979864aa1f96a621ad128be27/cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63", + "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl": "cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", + "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl": "7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", + "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl": "737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", + "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl": "c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", + "https://files.pythonhosted.org/packages/2b/e7/7c769804eb75e4c4b35e658dba01de1640a351a9653c3d49ca89d16ccc91/cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl": "89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322", + "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl": "7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", + "https://files.pythonhosted.org/packages/32/f2/81b63e288295928739d715d00952c8c6034cb6c6a516b17d37e0c8be5600/cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl": "cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f", + "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl": "b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", + "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl": "6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", + "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl": "19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", + "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl": "81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", + "https://files.pythonhosted.org/packages/3d/de/38d9726324e127f727b4ecc376bc85e505bfe61ef130eaf3f290c6847dd4/cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl": "de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7", + "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl": "9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", + "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl": "087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", + "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl": "a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", + "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", + "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl": "8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", + "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl": "45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", + "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl": "00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", + "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", + "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl": "2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", + "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl": "53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", + "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl": "d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", + "https://files.pythonhosted.org/packages/54/8f/a1e836f82d8e32a97e6b29cc8f641779181ac7363734f12df27db803ebda/cffi-2.0.0-cp39-cp39-win_amd64.whl": "b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9", + "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl": "c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", + "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl": "e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", + "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", + "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl": "da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", + "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl": "9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", + "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl": "fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", + "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl": "0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", + "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl": "4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", + "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl": "c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", + "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", + "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", + "https://files.pythonhosted.org/packages/9b/13/c92e36358fbcc39cf0962e83223c9522154ee8630e1df7c0b3a39a8124e2/cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl": "4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c", + "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl": "f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", + "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl": "dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", + "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl": "9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", + "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl": "1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", + "https://files.pythonhosted.org/packages/a3/ad/5c51c1c7600bdd7ed9a24a203ec255dccdd0ebf4527f7b922a0bde2fb6ed/cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534", + "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", + "https://files.pythonhosted.org/packages/aa/d9/6218d78f920dcd7507fc16a766b5ef8f3b913cc7aa938e7fc80b9978d089/cffi-2.0.0-cp39-cp39-win32.whl": "2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a", + "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl": "94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", + "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl": "0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", + "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl": "66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", + "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl": "07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", + "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl": "baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", + "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", + "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl": "2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", + "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", + "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl": "203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", + "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl": "d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", + "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", + "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl": "d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", + "https://files.pythonhosted.org/packages/c0/cc/08ed5a43f2996a16b462f64a7055c6e962803534924b9b2f1371d8c00b7b/cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl": "fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf", + "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", + "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl": "38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", + "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl": "256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", + "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl": "dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", + "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", + "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", + "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", + "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", + "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl": "b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", + "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", + "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl": "8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", + "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", + "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl": "6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", + "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl": "1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", + "https://files.pythonhosted.org/packages/e8/be/f6424d1dc46b1091ffcc8964fa7c0ab0cd36839dd2761b49c90481a6ba1b/cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl": "0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2", + "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl": "6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", + "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz": "44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", + "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl": "74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", + "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", + "https://files.pythonhosted.org/packages/f7/e0/dda537c2309817edf60109e39265f24f24aa7f050767e22c98c53fe7f48b/cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl": "1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65", + "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl": "da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", + "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl": "21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe" + }, + "charset-normalizer": { + "https://files.pythonhosted.org/packages/00/bd/ef9c88464b126fa176f4ef4a317ad9b6f4d30b2cffbc43386062367c3e2c/charset_normalizer-3.4.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9", + "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl": "ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", + "https://files.pythonhosted.org/packages/04/9a/914d294daa4809c57667b77470533e65def9c0be1ef8b4c1183a99170e9d/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7", + "https://files.pythonhosted.org/packages/05/35/bb59b1cd012d7196fc81c2f5879113971efc226a63812c9cf7f89fe97c40/charset_normalizer-3.4.3-cp38-cp38-win_amd64.whl": "5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40", + "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl": "42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", + "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl": "30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", + "https://files.pythonhosted.org/packages/0c/52/8b0c6c3e53f7e546a5e49b9edb876f379725914e1130297f3b423c7b71c5/charset_normalizer-3.4.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b", + "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", + "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", + "https://files.pythonhosted.org/packages/1a/79/ae516e678d6e32df2e7e740a7be51dc80b700e2697cb70054a0f1ac2c955/charset_normalizer-3.4.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b", + "https://files.pythonhosted.org/packages/20/30/5f64fe3981677fe63fa987b80e6c01042eb5ff653ff7cec1b7bd9268e54e/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl": "2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312", + "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", + "https://files.pythonhosted.org/packages/22/82/63a45bfc36f73efe46731a3a71cb84e2112f7e0b049507025ce477f0f052/charset_normalizer-3.4.3-cp38-cp38-macosx_10_9_universal2.whl": "0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c", + "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl": "d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", + "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl": "1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", + "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl": "cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", + "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl": "78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", + "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", + "https://files.pythonhosted.org/packages/39/c6/99271dc37243a4f925b09090493fb96c9333d7992c6187f5cfe5312008d2/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e", + "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl": "86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", + "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", + "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl": "4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", + "https://files.pythonhosted.org/packages/45/8c/dcef87cfc2b3f002a6478f38906f9040302c68aebe21468090e39cde1445/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl": "88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34", + "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl": "939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", + "https://files.pythonhosted.org/packages/50/10/c117806094d2c956ba88958dab680574019abc0c02bcf57b32287afca544/charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_x86_64.whl": "a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b", + "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl": "fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", + "https://files.pythonhosted.org/packages/59/c0/a74f3bd167d311365e7973990243f32c35e7a94e45103125275b9e6c479f/charset_normalizer-3.4.3-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4", + "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", + "https://files.pythonhosted.org/packages/61/c5/dc3ba772489c453621ffc27e8978a98fe7e41a93e787e5e5bde797f1dddb/charset_normalizer-3.4.3-cp38-cp38-win32.whl": "ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557", + "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl": "96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", + "https://files.pythonhosted.org/packages/63/86/9cbd533bd37883d467fcd1bd491b3547a3532d0fbb46de2b99feeebf185e/charset_normalizer-3.4.3-cp39-cp39-win32.whl": "16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432", + "https://files.pythonhosted.org/packages/64/d1/f9d141c893ef5d4243bc75c130e95af8fd4bc355beff06e9b1e941daad6e/charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_ppc64le.whl": "5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a", + "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl": "6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", + "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", + "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl": "14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", + "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl": "18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", + "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", + "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl": "d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", + "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl": "fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", + "https://files.pythonhosted.org/packages/7a/03/cbb6fac9d3e57f7e07ce062712ee80d80a5ab46614684078461917426279/charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_aarch64.whl": "d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb", + "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", + "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl": "53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", + "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", + "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl": "b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", + "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", + "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz": "6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", + "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", + "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", + "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", + "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl": "ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", + "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl": "3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", + "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl": "fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", + "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl": "cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", + "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", + "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl": "6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", + "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl": "02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", + "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl": "027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", + "https://files.pythonhosted.org/packages/b0/a8/6f5bcf1bcf63cb45625f7c5cadca026121ff8a6c8a3256d8d8cd59302663/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7", + "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl": "c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", + "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl": "320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", + "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl": "6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", + "https://files.pythonhosted.org/packages/c2/ca/9a0983dd5c8e9733565cf3db4df2b0a2e9a82659fd8aa2a868ac6e4a991f/charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl": "70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05", + "https://files.pythonhosted.org/packages/c4/72/d3d0e9592f4e504f9dea08b8db270821c909558c353dc3b457ed2509f2fb/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl": "1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19", + "https://files.pythonhosted.org/packages/c5/35/9c99739250742375167bc1b1319cd1cec2bf67438a70d84b2e1ec4c9daa3/charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_s390x.whl": "b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942", + "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", + "https://files.pythonhosted.org/packages/ce/d6/7e805c8e5c46ff9729c49950acc4ee0aeb55efb8b3a56687658ad10c3216/charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl": "d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca", + "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", + "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl": "fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", + "https://files.pythonhosted.org/packages/e1/ef/dd08b2cac9284fd59e70f7d97382c33a3d0a926e45b15fc21b3308324ffd/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl": "511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc", + "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl": "c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", + "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", + "https://files.pythonhosted.org/packages/e4/69/132eab043356bba06eb333cc2cc60c6340857d0a2e4ca6dc2b51312886b3/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99", + "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl": "e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", + "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl": "73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", + "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl": "c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", + "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", + "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl": "31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", + "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl": "bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", + "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018" + }, + "cryptography": { + "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", + "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl": "ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", + "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl": "cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", + "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl": "5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", + "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl": "cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", + "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl": "d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", + "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl": "258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", + "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl": "fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", + "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl": "397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", + "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl": "420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", + "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl": "e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", + "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl": "8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", + "https://files.pythonhosted.org/packages/3a/ea/075aac6a84b7c271578d81a2f9968acb6e273002408729f2ddff517fed4a/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl": "d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", + "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl": "60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", + "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl": "b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", + "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl": "c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", + "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl": "a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", + "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz": "e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", + "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", + "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl": "f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", + "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl": "fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", + "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl": "65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", + "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", + "https://files.pythonhosted.org/packages/63/0c/dca8abb64e7ca4f6b2978769f6fea5ad06686a190cec381f0a796fdcaaba/cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl": "fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", + "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl": "9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", + "https://files.pythonhosted.org/packages/6c/7b/1c55db7242b5e5612b29fc7a630e91ee7a6e3c8e7bf5406d22e206875fbd/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl": "d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", + "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", + "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl": "d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", + "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl": "935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", + "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", + "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl": "73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", + "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl": "abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", + "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl": "fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", + "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl": "5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", + "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl": "d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", + "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", + "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl": "7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", + "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl": "462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", + "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl": "80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", + "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl": "cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", + "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl": "35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", + "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl": "ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", + "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl": "1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", + "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl": "42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", + "https://files.pythonhosted.org/packages/cb/da/9870eec4b69c63ef5925bf7d8342b7e13bc2ee3d47791461c4e49ca212f4/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl": "04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", + "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl": "91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", + "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl": "506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", + "https://files.pythonhosted.org/packages/f4/72/05aa5832b82dd341969e9a734d1812a6aadb088d9eb6f0430fc337cc5a8f/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl": "3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", + "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl": "24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de" + }, + "docutils": { + "https://files.pythonhosted.org/packages/4a/c0/89fe6215b443b919cb98a5002e107cb5026854ed1ccb6b5833e0768419d1/docutils-0.22.2.tar.gz": "9fdb771707c8784c8f2728b67cb2c691305933d68137ef95a75db5f4dfbc213d", + "https://files.pythonhosted.org/packages/66/dd/f95350e853a4468ec37478414fc04ae2d61dad7a947b3015c3dcc51a09b9/docutils-0.22.2-py3-none-any.whl": "b0e98d679283fc3bb0ead8a5da7f501baa632654e7056e9c5846842213d674d8" + }, + "idna": { + "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl": "946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", + "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz": "12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9" + }, + "importlib-metadata": { + "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl": "e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", + "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz": "d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000" + }, + "jaraco-classes": { + "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz": "47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", + "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl": "f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790" + }, + "jaraco-context": { + "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz": "9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", + "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl": "f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4" + }, + "jaraco-functools": { + "https://files.pythonhosted.org/packages/b4/09/726f168acad366b11e420df31bf1c702a54d373a83f968d94141a8c3fde0/jaraco_functools-4.3.0-py3-none-any.whl": "227ff8ed6f7b8f62c56deff101545fa7543cf2c8e7b82a7c2116e672f29c26e8", + "https://files.pythonhosted.org/packages/f7/ed/1aa2d585304ec07262e1a83a9889880701079dde796ac7b1d1826f40c63d/jaraco_functools-4.3.0.tar.gz": "cfd13ad0dd2c47a3600b439ef72d8615d482cedcff1632930d6f28924d92f294" + }, + "jeepney": { + "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz": "cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", + "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl": "97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683" + }, + "keyring": { + "https://files.pythonhosted.org/packages/70/09/d904a6e96f76ff214be59e7aa6ef7190008f52a0ab6689760a98de0bf37d/keyring-25.6.0.tar.gz": "0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", + "https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl": "552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd" + }, + "markdown-it-py": { + "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz": "cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", + "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl": "87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147" + }, + "mdurl": { + "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl": "84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", + "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz": "bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" + }, + "more-itertools": { + "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl": "52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", + "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz": "f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd" + }, + "nh3": { + "https://files.pythonhosted.org/packages/0c/e0/cf1543e798ba86d838952e8be4cb8d18e22999be2a24b112a671f1c04fd6/nh3-0.3.0-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl": "ec6cfdd2e0399cb79ba4dcffb2332b94d9696c52272ff9d48a630c5dca5e325a", + "https://files.pythonhosted.org/packages/10/71/2fb1834c10fab6d9291d62c95192ea2f4c7518bd32ad6c46aab5d095cb87/nh3-0.3.0-cp313-cp313t-musllinux_1_2_i686.whl": "0649464ac8eee018644aacbc103874ccbfac80e3035643c3acaab4287e36e7f5", + "https://files.pythonhosted.org/packages/23/1e/80a8c517655dd40bb13363fc4d9e66b2f13245763faab1a20f1df67165a7/nh3-0.3.0-cp313-cp313t-win_amd64.whl": "423201bbdf3164a9e09aa01e540adbb94c9962cc177d5b1cbb385f5e1e79216e", + "https://files.pythonhosted.org/packages/2f/d6/f1c6e091cbe8700401c736c2bc3980c46dca770a2cf6a3b48a175114058e/nh3-0.3.0-cp313-cp313t-win32.whl": "7275fdffaab10cc5801bf026e3c089d8de40a997afc9e41b981f7ac48c5aa7d5", + "https://files.pythonhosted.org/packages/33/c1/8f8ccc2492a000b6156dce68a43253fcff8b4ce70ab4216d08f90a2ac998/nh3-0.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl": "1adeb1062a1c2974bc75b8d1ecb014c5fd4daf2df646bbe2831f7c23659793f9", + "https://files.pythonhosted.org/packages/39/2c/6394301428b2017a9d5644af25f487fa557d06bc8a491769accec7524d9a/nh3-0.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl": "f416c35efee3e6a6c9ab7716d9e57aa0a49981be915963a82697952cba1353e1", + "https://files.pythonhosted.org/packages/4c/3c/cba7b26ccc0ef150c81646478aa32f9c9535234f54845603c838a1dc955c/nh3-0.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl": "80fe20171c6da69c7978ecba33b638e951b85fb92059259edd285ff108b82a6d", + "https://files.pythonhosted.org/packages/4e/9a/344b9f9c4bd1c2413a397f38ee6a3d5db30f1a507d4976e046226f12b297/nh3-0.3.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl": "37d3003d98dedca6cd762bf88f2e70b67f05100f6b949ffe540e189cc06887f9", + "https://files.pythonhosted.org/packages/5b/76/3165e84e5266d146d967a6cc784ff2fbf6ddd00985a55ec006b72bc39d5d/nh3-0.3.0-cp38-abi3-win_arm64.whl": "d97d3efd61404af7e5721a0e74d81cdbfc6e5f97e11e731bb6d090e30a7b62b2", + "https://files.pythonhosted.org/packages/5c/86/a96b1453c107b815f9ab8fac5412407c33cc5c7580a4daf57aabeb41b774/nh3-0.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl": "ce5e7185599f89b0e391e2f29cc12dc2e206167380cea49b33beda4891be2fe1", + "https://files.pythonhosted.org/packages/63/da/c5fd472b700ba37d2df630a9e0d8cc156033551ceb8b4c49cc8a5f606b68/nh3-0.3.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl": "ba0caa8aa184196daa6e574d997a33867d6d10234018012d35f86d46024a2a95", + "https://files.pythonhosted.org/packages/66/3f/cd37f76c8ca277b02a84aa20d7bd60fbac85b4e2cbdae77cb759b22de58b/nh3-0.3.0-cp38-abi3-musllinux_1_2_aarch64.whl": "634e34e6162e0408e14fb61d5e69dbaea32f59e847cfcfa41b66100a6b796f62", + "https://files.pythonhosted.org/packages/6a/1b/b15bd1ce201a1a610aeb44afd478d55ac018b4475920a3118ffd806e2483/nh3-0.3.0-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl": "e9e6a7e4d38f7e8dda9edd1433af5170c597336c1a74b4693c5cb75ab2b30f2a", + "https://files.pythonhosted.org/packages/8c/ae/324b165d904dc1672eee5f5661c0a68d4bab5b59fbb07afb6d8d19a30b45/nh3-0.3.0-cp38-abi3-win_amd64.whl": "bae63772408fd63ad836ec569a7c8f444dd32863d0c67f6e0b25ebbd606afa95", + "https://files.pythonhosted.org/packages/8f/14/079670fb2e848c4ba2476c5a7a2d1319826053f4f0368f61fca9bb4227ae/nh3-0.3.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl": "7852f038a054e0096dac12b8141191e02e93e0b4608c4b993ec7d4ffafea4e49", + "https://files.pythonhosted.org/packages/97/03/03f79f7e5178eb1ad5083af84faff471e866801beb980cc72943a4397368/nh3-0.3.0-cp38-abi3-musllinux_1_2_i686.whl": "c7a32a7f0d89f7d30cb8f4a84bdbd56d1eb88b78a2434534f62c71dac538c450", + "https://files.pythonhosted.org/packages/97/33/11e7273b663839626f714cb68f6eb49899da5a0d9b6bc47b41fe870259c2/nh3-0.3.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl": "389d93d59b8214d51c400fb5b07866c2a4f79e4e14b071ad66c92184fec3a392", + "https://files.pythonhosted.org/packages/9a/e0/af86d2a974c87a4ba7f19bc3b44a8eaa3da480de264138fec82fe17b340b/nh3-0.3.0-cp313-cp313t-win_arm64.whl": "16f8670201f7e8e0e05ed1a590eb84bfa51b01a69dd5caf1d3ea57733de6a52f", + "https://files.pythonhosted.org/packages/a3/e5/ac7fc565f5d8bce7f979d1afd68e8cb415020d62fa6507133281c7d49f91/nh3-0.3.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl": "af5aa8127f62bbf03d68f67a956627b1bd0469703a35b3dad28d0c1195e6c7fb", + "https://files.pythonhosted.org/packages/ad/7f/7c6b8358cf1222921747844ab0eef81129e9970b952fcb814df417159fb9/nh3-0.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl": "7c915060a2c8131bef6a29f78debc29ba40859b6dbe2362ef9e5fd44f11487c2", + "https://files.pythonhosted.org/packages/b4/11/340b7a551916a4b2b68c54799d710f86cf3838a4abaad8e74d35360343bb/nh3-0.3.0-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl": "a537ece1bf513e5a88d8cff8a872e12fe8d0f42ef71dd15a5e7520fecd191bbb", + "https://files.pythonhosted.org/packages/c3/a4/96cff0977357f60f06ec4368c4c7a7a26cccfe7c9fcd54f5378bf0428fd3/nh3-0.3.0.tar.gz": "d8ba24cb31525492ea71b6aac11a4adac91d828aadeff7c4586541bf5dc34d2f", + "https://files.pythonhosted.org/packages/c9/50/76936ec021fe1f3270c03278b8af5f2079038116b5d0bfe8538ffe699d69/nh3-0.3.0-cp38-abi3-win32.whl": "6d68fa277b4a3cf04e5c4b84dd0c6149ff7d56c12b3e3fab304c525b850f613d", + "https://files.pythonhosted.org/packages/ce/55/1974bcc16884a397ee699cebd3914e1f59be64ab305533347ca2d983756f/nh3-0.3.0-cp38-abi3-musllinux_1_2_x86_64.whl": "3f1b4f8a264a0c86ea01da0d0c390fe295ea0bcacc52c2103aca286f6884f518", + "https://files.pythonhosted.org/packages/ee/db/7aa11b44bae4e7474feb1201d8dee04fabe5651c7cb51409ebda94a4ed67/nh3-0.3.0-cp38-abi3-musllinux_1_2_armv7l.whl": "b0612ccf5de8a480cf08f047b08f9d3fecc12e63d2ee91769cb19d7290614c23", + "https://files.pythonhosted.org/packages/f3/ba/59e204d90727c25b253856e456ea61265ca810cda8ee802c35f3fadaab00/nh3-0.3.0-cp313-cp313t-musllinux_1_2_armv7l.whl": "e90883f9f85288f423c77b3f5a6f4486375636f25f793165112679a7b6363b35" + }, + "pkginfo": { + "https://files.pythonhosted.org/packages/24/03/e26bf3d6453b7fda5bd2b84029a426553bb373d6277ef6b5ac8863421f87/pkginfo-1.12.1.2.tar.gz": "5cd957824ac36f140260964eba3c6be6442a8359b8c48f4adf90210f33a04b7b", + "https://files.pythonhosted.org/packages/fa/3d/f4f2ba829efb54b6cd2d91349c7463316a9cc55a43fc980447416c88540f/pkginfo-1.12.1.2-py3-none-any.whl": "c783ac885519cab2c34927ccfa6bf64b5a704d7c69afaea583dd9b7afe969343" + }, + "pycparser": { + "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl": "b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", + "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz": "600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", + "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl": "e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", + "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz": "78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2" + }, + "pygments": { + "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz": "636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", + "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl": "86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b" + }, + "pywin32-ctypes": { + "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz": "d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", + "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl": "8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8" + }, + "readme-renderer": { + "https://files.pythonhosted.org/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz": "8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", + "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl": "2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151" + }, + "requests": { + "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz": "c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", + "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl": "3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b" + }, + "requests-toolbelt": { + "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl": "cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", + "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz": "7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6" + }, + "rfc3986": { + "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz": "97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", + "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl": "50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd" + }, + "rich": { + "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl": "536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", + "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz": "e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8" + }, + "secretstorage": { + "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz": "2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", + "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl": "f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99" + }, + "six": { + "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz": "ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", + "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl": "4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274" + }, + "twine": { + "https://files.pythonhosted.org/packages/5d/ec/00f9d5fd040ae29867355e559a94e9a8429225a0284a3f5f091a3878bfc0/twine-5.1.1-py3-none-any.whl": "215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997", + "https://files.pythonhosted.org/packages/77/68/bd982e5e949ef8334e6f7dcf76ae40922a8750aa2e347291ae1477a4782b/twine-5.1.1.tar.gz": "9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db" + }, + "urllib3": { + "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl": "bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", + "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz": "1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed" + }, + "zipp": { + "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl": "071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", + "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz": "a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166" + } + } + }, + "fact_version": "v1" + } + } +} diff --git a/tests/integration/bzlmod_lockfile/README.md b/tests/integration/bzlmod_lockfile/README.md new file mode 100644 index 0000000000..42c941c4dc --- /dev/null +++ b/tests/integration/bzlmod_lockfile/README.md @@ -0,0 +1,6 @@ +# Pip parse isolation and lock file test + +Update the lock file with the following command: +``` +bazel mod deps --lockfile_mode=update +``` diff --git a/tests/integration/bzlmod_lockfile/WORKSPACE b/tests/integration/bzlmod_lockfile/WORKSPACE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/bzlmod_lockfile/requirements_lock.txt b/tests/integration/bzlmod_lockfile/requirements_lock.txt new file mode 100644 index 0000000000..d8dcf2d44e --- /dev/null +++ b/tests/integration/bzlmod_lockfile/requirements_lock.txt @@ -0,0 +1,8 @@ +six==1.17.0 \ + --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 +# rules_python v2.0.0's own tools/publish/requirements_linux.txt pins pycparser==2.23. +# On macOS: lock gets pycparser-3.0 only (darwin publish requirements have no pycparser). +# On Linux: rules_python_publish_deps hub adds pycparser-2.23 to computed facts → mismatch. +pycparser==3.0 \ + --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ + --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 diff --git a/tests/integration/bzlmod_lockfile/test_dummy.py b/tests/integration/bzlmod_lockfile/test_dummy.py new file mode 100644 index 0000000000..110b1ba42f --- /dev/null +++ b/tests/integration/bzlmod_lockfile/test_dummy.py @@ -0,0 +1,17 @@ +""" +Verify that a dependency added using the pip extension can be imported. +See MODULE.bazel. +""" + +import unittest + +import six + + +class TestDummy(unittest.TestCase): + def test_import(self): + self.assertTrue(hasattr(six, "PY3")) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/integration/compile_pip_requirements/BUILD.bazel b/tests/integration/compile_pip_requirements/BUILD.bazel index 6df46b8372..b1f398c4a8 100644 --- a/tests/integration/compile_pip_requirements/BUILD.bazel +++ b/tests/integration/compile_pip_requirements/BUILD.bazel @@ -29,6 +29,19 @@ compile_pip_requirements( requirements_txt = "requirements_lock.txt", ) +genquery( + name = "requirements_update_data", + expression = """ +some(labels(data, @compile_pip_requirements//:requirements.update) intersect @compile_pip_requirements//:requirements.in) union +some(labels(data, @compile_pip_requirements//:requirements.update) intersect @compile_pip_requirements//:requirements_extra.in) +""", + scope = [ + "@compile_pip_requirements//:requirements.update", + "@compile_pip_requirements//:requirements.in", + "@compile_pip_requirements//:requirements_extra.in", + ], +) + compile_pip_requirements( name = "requirements_nohashes", src = "requirements.txt", diff --git a/tests/integration/compile_pip_requirements/WORKSPACE b/tests/integration/compile_pip_requirements/WORKSPACE index 0eeab2067c..397d828261 100644 --- a/tests/integration/compile_pip_requirements/WORKSPACE +++ b/tests/integration/compile_pip_requirements/WORKSPACE @@ -3,6 +3,11 @@ local_repository( path = "../../..", ) +local_repository( + name = "compile_pip_requirements", + path = ".", +) + load("@rules_python//python:repositories.bzl", "py_repositories", "python_register_toolchains") py_repositories() diff --git a/tests/integration/custom_commands_test.py b/tests/integration/custom_commands_test.py index 2e9cb741b0..336937ece0 100644 --- a/tests/integration/custom_commands_test.py +++ b/tests/integration/custom_commands_test.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import logging import unittest from tests.integration import runner @@ -21,7 +20,12 @@ class CustomCommandsTest(runner.TestCase): # Regression test for https://github.com/bazel-contrib/rules_python/issues/1840 def test_run_build_python_zip_false(self): - result = self.run_bazel("run", "--build_python_zip=false", "//:bin") + result = self.run_bazel( + "run", + "--build_python_zip=false", + "--@rules_python//python/config_settings:build_python_zip=false", + "//:bin", + ) self.assert_result_matches(result, "bazel-out") diff --git a/tests/integration/ignore_root_user_error/.bazelrc b/tests/integration/ignore_root_user_error/.bazelrc deleted file mode 100644 index bb7b5742cd..0000000000 --- a/tests/integration/ignore_root_user_error/.bazelrc +++ /dev/null @@ -1,7 +0,0 @@ -common --action_env=RULES_PYTHON_BZLMOD_DEBUG=1 -common --lockfile_mode=off -test --test_output=errors - -# Windows requires these for multi-python support: -build --enable_runfiles -common:bazel7.x --incompatible_python_disallow_native_rules diff --git a/tests/integration/ignore_root_user_error/.gitignore b/tests/integration/ignore_root_user_error/.gitignore deleted file mode 100644 index ac51a054d2..0000000000 --- a/tests/integration/ignore_root_user_error/.gitignore +++ /dev/null @@ -1 +0,0 @@ -bazel-* diff --git a/tests/integration/ignore_root_user_error/BUILD.bazel b/tests/integration/ignore_root_user_error/BUILD.bazel deleted file mode 100644 index 6e3b7b9d24..0000000000 --- a/tests/integration/ignore_root_user_error/BUILD.bazel +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright 2024 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("@rules_python//python:py_test.bzl", "py_test") -load("@rules_python//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility - -py_test( - name = "foo_test", - srcs = ["foo_test.py"], - visibility = ["//visibility:public"], -) - -py_test( - name = "bzlmod_test", - srcs = ["bzlmod_test.py"], - data = [ - "@rules_python//python/runfiles", - "@rules_python_bzlmod_debug//:debug_info.json", - ], - target_compatible_with = [] if BZLMOD_ENABLED else ["@platforms//:incompatible"], -) diff --git a/tests/integration/ignore_root_user_error/MODULE.bazel b/tests/integration/ignore_root_user_error/MODULE.bazel deleted file mode 100644 index 15c37c4388..0000000000 --- a/tests/integration/ignore_root_user_error/MODULE.bazel +++ /dev/null @@ -1,20 +0,0 @@ -module(name = "ignore_root_user_error") - -bazel_dep(name = "rules_python", version = "0.0.0") -local_path_override( - module_name = "rules_python", - path = "../../..", -) - -bazel_dep(name = "submodule") -local_path_override( - module_name = "submodule", - path = "submodule", -) - -python = use_extension("@rules_python//python/extensions:python.bzl", "python") -python.toolchain( - ignore_root_user_error = True, - python_version = "3.11", -) -use_repo(python, "rules_python_bzlmod_debug") diff --git a/tests/integration/ignore_root_user_error/README.md b/tests/integration/ignore_root_user_error/README.md deleted file mode 100644 index 47da5eb9ad..0000000000 --- a/tests/integration/ignore_root_user_error/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# ignore_root_user_errors -There are cases when we have to run Python targets with root, e.g., in Docker containers, requiring setting `ignore_root_user_error = True` when registering Python toolchain. This test makes sure that rules_python works in this case. \ No newline at end of file diff --git a/tests/integration/ignore_root_user_error/WORKSPACE b/tests/integration/ignore_root_user_error/WORKSPACE deleted file mode 100644 index 0a25819ecd..0000000000 --- a/tests/integration/ignore_root_user_error/WORKSPACE +++ /dev/null @@ -1,29 +0,0 @@ -load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") - -local_repository( - name = "rules_python", - path = "../../..", -) - -load("@rules_python//python:repositories.bzl", "py_repositories", "python_register_toolchains") - -py_repositories() - -python_register_toolchains( - name = "python39", - ignore_root_user_error = True, - python_version = "3.9", -) - -http_archive( - name = "bazel_skylib", - sha256 = "c6966ec828da198c5d9adbaa94c05e3a1c7f21bd012a0b29ba8ddbccb2c93b0d", - urls = [ - "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.1.1/bazel-skylib-1.1.1.tar.gz", - "https://github.com/bazelbuild/bazel-skylib/releases/download/1.1.1/bazel-skylib-1.1.1.tar.gz", - ], -) - -load("@bazel_skylib//:workspace.bzl", "bazel_skylib_workspace") - -bazel_skylib_workspace() diff --git a/tests/integration/ignore_root_user_error/bzlmod_test.py b/tests/integration/ignore_root_user_error/bzlmod_test.py deleted file mode 100644 index a1d6dc0630..0000000000 --- a/tests/integration/ignore_root_user_error/bzlmod_test.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright 2024 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. - -import json -import pathlib -import unittest - -from python.runfiles import runfiles - - -class BzlmodTest(unittest.TestCase): - def test_ignore_root_user_error_true_for_all_toolchains(self): - rf = runfiles.Create() - debug_path = pathlib.Path( - rf.Rlocation("rules_python_bzlmod_debug/debug_info.json") - ) - debug_info = json.loads(debug_path.read_bytes()) - actual = debug_info["toolchains_registered"] - # Because the root module set ignore_root_user_error=True, that should - # be the default for all other toolchains. - for entry in actual: - self.assertTrue( - entry["ignore_root_user_error"], - msg=f"Expected ignore_root_user_error=True, but got: {entry}", - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/integration/ignore_root_user_error/submodule/MODULE.bazel b/tests/integration/ignore_root_user_error/submodule/MODULE.bazel deleted file mode 100644 index f12870963c..0000000000 --- a/tests/integration/ignore_root_user_error/submodule/MODULE.bazel +++ /dev/null @@ -1,9 +0,0 @@ -module(name = "submodule") - -bazel_dep(name = "rules_python", version = "0.0.0") - -python = use_extension("@rules_python//python/extensions:python.bzl", "python") -python.toolchain( - ignore_root_user_error = False, - python_version = "3.10", -) diff --git a/tests/integration/integration_test.bzl b/tests/integration/integration_test.bzl index c437953319..f3d5cb6967 100644 --- a/tests/integration/integration_test.bzl +++ b/tests/integration/integration_test.bzl @@ -21,32 +21,22 @@ load( ) load("//python:py_test.bzl", "py_test") -def _test_runner(*, name, bazel_version, py_main, bzlmod, gazelle_plugin): +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"], ) return test_runner - if bazel_version.startswith("6") and not bzlmod: - if gazelle_plugin: - return "//tests/integration:bazel_6_4_workspace_test_runner_gazelle_plugin" - else: - return "//tests/integration:bazel_6_4_workspace_test_runner" - - if bzlmod and gazelle_plugin: - return "//tests/integration:test_runner_gazelle_plugin" - elif bzlmod: + if bzlmod: return "//tests/integration:test_runner" - elif gazelle_plugin: - return "//tests/integration:workspace_test_runner_gazelle_plugin" else: return "//tests/integration:workspace_test_runner" @@ -54,9 +44,9 @@ def rules_python_integration_test( name, workspace_path = None, bzlmod = True, - gazelle_plugin = False, tags = None, py_main = None, + py_deps = None, bazel_versions = None, **kwargs): """Runs a bazel-in-bazel integration test. @@ -67,11 +57,11 @@ def rules_python_integration_test( `_test` suffix. bzlmod: bool, default True. If true, run with bzlmod enabled, otherwise disable bzlmod. - gazelle_plugin: Whether the test uses the gazelle plugin. tags: Test tags. 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. @@ -103,8 +93,8 @@ def rules_python_integration_test( name = name, bazel_version = bazel_version, py_main = py_main, + py_deps = py_deps or [], bzlmod = bzlmod, - gazelle_plugin = gazelle_plugin, ) bazel_integration_test( name = "{}_bazel_{}".format(name, bazel_version), diff --git a/tests/integration/local_toolchains/.bazelrc b/tests/integration/local_toolchains/.bazelrc index aed08b0790..0fbb7678d1 100644 --- a/tests/integration/local_toolchains/.bazelrc +++ b/tests/integration/local_toolchains/.bazelrc @@ -1,4 +1,6 @@ +startup --windows_enable_symlinks common --action_env=RULES_PYTHON_BZLMOD_DEBUG=1 +common --repo_env=RULES_PYTHON_BZLMOD_DEBUG=1 common --lockfile_mode=off test --test_output=errors # Windows requires these for multi-python support: diff --git a/tests/integration/local_toolchains/BUILD.bazel b/tests/integration/local_toolchains/BUILD.bazel index a0cb2b164d..20ee7bcfe6 100644 --- a/tests/integration/local_toolchains/BUILD.bazel +++ b/tests/integration/local_toolchains/BUILD.bazel @@ -18,10 +18,24 @@ load("@rules_python//python:py_test.bzl", "py_test") load(":py_extension.bzl", "py_extension") py_test( - name = "test", - srcs = ["test.py"], + name = "local_runtime_test", + srcs = ["local_runtime_test.py"], + config_settings = { + "//:py": "local", + }, # Make this test better respect pyenv - env_inherit = ["PYENV_VERSION"], + env_inherit = [ + "PYENV_VERSION", + "PATH", + ], +) + +py_test( + name = "repo_runtime_test", + srcs = ["repo_runtime_test.py"], + config_settings = { + "//:py": "repo", + }, ) config_setting( @@ -31,6 +45,13 @@ config_setting( }, ) +config_setting( + name = "is_py_repo", + flag_values = { + ":py": "repo", + }, +) + # Set `--//:py=local` to use the local toolchain # (This is set in this example's .bazelrc) string_flag( diff --git a/tests/integration/local_toolchains/MODULE.bazel b/tests/integration/local_toolchains/MODULE.bazel index e81c012c2d..fe90fa235a 100644 --- a/tests/integration/local_toolchains/MODULE.bazel +++ b/tests/integration/local_toolchains/MODULE.bazel @@ -23,32 +23,78 @@ local_path_override( path = "../../..", ) +# Step 1: Define the python runtime local_runtime_repo = use_repo_rule("@rules_python//python/local_toolchains:repos.bzl", "local_runtime_repo") local_runtime_toolchains_repo = use_repo_rule("@rules_python//python/local_toolchains:repos.bzl", "local_runtime_toolchains_repo") +# This will use `python3` from the environment local_runtime_repo( name = "local_python3", interpreter_path = "python3", on_failure = "fail", ) +pbs_archive = use_repo_rule("//:pbs_archive.bzl", "pbs_archive") + +# "pbs" means "python-build-standalone" +# This maps the different platform runtimes to URLS and SHAs +pbs_archive( + name = "pbs_runtime", + sha256 = { + "linux": "0a01bad99fd4a165a11335c29eb43015dfdb8bd5ba8e305538ebb54f3bf3146d", + "mac os x": "7f5ec658219bdb1d1142c6abab89680322166c78350a017fb0af3c869dceee41", + "windows": "005cb2abf4cfa4aaa48fb10ce4e33fe4335ea4d1f55202dbe4e20c852e45e0f9", + }, + urls = { + "linux": "https://github.com/astral-sh/python-build-standalone/releases/download/20250918/cpython-3.13.7+20250918-x86_64-unknown-linux-gnu-install_only.tar.gz", + "mac os x": "https://github.com/astral-sh/python-build-standalone/releases/download/20250918/cpython-3.13.7+20250918-aarch64-apple-darwin-install_only.tar.gz", + "windows": "https://github.com/astral-sh/python-build-standalone/releases/download/20250918/cpython-3.13.7+20250918-x86_64-pc-windows-msvc-install_only.tar.gz", + }, +) + +# This will use Python from the `pbs_runtime` repository. +# The pbs_runtime is just an example; the repo just needs to be a valid Python +# installation. +local_runtime_repo( + name = "repo_python3", + interpreter_target = "@pbs_runtime//:python/bin/python", + on_failure = "fail", +) + +# Step 2: Create toolchains for the runtimes +# Below, we configure them to only activate if the `//:py` flag has particular +# values. local_runtime_toolchains_repo( name = "local_toolchains", - runtimes = ["local_python3"], + runtimes = [ + "local_python3", + "repo_python3", + ], target_compatible_with = { "local_python3": [ "HOST_CONSTRAINTS", ], + "repo_python3": [ + "HOST_CONSTRAINTS", + ], }, target_settings = { "local_python3": [ "@//:is_py_local", ], + "repo_python3": [ + "@//:is_py_repo", + ], }, ) +config = use_extension("@rules_python//python/extensions:config.bzl", "config") +config.add_transition_setting(setting = "//:py") + python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.toolchain(python_version = "3.13") use_repo(python, "rules_python_bzlmod_debug") +# Step 3: Register the toolchains register_toolchains("@local_toolchains//:all") diff --git a/tests/integration/local_toolchains/WORKSPACE b/tests/integration/local_toolchains/WORKSPACE index 480cd2794a..159f16deab 100644 --- a/tests/integration/local_toolchains/WORKSPACE +++ b/tests/integration/local_toolchains/WORKSPACE @@ -9,7 +9,11 @@ local_repository( load("@rules_python//python:repositories.bzl", "py_repositories") -py_repositories() +py_repositories( + transition_settings = [ + "@//:py", + ], +) load("@rules_python//python/local_toolchains:repos.bzl", "local_runtime_repo", "local_runtime_toolchains_repo") @@ -21,10 +25,51 @@ local_runtime_repo( # or interpreter_path = "C:\\path\\to\\python.exe" ) +load("//:pbs_archive.bzl", "pbs_archive") + +pbs_archive( + name = "pbs_runtime", + sha256 = { + "linux": "0a01bad99fd4a165a11335c29eb43015dfdb8bd5ba8e305538ebb54f3bf3146d", + "mac os x": "4fb42ffc8aad2a42ca7646715b8926bc6b2e0d31f13d2fec25943dc236a6fd60", + "windows": "005cb2abf4cfa4aaa48fb10ce4e33fe4335ea4d1f55202dbe4e20c852e45e0f9", + }, + urls = { + "linux": "https://github.com/astral-sh/python-build-standalone/releases/download/20250918/cpython-3.13.7+20250918-x86_64-unknown-linux-gnu-install_only.tar.gz", + "mac os x": "https://github.com/astral-sh/python-build-standalone/releases/download/20250918/cpython-3.13.7+20250918-x86_64-apple-darwin-install_only.tar.gz", + "windows server 2022": "https://github.com/astral-sh/python-build-standalone/releases/download/20250918/cpython-3.13.7+20250918-x86_64-pc-windows-msvc-install_only.tar.gz", + }, +) + +local_runtime_repo( + name = "repo_python3", + interpreter_target = "@pbs_runtime//:python/bin/python3", + on_failure = "fail", +) + # Step 2: Create toolchains for the runtimes local_runtime_toolchains_repo( name = "local_toolchains", - runtimes = ["local_python3"], + runtimes = [ + "local_python3", + "repo_python3", + ], + target_compatible_with = { + "local_python3": [ + "HOST_CONSTRAINTS", + ], + "repo_python3": [ + "HOST_CONSTRAINTS", + ], + }, + target_settings = { + "local_python3": [ + "@//:is_py_local", + ], + "repo_python3": [ + "@//:is_py_repo", + ], + }, ) # Step 3: Register the toolchains diff --git a/tests/integration/local_toolchains/echo_test.py b/tests/integration/local_toolchains/echo_test.py index 4cc31ff759..17121e0f17 100644 --- a/tests/integration/local_toolchains/echo_test.py +++ b/tests/integration/local_toolchains/echo_test.py @@ -4,6 +4,5 @@ class ExtensionTest(unittest.TestCase): - def test_echo_extension(self): self.assertEqual(echo_ext.echo(42, "str"), tuple(42, "str")) diff --git a/tests/integration/local_toolchains/test.py b/tests/integration/local_toolchains/local_runtime_test.py similarity index 90% rename from tests/integration/local_toolchains/test.py rename to tests/integration/local_toolchains/local_runtime_test.py index 0a0d6bedeb..220ceaead4 100644 --- a/tests/integration/local_toolchains/test.py +++ b/tests/integration/local_toolchains/local_runtime_test.py @@ -16,15 +16,20 @@ def test_python_from_path_used(self): # that wouldn't be reflected when sub-shells are run later. shell_path = shutil.which("python3") + if shell_path is None: + self.fail( + "which(python3) returned None.\n" + f"PATH={os.environ.get('PATH')}" + ) + # We call the interpreter and print its executable because of # things like pyenv: they install a shim that re-execs python. # The shim is e.g. /home/user/.pyenv/shims/python3, which then # runs e.g. /usr/bin/python3 with tempfile.TemporaryDirectory() as temp_dir: file_path = os.path.join(temp_dir, "info.py") - with open(file_path, 'w') as f: + with open(file_path, "w") as f: f.write( - """ + """ import sys print(sys.executable) print(sys._base_executable) diff --git a/tests/integration/local_toolchains/pbs_archive.bzl b/tests/integration/local_toolchains/pbs_archive.bzl new file mode 100644 index 0000000000..7d817b1b40 --- /dev/null +++ b/tests/integration/local_toolchains/pbs_archive.bzl @@ -0,0 +1,57 @@ +"""A repository rule to download and extract a Python runtime archive.""" + +BUILD_BAZEL = """ +# Generated by pbs_archive.bzl + +package( + default_visibility = ["//visibility:public"], +) + +exports_files(glob(["**"])) +""" + +def _pbs_archive_impl(repository_ctx): + """Implementation of the python_build_standalone_archive rule.""" + os_name = repository_ctx.os.name.lower() + urls = repository_ctx.attr.urls + sha256s = repository_ctx.attr.sha256 + + # os.name for windows contain build and version; simplify it + if "windows" in os_name: + os_name = "windows" + + if os_name not in urls: + fail("Unsupported OS: '{}'. Available OSs are: {}".format( + os_name, + ", ".join(urls.keys()), + )) + + url = urls[os_name] + sha256 = sha256s.get(os_name, "") + + repository_ctx.download_and_extract( + url = url, + sha256 = sha256, + ) + + repository_ctx.file("BUILD.bazel", BUILD_BAZEL) + +pbs_archive = repository_rule( + implementation = _pbs_archive_impl, + attrs = { + "sha256": attr.string_dict( + doc = "A dictionary of SHA256 checksums for the archives, keyed by OS name.", + mandatory = True, + ), + "urls": attr.string_dict( + doc = "A dictionary of URLs to the runtime archives, keyed by OS name (e.g., 'linux', 'windows').", + mandatory = True, + ), + }, + doc = """ +Downloads and extracts a Python runtime archive for the current OS. + +This rule selects a URL from the `urls` attribute based on the host OS, +downloads the archive, and extracts it. +""", +) diff --git a/tests/integration/local_toolchains/repo_runtime_test.py b/tests/integration/local_toolchains/repo_runtime_test.py new file mode 100644 index 0000000000..0eb2ef4248 --- /dev/null +++ b/tests/integration/local_toolchains/repo_runtime_test.py @@ -0,0 +1,16 @@ +import os.path +import sys +import unittest + + +class RepoToolchainTest(unittest.TestCase): + maxDiff = None + + def test_python_from_repo_used(self): + actual = os.path.realpath(sys._base_executable.lower()) + # Normalize case: Windows may have case differences + self.assertIn("pbs_runtime", actual.lower()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/integration/pip_parse_isolated/.bazelrc b/tests/integration/pip_parse_isolated/.bazelrc new file mode 100644 index 0000000000..227ce5a4cd --- /dev/null +++ b/tests/integration/pip_parse_isolated/.bazelrc @@ -0,0 +1,8 @@ +# Bazel configuration flags + +build --enable_runfiles + +common --experimental_isolated_extension_usages + +# https://docs.bazel.build/versions/main/best-practices.html#using-the-bazelrc-file +try-import %workspace%/user.bazelrc diff --git a/tests/integration/pip_parse_isolated/BUILD.bazel b/tests/integration/pip_parse_isolated/BUILD.bazel new file mode 100644 index 0000000000..2f825107f1 --- /dev/null +++ b/tests/integration/pip_parse_isolated/BUILD.bazel @@ -0,0 +1,7 @@ +load("@rules_python//python:py_test.bzl", "py_test") + +py_test( + name = "test_isolated", + srcs = ["test_isolated.py"], + deps = ["@pypi//six"], +) diff --git a/tests/integration/pip_parse_isolated/MODULE.bazel b/tests/integration/pip_parse_isolated/MODULE.bazel new file mode 100644 index 0000000000..6c44257acb --- /dev/null +++ b/tests/integration/pip_parse_isolated/MODULE.bazel @@ -0,0 +1,19 @@ +module(name = "pip_parse_isolated") + +bazel_dep(name = "rules_python") +local_path_override( + module_name = "rules_python", + path = "../../..", +) + +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.toolchain(python_version = "3.13") + +# This test module verifies that dependencies can be used with `isolate = True`. +pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip", isolate = True) +pip.parse( + hub_name = "pypi", + python_version = "3.13", + requirements_lock = "//:requirements_lock.txt", +) +use_repo(pip, "pypi") diff --git a/tests/integration/pip_parse_isolated/WORKSPACE b/tests/integration/pip_parse_isolated/WORKSPACE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/pip_parse_isolated/requirements_lock.txt b/tests/integration/pip_parse_isolated/requirements_lock.txt new file mode 100644 index 0000000000..b1445a37ae --- /dev/null +++ b/tests/integration/pip_parse_isolated/requirements_lock.txt @@ -0,0 +1,2 @@ +six==1.17.0 \ + --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 diff --git a/tests/integration/pip_parse_isolated/test_isolated.py b/tests/integration/pip_parse_isolated/test_isolated.py new file mode 100644 index 0000000000..5620801955 --- /dev/null +++ b/tests/integration/pip_parse_isolated/test_isolated.py @@ -0,0 +1,17 @@ +""" +Verify that a dependency added using an isolated extension can be imported. +See MODULE.bazel. +""" + +import unittest + +import six + + +class TestIsolated(unittest.TestCase): + def test_import(self): + self.assertTrue(hasattr(six, "PY3")) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/integration/runtime_manifests/.bazelrc b/tests/integration/runtime_manifests/.bazelrc new file mode 100644 index 0000000000..d4d45a5ea7 --- /dev/null +++ b/tests/integration/runtime_manifests/.bazelrc @@ -0,0 +1,4 @@ +# Copy of fast-tests config +common:fast-tests --build_tests_only=true +common:fast-tests --build_tag_filters=-large,-enormous,-integration-test +common:fast-tests --test_tag_filters=-large,-enormous,-integration-test diff --git a/tests/integration/runtime_manifests/BUILD.bazel b/tests/integration/runtime_manifests/BUILD.bazel new file mode 100644 index 0000000000..4746fd419b --- /dev/null +++ b/tests/integration/runtime_manifests/BUILD.bazel @@ -0,0 +1,7 @@ +load("@rules_python//python:py_test.bzl", "py_test") + +py_test( + name = "basic_test", + srcs = ["basic_test.py"], + python_version = "3.11", +) diff --git a/tests/integration/runtime_manifests/MODULE.bazel b/tests/integration/runtime_manifests/MODULE.bazel new file mode 100644 index 0000000000..6891b07818 --- /dev/null +++ b/tests/integration/runtime_manifests/MODULE.bazel @@ -0,0 +1,19 @@ +module(name = "runtime_manifests") + +bazel_dep(name = "rules_python", version = "0.0.0") +local_path_override( + module_name = "rules_python", + path = "../../..", +) + +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.override( + add_runtime_manifest_urls = [ + "https://github.com/astral-sh/python-build-standalone/releases/download/20260414/SHA256SUMS", + ], + register_all_versions = True, + runtime_manifest_sha = "ce18fdfd47c66830a40ea9b9e314a14b1636bbfd684501bc5ca1fc6d55a7933f", +) +python.toolchain( + python_version = "3.11.15", +) diff --git a/tests/integration/runtime_manifests/WORKSPACE b/tests/integration/runtime_manifests/WORKSPACE new file mode 100644 index 0000000000..8277ec8090 --- /dev/null +++ b/tests/integration/runtime_manifests/WORKSPACE @@ -0,0 +1 @@ +# Workspace boundary file required by rules_bazel_integration_test diff --git a/tests/integration/runtime_manifests/basic_test.py b/tests/integration/runtime_manifests/basic_test.py new file mode 100644 index 0000000000..35f0e93b6e --- /dev/null +++ b/tests/integration/runtime_manifests/basic_test.py @@ -0,0 +1,27 @@ +import datetime +import platform +import sys +import unittest + + +class BasicTest(unittest.TestCase): + def test_basic(self): + print("Hello World from Python {}!".format(sys.version)) + print("Interpreter executable path: {}".format(sys.executable)) + + # Verify that the hermetic interpreter inside Bazel's output/sandbox tree is used + self.assertIn(".cache/bazel", sys.executable) + + # Verify that the exact custom version (3.11.15) parsed from the manifest is used + self.assertEqual(sys.version_info[:3], (3, 11, 15)) + + # Verify that the exact build version (20260414) parsed from the manifest is used + buildno, builddate = platform.python_build() + date_str = " ".join(builddate.split()[:3]) + dt = datetime.datetime.strptime(date_str, "%b %d %Y") + formatted_date = dt.strftime("%Y%m%d") + self.assertEqual(formatted_date, "20260414") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/integration/toolchain_target_settings/.bazelrc b/tests/integration/toolchain_target_settings/.bazelrc new file mode 100644 index 0000000000..e1df951d06 --- /dev/null +++ b/tests/integration/toolchain_target_settings/.bazelrc @@ -0,0 +1,3 @@ +common --lockfile_mode=off +test --test_output=errors +build --enable_runfiles diff --git a/tests/integration/toolchain_target_settings/BUILD.bazel b/tests/integration/toolchain_target_settings/BUILD.bazel new file mode 100644 index 0000000000..0e28e461b3 --- /dev/null +++ b/tests/integration/toolchain_target_settings/BUILD.bazel @@ -0,0 +1,56 @@ +load("@bazel_skylib//rules:common_settings.bzl", "string_flag") +load("@rules_python//python:py_test.bzl", "py_test") + +# A flag to select which "family" of toolchains to use. +string_flag( + name = "family", + build_setting_default = "prebuilt", + values = [ + "prebuilt", + "custom", + ], +) + +# Matches when the "prebuilt" family is selected. +# This is referenced in MODULE.bazel's python.override(add_target_settings=...). +config_setting( + name = "is_prebuilt", + flag_values = { + ":family": "prebuilt", + }, +) + +# Matches when the "custom" family is selected. +# No toolchains use this setting, so selecting it should produce an error. +config_setting( + name = "is_custom", + flag_values = { + ":family": "custom", + }, +) + +# This target selects the "prebuilt" family via config_settings transition. +# Since python.override(add_target_settings = ["@@//:is_prebuilt"]) gates +# the default toolchains, this should succeed: the flag matches, the config_setting +# is satisfied, and the default 3.13 toolchain resolves. +py_test( + name = "prebuilt_test", + srcs = ["main.py"], + config_settings = { + "//:family": "prebuilt", + }, + main = "main.py", +) + +# This target selects the "custom" family via config_settings transition. +# No toolchains have target_settings = [":is_custom"], so toolchain resolution +# should fail -- the default toolchains are gated behind ":is_prebuilt" and +# won't match. +py_test( + name = "custom_no_toolchain_test", + srcs = ["main.py"], + config_settings = { + "//:family": "custom", + }, + main = "main.py", +) diff --git a/tests/integration/toolchain_target_settings/MODULE.bazel b/tests/integration/toolchain_target_settings/MODULE.bazel new file mode 100644 index 0000000000..ec3a56749e --- /dev/null +++ b/tests/integration/toolchain_target_settings/MODULE.bazel @@ -0,0 +1,24 @@ +module(name = "module_under_test") + +bazel_dep(name = "rules_python", version = "0.0.0") +bazel_dep(name = "bazel_skylib", version = "1.7.1") + +local_path_override( + module_name = "rules_python", + path = "../../..", +) + +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.toolchain(python_version = "3.13") + +# Gate ALL default-registered toolchains behind the "prebuilt" config setting. +# This prevents them from being a silent fallback when a different toolchain +# family is requested. +python.override( + add_target_settings = ["@@//:is_prebuilt"], +) + +# Register //:family as a transition setting so py_binary/py_test +# config_settings can set it. +config = use_extension("@rules_python//python/extensions:config.bzl", "config") +config.add_transition_setting(setting = "//:family") diff --git a/tests/integration/toolchain_target_settings/REPO.bazel b/tests/integration/toolchain_target_settings/REPO.bazel new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/toolchain_target_settings/WORKSPACE b/tests/integration/toolchain_target_settings/WORKSPACE new file mode 100644 index 0000000000..d7be0c96c7 --- /dev/null +++ b/tests/integration/toolchain_target_settings/WORKSPACE @@ -0,0 +1 @@ +# Intentionally blank; bzlmod is used. diff --git a/tests/integration/toolchain_target_settings/WORKSPACE.bzlmod b/tests/integration/toolchain_target_settings/WORKSPACE.bzlmod new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/toolchain_target_settings/main.py b/tests/integration/toolchain_target_settings/main.py new file mode 100644 index 0000000000..52ec605a55 --- /dev/null +++ b/tests/integration/toolchain_target_settings/main.py @@ -0,0 +1,3 @@ +import sys + +print(f"Python {sys.version}") diff --git a/tests/integration/toolchain_target_settings_test.py b/tests/integration/toolchain_target_settings_test.py new file mode 100644 index 0000000000..a0c9d42fc3 --- /dev/null +++ b/tests/integration/toolchain_target_settings_test.py @@ -0,0 +1,39 @@ +"""Integration test for python.override(add_target_settings=...). + +Verifies that when all default toolchains are gated behind a config_setting, +requesting a different (unregistered) toolchain family produces a toolchain +resolution error instead of silently falling back to the default toolchains. +""" + +import unittest + +from tests.integration import runner + + +class AddTargetSettingsTest(runner.TestCase): + def test_prebuilt_family_resolves(self): + """Building with the 'prebuilt' family should succeed. + + The default toolchains have target_settings = [":is_prebuilt"], + and the transition sets //:family=prebuilt, so the config_setting + matches and toolchain resolution finds the default 3.13 toolchain. + """ + self.run_bazel("test", "//:prebuilt_test") + + def test_custom_family_without_toolchain_fails(self): + """Building with the 'custom' family should fail. + + No toolchains have target_settings = [":is_custom"], and the default + toolchains are gated behind ":is_prebuilt" (via add_target_settings), + so toolchain resolution should fail with no matching toolchain. + """ + result = self.run_bazel("build", "//:custom_no_toolchain_test", check=False) + self.assertNotEqual(result.exit_code, 0, "Expected build to fail") + self.assert_result_matches( + result, + r"No matching toolchains found for types", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/integration/unified_pypi/.bazelrc b/tests/integration/unified_pypi/.bazelrc new file mode 100644 index 0000000000..b3a24e8605 --- /dev/null +++ b/tests/integration/unified_pypi/.bazelrc @@ -0,0 +1 @@ +common --experimental_enable_bzlmod diff --git a/tests/integration/unified_pypi/BUILD.bazel b/tests/integration/unified_pypi/BUILD.bazel new file mode 100644 index 0000000000..c0b9905fa6 --- /dev/null +++ b/tests/integration/unified_pypi/BUILD.bazel @@ -0,0 +1,61 @@ +load("@rules_python//python:py_binary.bzl", "py_binary") +load("@rules_python//python:py_test.bzl", "py_test") + +package(default_visibility = ["//visibility:public"]) + +py_test( + name = "test_default", + srcs = ["test_default.py"], + deps = ["@pypi//colorama"], +) + +py_test( + name = "test_cli", + srcs = ["test_cli.py"], + deps = ["@pypi//colorama"], +) + +py_test( + name = "test_a", + srcs = ["test_a.py"], + config_settings = { + "@rules_python//python/config_settings:venv": "pypi_a", + }, + deps = [ + "@pypi//colorama", + "@pypi//colorama:my_colorama", + ], +) + +# Sibling extra alias failure target (my_colorama is missing in pypi_b): +py_binary( + name = "bin_extra_b", + srcs = ["bin_extra_b.py"], + config_settings = { + "@rules_python//python/config_settings:venv": "pypi_b", + }, + deps = ["@pypi//colorama:my_colorama"], +) + +# Disjoint package failure target (six is missing in pypi_a): +py_binary( + name = "bin_six_a", + srcs = ["bin_six_a.py"], + config_settings = { + "@rules_python//python/config_settings:venv": "pypi_a", + }, + deps = ["@pypi//six"], +) + +py_binary( + name = "bin_declared_only", + srcs = ["bin_declared_only.py"], + deps = ["@pypi//declared_only_pkg"], +) + +py_binary( + name = "bin_declared_only_alias", + srcs = ["bin_declared_only.py"], + main = "bin_declared_only.py", + deps = ["@pypi//declared_only_pkg:declared-only-alias"], +) diff --git a/tests/integration/unified_pypi/MODULE.bazel b/tests/integration/unified_pypi/MODULE.bazel new file mode 100644 index 0000000000..6a4b87e015 --- /dev/null +++ b/tests/integration/unified_pypi/MODULE.bazel @@ -0,0 +1,52 @@ +module(name = "unified_pypi") + +bazel_dep(name = "rules_python", version = "0.0.0") +local_path_override( + module_name = "rules_python", + path = "../../..", +) + +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.toolchain(python_version = "3.11") + +pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") +pip.whl_mods( + additive_build_content = """\ +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "my_colorama", + deps = [":pkg"], +) +""", + hub_name = "whl_mods_hub", + whl_name = "colorama", +) +use_repo(pip, "whl_mods_hub") + +# pypi_a has colorama and an extra alias +pip.parse( + extra_hub_aliases = {"colorama": ["my_colorama"]}, + hub_name = "pypi_a", + python_version = "3.11", + requirements_lock = "//:requirements_a.txt", + whl_modifications = { + "@whl_mods_hub//:colorama.json": "colorama", + }, +) +use_repo(pip, "pypi_a") + +# pypi_b has colorama and six, and acts as designated fallback +pip.parse( + hub_name = "pypi_b", + python_version = "3.11", + requirements_lock = "//:requirements_b.txt", +) +use_repo(pip, "pypi_b") + +pip.default(default_hub = "pypi_b") +pip.dep( + name = "declared-only-pkg", + extra_targets = ["declared-only-alias"], +) +use_repo(pip, "pypi") diff --git a/tests/integration/unified_pypi/WORKSPACE b/tests/integration/unified_pypi/WORKSPACE new file mode 100644 index 0000000000..0a08afe832 --- /dev/null +++ b/tests/integration/unified_pypi/WORKSPACE @@ -0,0 +1 @@ +# Minimal WORKSPACE file diff --git a/tests/integration/unified_pypi/WORKSPACE.bzlmod b/tests/integration/unified_pypi/WORKSPACE.bzlmod new file mode 100644 index 0000000000..7bd1c969b9 --- /dev/null +++ b/tests/integration/unified_pypi/WORKSPACE.bzlmod @@ -0,0 +1 @@ +# Minimal WORKSPACE.bzlmod diff --git a/tests/integration/unified_pypi/bin_declared_only.py b/tests/integration/unified_pypi/bin_declared_only.py new file mode 100644 index 0000000000..0b03607e77 --- /dev/null +++ b/tests/integration/unified_pypi/bin_declared_only.py @@ -0,0 +1,2 @@ +# Dummy file for integration test +print("declared_only") diff --git a/tests/integration/unified_pypi/bin_extra_b.py b/tests/integration/unified_pypi/bin_extra_b.py new file mode 100644 index 0000000000..f900d16fd2 --- /dev/null +++ b/tests/integration/unified_pypi/bin_extra_b.py @@ -0,0 +1 @@ +print("Should not be executed") diff --git a/tests/integration/unified_pypi/bin_six_a.py b/tests/integration/unified_pypi/bin_six_a.py new file mode 100644 index 0000000000..f900d16fd2 --- /dev/null +++ b/tests/integration/unified_pypi/bin_six_a.py @@ -0,0 +1 @@ +print("Should not be executed") diff --git a/tests/integration/unified_pypi/requirements_a.txt b/tests/integration/unified_pypi/requirements_a.txt new file mode 100644 index 0000000000..788f12f818 --- /dev/null +++ b/tests/integration/unified_pypi/requirements_a.txt @@ -0,0 +1,3 @@ +colorama==0.4.6 \ + --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ + --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 diff --git a/tests/integration/unified_pypi/requirements_b.txt b/tests/integration/unified_pypi/requirements_b.txt new file mode 100644 index 0000000000..c69f3631b2 --- /dev/null +++ b/tests/integration/unified_pypi/requirements_b.txt @@ -0,0 +1,6 @@ +colorama==0.4.5 \ + --hash=sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da \ + --hash=sha256:e6c6b4334fc50988a639d9b98ae42f5c90ec94cb1495b4fe76c5f72cf7f79435 +six==1.17.0 \ + --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ + --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 diff --git a/tests/integration/unified_pypi/test_a.py b/tests/integration/unified_pypi/test_a.py new file mode 100644 index 0000000000..a9127ba0c7 --- /dev/null +++ b/tests/integration/unified_pypi/test_a.py @@ -0,0 +1,3 @@ +import colorama + +assert colorama.__version__ == "0.4.6" diff --git a/tests/integration/unified_pypi/test_cli.py b/tests/integration/unified_pypi/test_cli.py new file mode 100644 index 0000000000..a9127ba0c7 --- /dev/null +++ b/tests/integration/unified_pypi/test_cli.py @@ -0,0 +1,3 @@ +import colorama + +assert colorama.__version__ == "0.4.6" diff --git a/tests/integration/unified_pypi/test_default.py b/tests/integration/unified_pypi/test_default.py new file mode 100644 index 0000000000..559df59961 --- /dev/null +++ b/tests/integration/unified_pypi/test_default.py @@ -0,0 +1,3 @@ +import colorama + +assert colorama.__version__ == "0.4.5" diff --git a/tests/integration/unified_pypi_test.py b/tests/integration/unified_pypi_test.py new file mode 100644 index 0000000000..a19a9fcfbe --- /dev/null +++ b/tests/integration/unified_pypi_test.py @@ -0,0 +1,100 @@ +"""Integration test for Unified PyPI Hub dynamic dependency resolution.""" + +import contextlib +import unittest + +from tests.integration import runner + + +class UnifiedPypiTest(runner.TestCase): + def test_default_fallback_hub(self): + self.run_bazel("test", "//:test_default") + + def test_transitioned_hub(self): + self.run_bazel("test", "//:test_a") + + def test_cli_override(self): + self.run_bazel( + "run", + "--@rules_python//python/config_settings:venv=pypi_a", + "//:test_cli", + ) + + def test_disjoint_package_cquery_succeeds_but_build_fails(self): + self.run_bazel("cquery", "//:bin_six_a") + result = self.run_bazel("build", "//:bin_six_a", check=False) + self.assertNotEqual( + result.exit_code, + 0, + "Expected build to fail during execution phase", + ) + self.assert_result_matches( + result, + 'ERROR: PyPI package "six" is not available when building under PyPI hub "pypi_a"\\. Try adding it to the requirements of this hub', + ) + + def test_sibling_extra_alias_cquery_succeeds_but_build_fails(self): + self.run_bazel("cquery", "//:bin_extra_b") + result = self.run_bazel("build", "//:bin_extra_b", check=False) + self.assertNotEqual( + result.exit_code, + 0, + "Expected build to fail during execution phase", + ) + self.assert_result_matches( + result, + 'ERROR: PyPI package "colorama:my_colorama" is not available when building under PyPI hub "pypi_b"\\. Try adding it to the requirements of this hub', + ) + + @contextlib.contextmanager + def _temp_modify_file(self, path, new_content): + original_content = path.read_text() + path.write_text(new_content) + try: + yield + finally: + path.write_text(original_content) + + def test_invalid_default_hub_fails_evaluation(self): + module_bazel = self.repo_root / "MODULE.bazel" + invalid_content = module_bazel.read_text().replace( + 'pip.default(default_hub = "pypi_b")', + 'pip.default(default_hub = "invalid_hub")', + ) + with self._temp_modify_file(module_bazel, invalid_content): + # Run bazel cquery and expect it to fail during loading/extension phase + result = self.run_bazel("cquery", "//:test_default", check=False) + self.assertNotEqual( + result.exit_code, + 0, + "Expected extension evaluation to fail due to invalid default_hub", + ) + self.assert_result_matches( + result, + "default_hub 'invalid_hub' is not a defined PyPI hub", + ) + + def test_unimplemented_declared_dep_fails_build(self): + # Even though cquery succeeds: + self.run_bazel("cquery", "//:bin_declared_only") + + # Build must fail because the package is not implemented by any concrete hub + result = self.run_bazel("build", "//:bin_declared_only", check=False) + self.assertNotEqual(result.exit_code, 0) + self.assert_result_matches( + result, + 'ERROR: PyPI package "declared_only_pkg" is not available when building under PyPI hub "pypi_b"\\. Try adding it to the requirements of this hub', + ) + + def test_unimplemented_declared_dep_alias_fails_build(self): + # Build must fail for alias too + result = self.run_bazel("build", "//:bin_declared_only_alias", check=False) + self.assertNotEqual(result.exit_code, 0) + self.assert_result_matches( + result, + 'ERROR: PyPI package "declared_only_pkg:declared-only-alias" is not available when building under PyPI hub "pypi_b"\\. Try adding it to the requirements of this hub', + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/integration/uv_lock/.bazelrc b/tests/integration/uv_lock/.bazelrc new file mode 100644 index 0000000000..511f8a2413 --- /dev/null +++ b/tests/integration/uv_lock/.bazelrc @@ -0,0 +1,5 @@ +build --enable_runfiles +common --experimental_isolated_extension_usages +common --action_env=UV_EXTRA_INDEX_URL + +try-import %workspace%/user.bazelrc diff --git a/tests/integration/uv_lock/.bazelversion b/tests/integration/uv_lock/.bazelversion new file mode 100644 index 0000000000..47da986f86 --- /dev/null +++ b/tests/integration/uv_lock/.bazelversion @@ -0,0 +1 @@ +9.1.0 diff --git a/tests/integration/uv_lock/BUILD.bazel b/tests/integration/uv_lock/BUILD.bazel new file mode 100644 index 0000000000..da6482ba08 --- /dev/null +++ b/tests/integration/uv_lock/BUILD.bazel @@ -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", +) diff --git a/tests/integration/uv_lock/MODULE.bazel b/tests/integration/uv_lock/MODULE.bazel new file mode 100644 index 0000000000..0e0978ed36 --- /dev/null +++ b/tests/integration/uv_lock/MODULE.bazel @@ -0,0 +1,18 @@ +module(name = "uv_lock") + +bazel_dep(name = "bazel_skylib", version = "1.7.1") +bazel_dep(name = "platforms", version = "0.0.10") +bazel_dep(name = "rules_python") +local_path_override( + module_name = "rules_python", + path = "../../..", +) + +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.toolchain(python_version = "3.13") + +uv = use_extension("@rules_python//python/uv:uv.bzl", "uv") +uv.configure(version = "0.11.2") +use_repo(uv, "uv") + +register_toolchains("@uv//:all") diff --git a/tests/integration/uv_lock/README.md b/tests/integration/uv_lock/README.md new file mode 100644 index 0000000000..cce142852c --- /dev/null +++ b/tests/integration/uv_lock/README.md @@ -0,0 +1,64 @@ +# uv_lock integration test workspace + +This directory is a self-contained Bazel workspace used by the +`//tests/integration:uv_lock_test` integration test. + +It demonstrates how to use the `lock()` macro from `@rules_python//python/uv` +to pin requirements with `uv pip compile`. + +## Targets + +| Target | Description | +|--------|-------------| +| `//:requirements` | Build action that produces the locked requirements file | +| `//:requirements.update` | Update the in-source `requirements.txt` via `bazel run` | +| `//:requirements.run` | Run `uv pip compile` with extra command-line args | +| `//:requirements_diff_test` | Diff test comparing the lock output to the in-source file | +| `//:uv` | The `uv` binary from the registered toolchain | + +## Workflow for debugging + +If you want to debug and play around, you can start the server and then run the uv lock command +manually. + +### Start the local PyPI server + +In a separate terminal, start the pypiserver that serves the `my-local-pkg` test wheel: + +```shell +bazel run //tests/integration:uv_lock_pypi_server [-- --no-auth] +``` + +The server prints the URL to use (with and without authentication) and the +SHA256 of the wheel. Pass `--no-auth` to allow anonymous access. + +### Lock the requirements + +With the server running, lock the requirements from this directory: + +```shell +cd tests/integration/uv_lock +bazel run //:requirements.update \ + --action_env=UV_EXTRA_INDEX_URL="" \ + --action_env=UV_CREDENTIALS_DIR= +``` + +The `` and `` values are printed by the pypi-server. + +### Verify the lock output matches the in-source file + +```shell +bazel test //:requirements_diff_test +``` + +## bazel-in-bazel Testing + +When iterating on changes to `lock.bzl`, the integration test can be run +directly from rules_python: + +```shell +bazel test //tests/integration:uv_lock_test_bazel_self \ + --config=fast-tests \ + --test_output=streamed \ + --test_filter= +``` diff --git a/tests/integration/uv_lock/WORKSPACE b/tests/integration/uv_lock/WORKSPACE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/uv_lock/requirements.in b/tests/integration/uv_lock/requirements.in new file mode 100644 index 0000000000..fba55a7329 --- /dev/null +++ b/tests/integration/uv_lock/requirements.in @@ -0,0 +1 @@ +my-local-pkg==1.0.0 diff --git a/tests/integration/uv_lock/requirements.txt b/tests/integration/uv_lock/requirements.txt new file mode 100644 index 0000000000..52f5c757f5 --- /dev/null +++ b/tests/integration/uv_lock/requirements.txt @@ -0,0 +1,5 @@ +# This file was autogenerated by uv via the following command: +# bazel run //:requirements.update +my-local-pkg==1.0.0 \ + --hash=sha256:be24d5183a182e8da4465ae1b7e60324864d1a72866ec9aefa6aaf80a4529eb1 + # via -r requirements.in diff --git a/tests/integration/uv_lock/uv_runner.bzl b/tests/integration/uv_lock/uv_runner.bzl new file mode 100644 index 0000000000..3faa3c6fc0 --- /dev/null +++ b/tests/integration/uv_lock/uv_runner.bzl @@ -0,0 +1,30 @@ +"""A rule exposing the uv binary from the registered toolchain as an executable target. + +This allows running ``bazel run //:uv -- `` from the test workspace. +""" + +def _uv_runner_impl(ctx): + toolchain_info = ctx.toolchains["@rules_python//python/uv:uv_toolchain_type"] + original_uv_executable = toolchain_info.uv_toolchain_info.uv[DefaultInfo].files_to_run.executable + + ext = "" + if ctx.attr.is_windows: + ext = ".exe" + + uv_exe = ctx.actions.declare_file("uv" + ext) + ctx.actions.symlink(output = uv_exe, target_file = original_uv_executable) + + return DefaultInfo( + files = depset([uv_exe]), + executable = uv_exe, + runfiles = toolchain_info.default_info.default_runfiles, + ) + +uv_runner = rule( + implementation = _uv_runner_impl, + executable = True, + attrs = { + "is_windows": attr.bool(mandatory = True), + }, + toolchains = ["@rules_python//python/uv:uv_toolchain_type"], +) diff --git a/tests/integration/uv_lock_pypi_server.py b/tests/integration/uv_lock_pypi_server.py new file mode 100644 index 0000000000..0d940e7569 --- /dev/null +++ b/tests/integration/uv_lock_pypi_server.py @@ -0,0 +1,148 @@ +import argparse +import hashlib +import io +import os +import sys +import uuid +import zipfile +from wsgiref.simple_server import make_server + +from pypiserver import app_from_config, setup_routes_from_config +from pypiserver.config import Config + + +def _create_wheel_bytes(name, version): + pkg_name_normalized = name.replace("-", "_") + wheel_name = "{}-{}-py3-none-any.whl".format(pkg_name_normalized, version) + dist_info = "{}-{}.dist-info".format(pkg_name_normalized, version) + + metadata = ( + "Metadata-Version: 2.1\n" + "Name: {name}\n" + "Version: {version}\n" + "Summary: A test package\n" + ).format(name=pkg_name_normalized, version=version) + + wheel_file = ( + "Wheel-Version: 1.0\n" + "Generator: test\n" + "Root-Is-Purelib: true\n" + "Tag: py3-none-any\n" + ) + + record_entries = [ + "{}/__init__.py,".format(pkg_name_normalized), + "{}/METADATA,".format(dist_info), + "{}/WHEEL,".format(dist_info), + "{}/RECORD,".format(dist_info), + ] + + buf = io.BytesIO() + with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf: + zf.writestr("{}/__init__.py".format(pkg_name_normalized), "# empty\n") + zf.writestr("{}/METADATA".format(dist_info), metadata) + zf.writestr("{}/WHEEL".format(dist_info), wheel_file) + zf.writestr("{}/RECORD".format(dist_info), "\n".join(record_entries)) + + wheel_data = buf.getvalue() + sha256 = hashlib.sha256(wheel_data).hexdigest() + return wheel_data, sha256, wheel_name + + +def main(): + parser = argparse.ArgumentParser( + description="Standalone pypiserver for uv_lock integration tests" + ) + parser.add_argument( + "--packages-dir", + type=str, + default=None, + help="Directory for the test wheels (default: $TEST_TMPDIR/pypi-server-packages)", + ) + parser.add_argument( + "--no-auth", + action="store_true", + default=False, + help="Disable authentication (allows anonymous access)", + ) + parser.add_argument( + "--port", + type=int, + default=0, + help="Port to listen on (0 = find free port)", + ) + parser.add_argument( + "--host", + default="localhost", + help="Host to bind to", + ) + args = parser.parse_args() + + if args.packages_dir is None: + sandbox_root = ( + os.environ.get("TEST_TMPDIR") or os.environ.get("TMPDIR") or "/tmp" + ) + args.packages_dir = os.path.join(sandbox_root, "pypi-server-packages") + packages_dir = args.packages_dir + os.makedirs(packages_dir, exist_ok=True) + + wheel_data, sha256, wheel_name = _create_wheel_bytes("my-local-pkg", "1.0.0") + wheel_path = os.path.join(packages_dir, wheel_name) + with open(wheel_path, "wb") as f: + f.write(wheel_data) + + print("Wheel: {}".format(wheel_path), flush=True) + print("SHA256: {}".format(sha256), flush=True) + + password = uuid.uuid4().hex + username = "testuser" + + if args.no_auth: + authenticate = [] + else: + authenticate = ["download", "list", "update"] + + config = Config.default_with_overrides( + roots=[packages_dir], + port=args.port, + host=args.host, + authenticate=authenticate, + password_file=None, + auther=lambda u, p: u == username and p == password, + disable_fallback=True, + fallback_url="", + server_method="wsgiref", + verbosity=0, + log_stream=None, + ) + app = app_from_config(config) + app = setup_routes_from_config(app, config) + + server = make_server(args.host, args.port, app) + port = server.server_address[1] + + base_url = "http://{}:{}".format(args.host, port) + auth_url = "http://{}:{}@{}:{}".format(username, password, args.host, port) + + print("\npypiserver listening on:\n", flush=True) + print(" URL (no auth): {}".format(base_url), flush=True) + print(" URL (auth): {}".format(auth_url), flush=True) + print("\nRequired dependency in requirements.in:", flush=True) + print(" my-local-pkg==1.0.0", flush=True) + print("\nTo use from the uv_lock test workspace, run:", flush=True) + print(" cd tests/integration/uv_lock", flush=True) + print(" bazel run //:requirements.update \\", flush=True) + print(' --action_env=UV_EXTRA_INDEX_URL="{}" \\'.format(auth_url), flush=True) + print(" --action_env=UV_CREDENTIALS_DIR=", flush=True) + print("\nPress Ctrl+C to stop the server.\n", flush=True) + + try: + server.serve_forever() + except KeyboardInterrupt: + print("\nShutting down...", flush=True) + server.shutdown() + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/integration/uv_lock_test.py b/tests/integration/uv_lock_test.py new file mode 100644 index 0000000000..8efd3cb84f --- /dev/null +++ b/tests/integration/uv_lock_test.py @@ -0,0 +1,288 @@ +import base64 +import hashlib +import os +import re +import threading +import time +import unittest +import uuid +from pathlib import Path +from urllib.error import URLError +from urllib.request import Request, urlopen +from wsgiref.simple_server import make_server + +from pypiserver import app_from_config, setup_routes_from_config +from pypiserver.config import Config + +from tests.integration import runner +from tests.integration.uv_lock_pypi_server import _create_wheel_bytes + + +def _make_server_on_free_port(app): + server = make_server("localhost", 0, app) + port = server.server_address[1] + return server, port + + +class UvLockIntegrationTest(runner.TestCase): + def setUp(self): + super().setUp() + + self.username = "testuser" + self.password = uuid.uuid4().hex + + self.dir = Path(os.environ["TEST_TMPDIR"]) + self.docroot = self.dir / "simple" + self.docroot.mkdir(exist_ok=True) + + self.wheel_data, self.wheel_sha256, wheel_name = _create_wheel_bytes( + "my-local-pkg", + "1.0.0", + ) + + packages_dir = self.docroot / "packages" + packages_dir.mkdir(exist_ok=True) + self.wheel_path = packages_dir / wheel_name + self.wheel_path.write_bytes(self.wheel_data) + + config = Config.default_with_overrides( + roots=[packages_dir], + port=0, + host="localhost", + authenticate=["download", "list", "update"], + password_file=None, + auther=lambda u, p: u == self.username and p == self.password, + disable_fallback=True, + fallback_url="", + server_method="wsgiref", + verbosity=0, + log_stream=None, + ) + app = app_from_config(config) + app = setup_routes_from_config(app, config) + + self._server, self.port = _make_server_on_free_port(app) + self.server_url = "http://localhost:{port}".format(port=self.port) + self.auth_url = "http://{user}:{passwd}@localhost:{port}".format( + user=self.username, + passwd=self.password, + port=self.port, + ) + + self._thread = threading.Thread(target=self._server.serve_forever) + self._thread.daemon = True + self._thread.start() + + interval = 0.1 + wait_seconds = 40 + for _ in range(int(wait_seconds / interval)): + try: + req = Request(self.server_url) + with urlopen(req, timeout=1) as response: + if response.status in (200, 401): + break + except (URLError, OSError): + pass + time.sleep(interval) + else: + raise RuntimeError( + "Could not start the server, waited for {}s".format(wait_seconds) + ) + + # Set a default value for UV_EXTRA_INDEX_URL in the bazel env so that + # the workspace .bazelrc `--action_env=UV_EXTRA_INDEX_URL` doesn't + # fail on Windows when the variable is unset in the client env. + self.bazel_env.setdefault("UV_EXTRA_INDEX_URL", "") + + # Use a sandbox-local credential store so credentials don't leak + # to the host system. + self.creds_dir = self.repo_root / ".uv-creds" + self.creds_dir.mkdir(parents=True, exist_ok=True) + self.bazel_env["UV_CREDENTIALS_DIR"] = str(self.creds_dir) + + # Log in to uv's credential store so `uv auth helper` can later + # serve the credentials to Bazel or uv itself. + self.run_bazel( + "run", + "//:uv", + "--", + "auth", + "login", + f"--username={self.username}", + f"--password={self.password}", + self.server_url, + ) + + def tearDown(self): + # Clear credentials from uv's credential store to ensure we are not + # logged into the service after the test. + self.run_bazel( + "run", + "//:uv", + "--", + "auth", + "logout", + self.server_url, + check=False, + ) + self._server.shutdown() + + def _assert_server_requires_auth(self): + req = Request(self.server_url + "/my-local-pkg/") + try: + urlopen(req, timeout=5) + self.fail("Expected 401 without auth") + except URLError: + pass + + def _auth_header(self): + return "Basic " + base64.b64encode( + "{user}:{passwd}".format( + user=self.username, + passwd=self.password, + ).encode("utf-8") + ).decode("utf-8") + + def _assert_simple_api_sha256(self): + auth_header = self._auth_header() + req = Request(self.server_url + "/simple/my-local-pkg/") + req.add_header("Authorization", auth_header) + resp = urlopen(req, timeout=5) + html = resp.read().decode("utf-8") + + match = re.search(r"#sha256=([a-f0-9]+)", html) + self.assertIsNotNone(match, "No sha256 found in simple API: {}".format(html)) + pypiserver_sha256 = match.group(1) + disk_sha256 = hashlib.sha256(self.wheel_path.read_bytes()).hexdigest() + self.assertEqual( + pypiserver_sha256, + disk_sha256, + "pypiserver hash {} != disk hash {}".format(pypiserver_sha256, disk_sha256), + ) + + def _creds_auth_args(self): + return [ + "--strategy=PyRequirementsLockUv=local", + "--action_env={key}={value}".format( + key="UV_CREDENTIALS_DIR", + value=str(self.creds_dir), + ), + "--action_env={key}={value}".format( + key="UV_EXTRA_INDEX_URL", + value=self.server_url, + ), + ] + + def _assert_lock_file(self, result): + self.assertEqual( + result.exit_code, + 0, + "Lock update failed:\n{}".format(result.describe()), + ) + lock_file = self.repo_root / "requirements.txt" + self.assertTrue(lock_file.exists(), "Lock file was not created") + contents = lock_file.read_text() + self.assertIn("my-local-pkg", contents) + self.assertIn("--hash=sha256:", contents) + + def test_lock_update_with_custom_index(self): + self._assert_server_requires_auth() + self._assert_simple_api_sha256() + + result = self.run_bazel( + "run", + "--action_env={key}={value}".format( + key="UV_EXTRA_INDEX_URL", + value=self.auth_url, + ), + "//:requirements.update", + ) + self._assert_lock_file(result) + + def test_update_with_credential_helper(self): + """Use a credential helper for authentication.""" + self._assert_server_requires_auth() + result = self.run_bazel( + "run", + *self._creds_auth_args(), + "//:requirements.update", + ) + self._assert_lock_file(result) + + def test_update_with_uv_auth_helper(self): + """Use the uv auth helper for authentication.""" + self._assert_server_requires_auth() + result = self.run_bazel( + "run", + *self._creds_auth_args(), + "//:requirements.update", + ) + self._assert_lock_file(result) + + def test_diff_test_with_requirements(self): + """Verify that ``diff_test`` can verify the generated lock file.""" + self._assert_server_requires_auth() + + # First generate the lock file + result = self.run_bazel( + "run", + *self._creds_auth_args(), + "//:requirements.update", + ) + self._assert_lock_file(result) + + # Copy the generated lock file to the expected location. The inner + # Bazel workspace is writable because it is a temporary copy created + # by the integration test framework. + generated = self.repo_root / "requirements.txt" + expected = self.repo_root / "requirements_expected.txt" + expected.write_text(generated.read_text()) + + # Run the diff_test: it builds the lock action, then compares the + # output to our expected file. + result = self.run_bazel( + "test", + *self._creds_auth_args(), + "//:requirements_diff_test", + ) + self.assertEqual( + result.exit_code, + 0, + "diff_test failed:\n{}".format(result.describe()), + ) + + def test_no_existing_requirements(self): + """Verify that ``bazel run`` and ``diff_test`` work when + ``requirements.txt`` does not yet exist.""" + self._assert_server_requires_auth() + + # Remove the existing lock file to simulate a fresh checkout + existing = self.repo_root / "requirements.txt" + existing.unlink() + self.assertFalse(existing.exists()) + + # Run ``requirements.update`` to generate the lock from scratch. The + # underlying lock rule will have no ``existing_output`` to copy, but + # ``uv pip compile`` should still produce the output. + result = self.run_bazel( + "run", + *self._creds_auth_args(), + "//:requirements.update", + ) + self._assert_lock_file(result) + + # diff_test should pass now that ``requirements.txt`` exists again + result = self.run_bazel( + "test", + *self._creds_auth_args(), + "//:requirements_diff_test", + ) + self.assertEqual( + result.exit_code, + 0, + "diff_test failed:\n{}".format(result.describe()), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/integration/validate_test_main/BUILD.bazel b/tests/integration/validate_test_main/BUILD.bazel new file mode 100644 index 0000000000..cf931d3303 --- /dev/null +++ b/tests/integration/validate_test_main/BUILD.bazel @@ -0,0 +1,21 @@ +load("@rules_python//python:py_test.bzl", "py_test") + +# A test whose main module only defines a test case but never runs it. With +# --validate_test_main=enabled this should fail to build. +py_test( + name = "inert_test", + srcs = ["inert_test.py"], +) + +# A test whose main module invokes a runner. This should always build. +py_test( + name = "good_test", + srcs = ["good_test.py"], +) + +# A test whose main module defines nothing and only imports. This is allowed +# even with validation enabled, since it isn't the "defined but never run" case. +py_test( + name = "import_only_test", + srcs = ["import_only_test.py"], +) diff --git a/tests/integration/validate_test_main/MODULE.bazel b/tests/integration/validate_test_main/MODULE.bazel new file mode 100644 index 0000000000..c154ac169e --- /dev/null +++ b/tests/integration/validate_test_main/MODULE.bazel @@ -0,0 +1,10 @@ +module(name = "module_under_test") + +bazel_dep(name = "rules_python", version = "0.0.0") +local_path_override( + module_name = "rules_python", + path = "../../..", +) + +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.toolchain(python_version = "3.11") diff --git a/tests/integration/validate_test_main/WORKSPACE b/tests/integration/validate_test_main/WORKSPACE new file mode 100644 index 0000000000..de908549c0 --- /dev/null +++ b/tests/integration/validate_test_main/WORKSPACE @@ -0,0 +1,13 @@ +local_repository( + name = "rules_python", + path = "../../..", +) + +load("@rules_python//python:repositories.bzl", "py_repositories", "python_register_toolchains") + +py_repositories() + +python_register_toolchains( + name = "python_3_11", + python_version = "3.11", +) diff --git a/tests/integration/validate_test_main/WORKSPACE.bzlmod b/tests/integration/validate_test_main/WORKSPACE.bzlmod new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/validate_test_main/good_test.py b/tests/integration/validate_test_main/good_test.py new file mode 100644 index 0000000000..46107c96ec --- /dev/null +++ b/tests/integration/validate_test_main/good_test.py @@ -0,0 +1,10 @@ +import unittest + + +class GoodTest(unittest.TestCase): + def test_something(self): + self.assertTrue(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/integration/validate_test_main/import_only_test.py b/tests/integration/validate_test_main/import_only_test.py new file mode 100644 index 0000000000..1bd46dead7 --- /dev/null +++ b/tests/integration/validate_test_main/import_only_test.py @@ -0,0 +1,3 @@ +# This module defines no classes or functions; it only imports. The validation +# action allows it even when enabled. +import os # noqa: F401 diff --git a/tests/integration/validate_test_main/inert_test.py b/tests/integration/validate_test_main/inert_test.py new file mode 100644 index 0000000000..8decf676d4 --- /dev/null +++ b/tests/integration/validate_test_main/inert_test.py @@ -0,0 +1,7 @@ +import unittest + + +class InertTest(unittest.TestCase): + def test_nothing_runs(self): + # This test case is never executed because nothing invokes a runner. + self.assertTrue(True) diff --git a/tests/integration/validate_test_main_test.py b/tests/integration/validate_test_main_test.py new file mode 100644 index 0000000000..4d108411d5 --- /dev/null +++ b/tests/integration/validate_test_main_test.py @@ -0,0 +1,40 @@ +import unittest + +from tests.integration import runner + +_FLAG = "--@rules_python//python/config_settings:validate_test_main" + + +class ValidateTestMainTest(runner.TestCase): + def test_inert_test_fails_when_enabled(self): + """An inert test main should fail the build when validation is on.""" + result = self.run_bazel( + "build", + f"{_FLAG}=enabled", + "//:inert_test", + check=False, + ) + self.assertNotEqual(result.exit_code, 0, "Expected build to fail") + self.assert_result_matches(result, r"will not run any tests") + + def test_good_test_builds_when_enabled(self): + """A main that invokes a runner should build when validation is on.""" + self.run_bazel("build", f"{_FLAG}=enabled", "//:good_test") + + def test_import_only_test_builds_when_enabled(self): + """A main that only imports (defines nothing) is allowed when on.""" + self.run_bazel("build", f"{_FLAG}=enabled", "//:import_only_test") + + def test_inert_test_builds_when_disabled(self): + """Validation is off by default, so even an inert test builds.""" + self.run_bazel("build", f"{_FLAG}=disabled", "//:inert_test") + + def test_inert_test_builds_by_default(self): + """The default (auto) resolves to disabled, so an inert test builds.""" + self.run_bazel("build", "//:inert_test") + + +if __name__ == "__main__": + # Enabling this makes the runner log subprocesses as the test goes along. + # logging.basicConfig(level = "INFO") + unittest.main() diff --git a/tests/modules/other/BUILD.bazel b/tests/modules/other/BUILD.bazel index 46f1b96faa..665049b9f5 100644 --- a/tests/modules/other/BUILD.bazel +++ b/tests/modules/other/BUILD.bazel @@ -1,3 +1,4 @@ +load("@rules_python//python:py_binary.bzl", "py_binary") load("@rules_python//tests/support:py_reconfig.bzl", "py_reconfig_binary") package( @@ -12,3 +13,18 @@ py_reconfig_binary( bootstrap_impl = "system_python", main = "external_main.py", ) + +py_binary( + name = "venv_bin", + srcs = ["venv_bin.py"], + config_settings = { + "@rules_python//python/config_settings:bootstrap_impl": "script", + "@rules_python//python/config_settings:venvs_site_packages": "yes", + }, + deps = [ + # Add two packages that install into the same directory. This is + # to test that namespace packages install correctly and are importable. + "//nspkg_delta", + "//nspkg_gamma", + ], +) diff --git a/tests/modules/other/nspkg_delta/BUILD.bazel b/tests/modules/other/nspkg_delta/BUILD.bazel index 457033aacf..ca142d2c10 100644 --- a/tests/modules/other/nspkg_delta/BUILD.bazel +++ b/tests/modules/other/nspkg_delta/BUILD.bazel @@ -6,5 +6,5 @@ py_library( name = "nspkg_delta", srcs = glob(["site-packages/**/*.py"]), experimental_venvs_site_packages = "@rules_python//python/config_settings:venvs_site_packages", - imports = [package_name() + "/site-packages"], + imports = ["site-packages"], ) diff --git a/tests/modules/other/nspkg_gamma/BUILD.bazel b/tests/modules/other/nspkg_gamma/BUILD.bazel index 89038e80d2..0fe099eb0a 100644 --- a/tests/modules/other/nspkg_gamma/BUILD.bazel +++ b/tests/modules/other/nspkg_gamma/BUILD.bazel @@ -6,5 +6,5 @@ py_library( name = "nspkg_gamma", srcs = glob(["site-packages/**/*.py"]), experimental_venvs_site_packages = "@rules_python//python/config_settings:venvs_site_packages", - imports = [package_name() + "/site-packages"], + imports = ["site-packages"], ) diff --git a/tests/modules/other/nspkg_single/BUILD.bazel b/tests/modules/other/nspkg_single/BUILD.bazel index 08cb4f373e..07e269b878 100644 --- a/tests/modules/other/nspkg_single/BUILD.bazel +++ b/tests/modules/other/nspkg_single/BUILD.bazel @@ -6,5 +6,5 @@ py_library( name = "nspkg_single", srcs = glob(["site-packages/**/*.py"]), experimental_venvs_site_packages = "@rules_python//python/config_settings:venvs_site_packages", - imports = [package_name() + "/site-packages"], + imports = ["site-packages"], ) diff --git a/tests/modules/other/simple_v1/BUILD.bazel b/tests/modules/other/simple_v1/BUILD.bazel index da5db8164a..0fa2f14a88 100644 --- a/tests/modules/other/simple_v1/BUILD.bazel +++ b/tests/modules/other/simple_v1/BUILD.bazel @@ -10,5 +10,5 @@ py_library( exclude = ["site-packages/**/*.py"], ), experimental_venvs_site_packages = "@rules_python//python/config_settings:venvs_site_packages", - imports = [package_name() + "/site-packages"], + imports = ["site-packages"], ) diff --git a/tests/modules/other/simple_v2/BUILD.bazel b/tests/modules/other/simple_v2/BUILD.bazel index 45f83a5a88..5a7e066aec 100644 --- a/tests/modules/other/simple_v2/BUILD.bazel +++ b/tests/modules/other/simple_v2/BUILD.bazel @@ -10,6 +10,6 @@ py_library( exclude = ["site-packages/**/*.py"], ), experimental_venvs_site_packages = "@rules_python//python/config_settings:venvs_site_packages", - imports = [package_name() + "/site-packages"], + imports = ["site-packages"], pyi_srcs = glob(["**/*.pyi"]), ) diff --git a/tests/modules/other/venv_bin.py b/tests/modules/other/venv_bin.py new file mode 100644 index 0000000000..81de1a9ab9 --- /dev/null +++ b/tests/modules/other/venv_bin.py @@ -0,0 +1,17 @@ +import nspkg + +print(nspkg) + +import nspkg.subnspkg # noqa: E402 + +print(nspkg.subnspkg) + +import nspkg.subnspkg.delta # noqa: E402 + +print(nspkg.subnspkg.delta) + +import nspkg.subnspkg.gamma # noqa: E402 + +print(nspkg.subnspkg.gamma) + +print("@other//:venv_bin ran successfully.") diff --git a/tests/modules/other/with_external_data/BUILD.bazel b/tests/modules/other/with_external_data/BUILD.bazel index fc047aadab..338f77947d 100644 --- a/tests/modules/other/with_external_data/BUILD.bazel +++ b/tests/modules/other/with_external_data/BUILD.bazel @@ -19,5 +19,5 @@ py_library( srcs = ["site-packages/with_external_data.py"], data = [":external_data"], experimental_venvs_site_packages = "@rules_python//python/config_settings:venvs_site_packages", - imports = [package_name() + "/site-packages"], + imports = ["site-packages"], ) diff --git a/tests/news/BUILD.bazel b/tests/news/BUILD.bazel new file mode 100644 index 0000000000..31abff1a8a --- /dev/null +++ b/tests/news/BUILD.bazel @@ -0,0 +1,8 @@ +load("//python:py_test.bzl", "py_test") + +py_test( + name = "news_test", + srcs = ["news_test.py"], + data = ["//news:news_files"], + deps = ["//python/runfiles"], +) diff --git a/tests/news/news_test.py b/tests/news/news_test.py new file mode 100644 index 0000000000..a8ed7a2849 --- /dev/null +++ b/tests/news/news_test.py @@ -0,0 +1,68 @@ +import pathlib +import unittest + +from python.runfiles import runfiles + + +def _get_news_dir(): + rf = runfiles.Create() + path = rf.Rlocation("rules_python/news") + if path: + return pathlib.Path(path) + return None + + +class NewsTest(unittest.TestCase): + def test_all_news_files_are_valid(self): + news_dir = _get_news_dir() + self.assertIsNotNone(news_dir, "Could not locate news directory in runfiles") + self.assertTrue(news_dir.exists(), "News directory does not exist in runfiles") + + allowed_categories = {"added", "changed", "fixed", "removed"} + + for p in news_dir.iterdir(): + if not p.is_file(): + continue + # Ignore BUILD files and .gitkeep if they are in the directory + if p.name in ("BUILD", "BUILD.bazel", ".gitkeep"): + continue + + filename = p.name + + # Collapse extension and filename check into a single assertRegex + self.assertRegex( + filename, + r"^[^.]+\.[^.]+\.md$", + f"News filename {filename} must follow ..md pattern", + ) + + parts = filename.split(".") + category = parts[1].lower() + + # Category must be valid + self.assertIn( + category, + allowed_categories, + f"News file {filename} has invalid category '{category}'. " + f"Must be one of {allowed_categories}", + ) + + # Must be readable as UTF-8 + try: + content = p.read_text(encoding="utf-8").strip() + except (IOError, UnicodeDecodeError) as e: + self.fail(f"Failed to read news file {filename} as UTF-8: {e}") + + # Content must not be empty + self.assertTrue(len(content) > 0, f"News file {filename} must not be empty") + + # Content must NOT start with bullet points (* or -) + self.assertFalse( + content.startswith("* ") or content.startswith("- "), + f"News file {filename} must not start with bullet points (* or -). " + "The release tool adds them automatically.", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/py_runtime/py_runtime_tests.bzl b/tests/py_runtime/py_runtime_tests.bzl index 4ec7590ab2..ac80c8556d 100644 --- a/tests/py_runtime/py_runtime_tests.bzl +++ b/tests/py_runtime/py_runtime_tests.bzl @@ -13,7 +13,6 @@ # limitations under the License. """Starlark tests for py_runtime rule.""" -load("@rules_python_internal//:rules_python_config.bzl", "config") load("@rules_testing//lib:analysis_test.bzl", "analysis_test") load("@rules_testing//lib:test_suite.bzl", "test_suite") load("@rules_testing//lib:truth.bzl", "matching") @@ -21,15 +20,10 @@ load("@rules_testing//lib:util.bzl", rt_util = "util") load("//python:py_runtime.bzl", "py_runtime") load("//python:py_runtime_info.bzl", "PyRuntimeInfo") load("//python/private:common_labels.bzl", "labels") # buildifier: disable=bzl-visibility -load("//tests/base_rules:util.bzl", br_util = "util") load("//tests/support:py_runtime_info_subject.bzl", "py_runtime_info_subject") _tests = [] -_SKIP_TEST = { - "target_compatible_with": ["@platforms//:incompatible"], -} - def _simple_binary_impl(ctx): executable = ctx.actions.declare_file(ctx.label.name) ctx.actions.write(executable, "", is_executable = True) @@ -48,28 +42,33 @@ _simple_binary = rule( executable = True, ) -def _test_bootstrap_template(name): - # The bootstrap_template arg isn't present in older Bazel versions, so - # we have to conditionally pass the arg and mark the test incompatible. - if config.enable_pystar: - py_runtime_kwargs = {"bootstrap_template": "bootstrap.txt"} - attr_values = {} - else: - py_runtime_kwargs = {} - attr_values = _SKIP_TEST +def _source_file_wrapper_impl(ctx): + return [DefaultInfo( + files = depset([ctx.file.src]), + runfiles = ctx.runfiles(files = ctx.files.data), + )] +_source_file_wrapper = rule( + implementation = _source_file_wrapper_impl, + attrs = { + "data": attr.label_list(allow_files = True), + "src": attr.label(allow_single_file = True, mandatory = True), + }, +) + +def _test_bootstrap_template(name): rt_util.helper_target( py_runtime, name = name + "_subject", interpreter_path = "/py", python_version = "PY3", - **py_runtime_kwargs + bootstrap_template = "bootstrap.txt", ) analysis_test( name = name, target = name + "_subject", impl = _test_bootstrap_template_impl, - attr_values = attr_values, + attr_values = {}, ) def _test_bootstrap_template_impl(env, target): @@ -81,29 +80,19 @@ def _test_bootstrap_template_impl(env, target): _tests.append(_test_bootstrap_template) def _test_cannot_have_both_inbuild_and_system_interpreter(name): - if br_util.is_bazel_6_or_higher(): - py_runtime_kwargs = { - "interpreter": "fake_interpreter", - "interpreter_path": "/some/path", - } - attr_values = {} - else: - py_runtime_kwargs = { - "interpreter_path": "/some/path", - } - attr_values = _SKIP_TEST rt_util.helper_target( py_runtime, name = name + "_subject", python_version = "PY3", - **py_runtime_kwargs + interpreter = "fake_interpreter", + interpreter_path = "/some/path", ) analysis_test( name = name, target = name + "_subject", impl = _test_cannot_have_both_inbuild_and_system_interpreter_impl, expect_failure = True, - attr_values = attr_values, + attr_values = {}, ) def _test_cannot_have_both_inbuild_and_system_interpreter_impl(env, target): @@ -114,25 +103,19 @@ def _test_cannot_have_both_inbuild_and_system_interpreter_impl(env, target): _tests.append(_test_cannot_have_both_inbuild_and_system_interpreter) def _test_cannot_specify_files_for_system_interpreter(name): - if br_util.is_bazel_6_or_higher(): - py_runtime_kwargs = {"files": ["foo.txt"]} - attr_values = {} - else: - py_runtime_kwargs = {} - attr_values = _SKIP_TEST rt_util.helper_target( py_runtime, name = name + "_subject", interpreter_path = "/foo", python_version = "PY3", - **py_runtime_kwargs + files = ["foo.txt"], ) analysis_test( name = name, target = name + "_subject", impl = _test_cannot_specify_files_for_system_interpreter_impl, expect_failure = True, - attr_values = attr_values, + attr_values = {}, ) def _test_cannot_specify_files_for_system_interpreter_impl(env, target): @@ -143,21 +126,12 @@ def _test_cannot_specify_files_for_system_interpreter_impl(env, target): _tests.append(_test_cannot_specify_files_for_system_interpreter) def _test_coverage_tool_executable(name): - if br_util.is_bazel_6_or_higher(): - py_runtime_kwargs = { - "coverage_tool": name + "_coverage_tool", - } - attr_values = {} - else: - py_runtime_kwargs = {} - attr_values = _SKIP_TEST - rt_util.helper_target( py_runtime, name = name + "_subject", python_version = "PY3", interpreter_path = "/bogus", - **py_runtime_kwargs + coverage_tool = name + "_coverage_tool", ) rt_util.helper_target( _simple_binary, @@ -168,7 +142,7 @@ def _test_coverage_tool_executable(name): name = name, target = name + "_subject", impl = _test_coverage_tool_executable_impl, - attr_values = attr_values, + attr_values = {}, ) def _test_coverage_tool_executable_impl(env, target): @@ -183,14 +157,10 @@ def _test_coverage_tool_executable_impl(env, target): _tests.append(_test_coverage_tool_executable) def _test_coverage_tool_plain_files(name): - if br_util.is_bazel_6_or_higher(): - py_runtime_kwargs = { - "coverage_tool": name + "_coverage_tool", - } - attr_values = {} - else: - py_runtime_kwargs = {} - attr_values = _SKIP_TEST + py_runtime_kwargs = { + "coverage_tool": name + "_coverage_tool", + } + attr_values = {} rt_util.helper_target( py_runtime, name = name + "_subject", @@ -239,11 +209,54 @@ def _test_in_build_interpreter(name): def _test_in_build_interpreter_impl(env, target): info = env.expect.that_target(target).provider(PyRuntimeInfo, factory = py_runtime_info_subject) info.python_version().equals("PY3") - info.files().contains_predicate(matching.file_basename_equals("file1.txt")) + info.files().contains_exactly([ + "{package}/fake_interpreter", + "{package}/file1.txt", + ]) info.interpreter().path().contains("fake_interpreter") + env.expect.that_bool(info.actual.interpreter_files_to_run == None).equals(True) _tests.append(_test_in_build_interpreter) +def _test_non_executable_source_file_interpreter_keeps_file_only_behavior(name): + rt_util.helper_target( + _source_file_wrapper, + name = name + "_wrapped_interpreter", + src = "fake_interpreter", + data = ["runfile.txt"], + ) + + rt_util.helper_target( + py_runtime, + name = name + "_subject", + interpreter = name + "_wrapped_interpreter", + python_version = "PY3", + files = ["file1.txt"], + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_non_executable_source_file_interpreter_keeps_file_only_behavior_impl, + ) + +def _test_non_executable_source_file_interpreter_keeps_file_only_behavior_impl(env, target): + target = env.expect.that_target(target) + py_runtime_info = target.provider( + PyRuntimeInfo, + factory = py_runtime_info_subject, + ) + py_runtime_info.interpreter().short_path_equals("{package}/fake_interpreter") + env.expect.that_bool(py_runtime_info.actual.interpreter_files_to_run == None).equals(True) + py_runtime_info.files().contains_exactly([ + "{package}/file1.txt", + ]) + + target.default_outputs().contains_exactly([ + "{package}/file1.txt", + ]) + +_tests.append(_test_non_executable_source_file_interpreter_keeps_file_only_behavior) + def _test_interpreter_binary_with_multiple_outputs(name): rt_util.helper_target( _simple_binary, @@ -271,6 +284,9 @@ def _test_interpreter_binary_with_multiple_outputs_impl(env, target): factory = py_runtime_info_subject, ) py_runtime_info.interpreter().short_path_equals("{package}/{test_name}_built_interpreter") + py_runtime_info.interpreter_files_to_run().executable().short_path_equals( + "{package}/{test_name}_built_interpreter", + ) py_runtime_info.files().contains_exactly([ "{package}/extra_default_output.txt", "{package}/runfile.txt", @@ -316,6 +332,9 @@ def _test_interpreter_binary_with_single_output_and_runfiles_impl(env, target): factory = py_runtime_info_subject, ) py_runtime_info.interpreter().short_path_equals("{package}/{test_name}_built_interpreter") + py_runtime_info.interpreter_files_to_run().executable().short_path_equals( + "{package}/{test_name}_built_interpreter", + ) py_runtime_info.files().contains_exactly([ "{package}/runfile.txt", "{package}/{test_name}_built_interpreter", @@ -334,14 +353,8 @@ def _test_interpreter_binary_with_single_output_and_runfiles_impl(env, target): _tests.append(_test_interpreter_binary_with_single_output_and_runfiles) def _test_must_have_either_inbuild_or_system_interpreter(name): - if br_util.is_bazel_6_or_higher(): - py_runtime_kwargs = {} - attr_values = {} - else: - py_runtime_kwargs = { - "interpreter_path": "/some/path", - } - attr_values = _SKIP_TEST + py_runtime_kwargs = {} + attr_values = {} rt_util.helper_target( py_runtime, name = name + "_subject", @@ -377,22 +390,18 @@ def _test_system_interpreter(name): ) def _test_system_interpreter_impl(env, target): - env.expect.that_target(target).provider( + info = env.expect.that_target(target).provider( PyRuntimeInfo, factory = py_runtime_info_subject, - ).interpreter_path().equals("/system/python") + ) + info.interpreter_path().equals("/system/python") + env.expect.that_bool(info.actual.interpreter_files_to_run == None).equals(True) _tests.append(_test_system_interpreter) def _test_system_interpreter_must_be_absolute(name): - # Bazel 5.4 will entirely crash when an invalid interpreter_path - # is given. - if br_util.is_bazel_6_or_higher(): - py_runtime_kwargs = {"interpreter_path": "relative/path"} - attr_values = {} - else: - py_runtime_kwargs = {"interpreter_path": "/junk/value/for/bazel5.4"} - attr_values = _SKIP_TEST + py_runtime_kwargs = {"interpreter_path": "relative/path"} + attr_values = {} rt_util.helper_target( py_runtime, name = name + "_subject", @@ -415,28 +424,19 @@ def _test_system_interpreter_must_be_absolute_impl(env, target): _tests.append(_test_system_interpreter_must_be_absolute) def _interpreter_version_info_test(name, interpreter_version_info, impl, expect_failure = True): - if config.enable_pystar: - py_runtime_kwargs = { - "interpreter_version_info": interpreter_version_info, - } - attr_values = {} - else: - py_runtime_kwargs = {} - attr_values = _SKIP_TEST - rt_util.helper_target( py_runtime, name = name + "_subject", python_version = "PY3", interpreter_path = "/py", - **py_runtime_kwargs + interpreter_version_info = interpreter_version_info, ) analysis_test( name = name, target = name + "_subject", impl = impl, expect_failure = expect_failure, - attr_values = attr_values, + attr_values = {}, ) def _test_interpreter_version_info_must_define_major_and_minor_only_major(name): @@ -530,9 +530,6 @@ def _test_interpreter_version_info_parses_values_to_struct_impl(env, target): _tests.append(_test_interpreter_version_info_parses_values_to_struct) def _test_version_info_from_flag(name): - if not config.enable_pystar: - rt_util.skip_test(name) - return py_runtime( name = name + "_subject", interpreter_version_info = None, diff --git a/tests/py_runtime_info/py_runtime_info_tests.bzl b/tests/py_runtime_info/py_runtime_info_tests.bzl index 9acf541683..6b9ecac605 100644 --- a/tests/py_runtime_info/py_runtime_info_tests.bzl +++ b/tests/py_runtime_info/py_runtime_info_tests.bzl @@ -15,19 +15,16 @@ load("@rules_testing//lib:analysis_test.bzl", "analysis_test") load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("@rules_testing//lib:truth.bzl", "matching") load("//python:py_runtime_info.bzl", "PyRuntimeInfo") -load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER") # buildifier: disable=bzl-visibility +load("//tests/support:py_runtime_info_subject.bzl", "py_runtime_info_subject") def _create_py_runtime_info_without_interpreter_version_info_impl(ctx): - kwargs = {} - if IS_BAZEL_7_OR_HIGHER: - kwargs["bootstrap_template"] = ctx.attr.bootstrap_template - return [PyRuntimeInfo( interpreter = ctx.file.interpreter, files = depset(ctx.files.files), python_version = "PY3", - **kwargs + bootstrap_template = ctx.attr.bootstrap_template, )] _create_py_runtime_info_without_interpreter_version_info = rule( @@ -40,6 +37,57 @@ _create_py_runtime_info_without_interpreter_version_info = rule( }, ) +def _simple_binary_impl(ctx): + executable = ctx.actions.declare_file(ctx.label.name) + ctx.actions.write(executable, "", is_executable = True) + return [DefaultInfo( + executable = executable, + files = depset([executable]), + )] + +_simple_binary = rule( + implementation = _simple_binary_impl, + executable = True, +) + +def _file_target_impl(ctx): + output = ctx.actions.declare_file(ctx.label.name + ".txt") + ctx.actions.write(output, "") + return [DefaultInfo(files = depset([output]))] + +_file_target = rule( + implementation = _file_target_impl, +) + +def _create_py_runtime_info_with_interpreter_files_to_run_impl(ctx): + files_to_run = ctx.attr.files_to_run[DefaultInfo].files_to_run + kwargs = dict( + bootstrap_template = ctx.file.bootstrap_template, + interpreter_files_to_run = files_to_run, + python_version = "PY3", + ) + if ctx.attr.use_interpreter_path: + kwargs["interpreter_path"] = "/python" + else: + kwargs["files"] = depset() + kwargs["interpreter"] = ctx.executable.interpreter + + return [PyRuntimeInfo(**kwargs)] + +_create_py_runtime_info_with_interpreter_files_to_run = rule( + implementation = _create_py_runtime_info_with_interpreter_files_to_run_impl, + attrs = { + "bootstrap_template": attr.label(allow_single_file = True, default = "bootstrap.txt"), + "files_to_run": attr.label(mandatory = True), + "interpreter": attr.label( + cfg = "target", + executable = True, + mandatory = True, + ), + "use_interpreter_path": attr.bool(), + }, +) + _tests = [] def _test_can_create_py_runtime_info_without_interpreter_version_info(name): @@ -58,6 +106,112 @@ def _test_can_create_py_runtime_info_without_interpreter_version_info_impl(env, _tests.append(_test_can_create_py_runtime_info_without_interpreter_version_info) +def _test_interpreter_files_to_run_with_interpreter(name): + _simple_binary( + name = name + "_interpreter", + ) + _create_py_runtime_info_with_interpreter_files_to_run( + name = name + "_subject", + files_to_run = name + "_interpreter", + interpreter = name + "_interpreter", + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_interpreter_files_to_run_with_interpreter_impl, + ) + +def _test_interpreter_files_to_run_with_interpreter_impl(env, target): + info = env.expect.that_target(target).provider( + PyRuntimeInfo, + factory = py_runtime_info_subject, + ) + info.interpreter().short_path_equals("{package}/{test_name}_interpreter") + info.interpreter_files_to_run().executable().short_path_equals( + "{package}/{test_name}_interpreter", + ) + +_tests.append(_test_interpreter_files_to_run_with_interpreter) + +def _test_interpreter_files_to_run_disallows_interpreter_path(name): + _simple_binary( + name = name + "_interpreter", + ) + _create_py_runtime_info_with_interpreter_files_to_run( + name = name + "_subject", + files_to_run = name + "_interpreter", + interpreter = name + "_interpreter", + tags = ["manual"], + use_interpreter_path = True, + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_interpreter_files_to_run_disallows_interpreter_path_impl, + expect_failure = True, + ) + +def _test_interpreter_files_to_run_disallows_interpreter_path_impl(env, target): + env.expect.that_target(target).failures().contains_predicate( + matching.str_matches("*interpreter_files_to_run*interpreter_path*"), + ) + +_tests.append(_test_interpreter_files_to_run_disallows_interpreter_path) + +def _test_interpreter_files_to_run_requires_executable(name): + _simple_binary( + name = name + "_interpreter", + ) + _file_target( + name = name + "_files_to_run", + ) + _create_py_runtime_info_with_interpreter_files_to_run( + name = name + "_subject", + files_to_run = name + "_files_to_run", + interpreter = name + "_interpreter", + tags = ["manual"], + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_interpreter_files_to_run_requires_executable_impl, + expect_failure = True, + ) + +def _test_interpreter_files_to_run_requires_executable_impl(env, target): + env.expect.that_target(target).failures().contains_predicate( + matching.str_matches("*interpreter_files_to_run*executable*"), + ) + +_tests.append(_test_interpreter_files_to_run_requires_executable) + +def _test_interpreter_files_to_run_requires_matching_interpreter(name): + _simple_binary( + name = name + "_interpreter", + ) + _simple_binary( + name = name + "_other_interpreter", + ) + _create_py_runtime_info_with_interpreter_files_to_run( + name = name + "_subject", + files_to_run = name + "_other_interpreter", + interpreter = name + "_interpreter", + tags = ["manual"], + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_interpreter_files_to_run_requires_matching_interpreter_impl, + expect_failure = True, + ) + +def _test_interpreter_files_to_run_requires_matching_interpreter_impl(env, target): + env.expect.that_target(target).failures().contains_predicate( + matching.str_matches("*interpreter_files_to_run.executable*interpreter*"), + ) + +_tests.append(_test_interpreter_files_to_run_requires_matching_interpreter) + def py_runtime_info_test_suite(name): test_suite( name = name, diff --git a/tests/py_zipapp/BUILD.bazel b/tests/py_zipapp/BUILD.bazel new file mode 100644 index 0000000000..a68448d964 --- /dev/null +++ b/tests/py_zipapp/BUILD.bazel @@ -0,0 +1,116 @@ +load("@rules_shell//shell:sh_test.bzl", "sh_test") +load("//python:py_binary.bzl", "py_binary") +load("//python:py_library.bzl", "py_library") +load("//python:py_test.bzl", "py_test") +load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility +load("//python/zipapp:py_zipapp_binary.bzl", "py_zipapp_binary") + +py_binary( + name = "venv_bin", + srcs = ["main.py"], + config_settings = { + "//python/config_settings:venvs_site_packages": "yes", + } | select({ + "@platforms//os:windows": {}, + "//conditions:default": { + "//python/config_settings:bootstrap_impl": "script", + }, + }), + main = "main.py", + deps = [":bin_deps"], +) + +py_zipapp_binary( + name = "venv_zipapp", + binary = ":venv_bin", +) + +py_test( + name = "venv_zipapp_test", + srcs = ["venv_zipapp_test.py"], + data = [":venv_zipapp"], + env = { + "BZLMOD_ENABLED": str(int(BZLMOD_ENABLED)), + "TEST_ZIPAPP": "$(location :venv_zipapp)", + }, +) + +# Create the app with a supported level of compression +py_zipapp_binary( + name = "venv_zipapp_compressed", + binary = ":venv_bin", + compression = "4", +) + +py_test( + name = "venv_zipapp_compressed_test", + srcs = ["venv_zipapp_test.py"], + data = [":venv_zipapp_compressed"], + env = { + "BZLMOD_ENABLED": str(int(BZLMOD_ENABLED)), + "COMPRESSED": "1", + "TEST_ZIPAPP": "$(location :venv_zipapp_compressed)", + }, + main = "venv_zipapp_test.py", +) + +py_binary( + name = "system_python_bin", + srcs = ["main.py"], + config_settings = { + "//python/config_settings:bootstrap_impl": "system_python", + "//python/config_settings:venvs_site_packages": "no", + }, + main = "main.py", + deps = [":bin_deps"], +) + +py_zipapp_binary( + name = "system_python_zipapp", + binary = ":system_python_bin", +) + +py_test( + name = "system_python_zipapp_test", + srcs = ["system_python_zipapp_test.py"], + data = [":system_python_zipapp"], + env = { + "TEST_ZIPAPP": "$(location :system_python_zipapp)", + }, +) + +sh_test( + name = "system_python_zipapp_external_bootstrap_test", + srcs = ["system_python_zipapp_external_bootstrap_test.sh"], + data = [ + ":system_python_zipapp", + "//python:current_py_toolchain", + ], + env = { + "PYTHON": "$(PYTHON3_ROOTPATH)", + "ZIPAPP": "$(location :system_python_zipapp)", + }, + toolchains = ["//python:current_py_toolchain"], +) + +py_library( + name = "bin_deps", + deps = [ + ":pkgdep", + ":some_dep", + ], +) + +py_library( + name = "some_dep", + srcs = ["some_dep.py"], + experimental_venvs_site_packages = "//python/config_settings:venvs_site_packages", + imports = ["."], +) + +py_library( + name = "pkgdep", + srcs = glob(["site-packages/**"]), + experimental_venvs_site_packages = "//python/config_settings:venvs_site_packages", + imports = ["site-packages"], +) diff --git a/tests/py_zipapp/main.py b/tests/py_zipapp/main.py new file mode 100644 index 0000000000..5770170d2c --- /dev/null +++ b/tests/py_zipapp/main.py @@ -0,0 +1,24 @@ +"A trivial zipapp that prints a message" + + +def main(): + print("Hello from zipapp") + try: + import some_dep + + print(f"dep: {some_dep}") + + import pkgdep.pkgmod + + print(f"dep: {pkgdep.pkgmod}") + except ImportError as e: + import sys + + e.add_note( + "Failed to import a dependency.\n" + "sys.path:\n" + "\n".join(sys.path) + ) + raise + + +if __name__ == "__main__": + main() diff --git a/tests/py_zipapp/site-packages/pkgdep/__init__.py b/tests/py_zipapp/site-packages/pkgdep/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/py_zipapp/site-packages/pkgdep/pkgmod.py b/tests/py_zipapp/site-packages/pkgdep/pkgmod.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/py_zipapp/some_dep.py b/tests/py_zipapp/some_dep.py new file mode 100644 index 0000000000..b64ecfb84a --- /dev/null +++ b/tests/py_zipapp/some_dep.py @@ -0,0 +1 @@ +"""empty module""" diff --git a/tests/py_zipapp/system_python_zipapp_external_bootstrap_test.sh b/tests/py_zipapp/system_python_zipapp_external_bootstrap_test.sh new file mode 100755 index 0000000000..e7396007d9 --- /dev/null +++ b/tests/py_zipapp/system_python_zipapp_external_bootstrap_test.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash + +set -xeuo pipefail + +# This test expects ZIPAPP env var to point to the zipapp file. +if [[ -z "${ZIPAPP:-}" ]]; then + echo "ZIPAPP env var not set" + exit 1 +fi + +# On Windows, the executable file is an exe, and the .zip is a sibling +# output. +ZIPAPP="${ZIPAPP/.exe/.zip}" + +export RULES_PYTHON_BOOTSTRAP_VERBOSE=1 + +# We're testing the invocation of `__main__.py`, so we have to +# manually pass the zipapp to python. +echo "=====================================================================" +echo "Running zipapp using an automatic temp directory..." +echo "=====================================================================" +"$PYTHON" "$ZIPAPP" + +echo +echo + +echo "=====================================================================" +echo "Running zipapp with extract root set..." +echo "=====================================================================" +export RULES_PYTHON_EXTRACT_ROOT="${TEST_TMPDIR:-/tmp}/extract_root_test" +"$PYTHON" "$ZIPAPP" + +# Verify that the directory was created +if [[ ! -d "$RULES_PYTHON_EXTRACT_ROOT" ]]; then + echo "Error: Extract root directory $RULES_PYTHON_EXTRACT_ROOT was not created!" + exit 1 +fi + +# On windows, the path is shortened to just the basename to avoid long path errors. +# Other platforms use the full path. +# Note: [ -d ... ] expands globs, while [[ -d ... ]] does not. +if [ -d "$RULES_PYTHON_EXTRACT_ROOT/_main/tests/py_zipapp/system_python_zipapp"/*/runfiles ]; then + echo "Found runfiles at $RULES_PYTHON_EXTRACT_ROOT/_main/tests/py_zipapp/system_python_zipapp/*/runfiles" +elif [ -d "$RULES_PYTHON_EXTRACT_ROOT/system_python_zipapp"/*/runfiles ]; then + echo "Found runfiles at $RULES_PYTHON_EXTRACT_ROOT/system_python_zipapp/*/runfiles" +else + echo "Error: Could not find 'runfiles' directory" + exit 1 +fi + +echo "=====================================================================" +echo "Running zipapp with extract root set a second time..." +echo "=====================================================================" +"$PYTHON" "$ZIPAPP" diff --git a/tests/py_zipapp/system_python_zipapp_test.py b/tests/py_zipapp/system_python_zipapp_test.py new file mode 100644 index 0000000000..7c3e2deeaf --- /dev/null +++ b/tests/py_zipapp/system_python_zipapp_test.py @@ -0,0 +1,32 @@ +import os +import subprocess +import unittest + + +class SystemPythonZipAppTest(unittest.TestCase): + def test_zipapp_runnable(self): + zipapp_path = os.environ["TEST_ZIPAPP"] + + self.assertTrue(os.path.exists(zipapp_path)) + self.assertTrue(os.path.isfile(zipapp_path)) + + try: + output = ( + subprocess.check_output([zipapp_path], stderr=subprocess.STDOUT) + .decode("utf-8") + .strip() + ) + except subprocess.CalledProcessError as e: + self.fail( + "exit code: {}\n" + " command: {}\n" + "===== stdout/stderr start ==={}===== stdout/stderr end ====".format( + e.returncode, e.cmd, e.output.decode("utf-8") + ) + ) + self.assertIn("Hello from zipapp", output) + self.assertIn("dep:", output) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/py_zipapp/venv_zipapp_test.py b/tests/py_zipapp/venv_zipapp_test.py new file mode 100644 index 0000000000..bd26d533a3 --- /dev/null +++ b/tests/py_zipapp/venv_zipapp_test.py @@ -0,0 +1,109 @@ +import contextlib +import os +import subprocess +import unittest +import zipfile + + +class PyZipAppTest(unittest.TestCase): + def test_zipapp_runnable(self): + zipapp_path = os.environ["TEST_ZIPAPP"] + + try: + output = ( + subprocess.check_output([zipapp_path], stderr=subprocess.STDOUT) + .decode("utf-8") + .strip() + ) + except subprocess.CalledProcessError as e: + self.fail( + ( + "exec failed: {}\n" + + "exit code: {}\n" + + "=== stdout/stderr start ===\n" + "{}\n" + "=== stdout/stderr end ===" + ).format(zipapp_path, e.returncode, e.output.decode("utf-8")) + ) + self.assertIn("Hello from zipapp", output) + self.assertIn("dep:", output) + + def assertHasPathMatchingSuffix(self, namelist, suffix, msg=None): + if not any(name.endswith(suffix) for name in namelist): + self.fail( + (msg or f"No path in zipapp matching suffix '{suffix}'") + + "\nAvailable paths:\n" + + "\n".join(namelist) + ) + + def assertZipEntryIsSymlink(self, zip_file, path, msg=None): + try: + info = zip_file.getinfo(path) + except KeyError: + self.fail(msg or f"Path '{path}' not found in zipfile") + + # S_IFLNK is 0o120000. + # ZipInfo.external_attr is 32 bits: the high 16 bits are Unix attributes. + is_symlink = (info.external_attr >> 16) & 0o170000 == 0o120000 + if not is_symlink: + self.fail(msg or f"Path '{path}' is not a symlink") + + def _is_bzlmod_enabled(self): + return os.environ["BZLMOD_ENABLED"] == "1" + + @contextlib.contextmanager + def _open_zipapp(self, path): + zf = None + try: + try: + zf = zipfile.ZipFile(path, "r") + except zipfile.BadZipFile: + # On windows, the main output is the launcher .exe file, and the + # zip file is a sibling file. + path = path.replace(".exe", ".zip") + zf = zipfile.ZipFile(path, "r") + if zf: + yield zf + finally: + if zf: + zf.close() + + def test_zipapp_structure(self): + zipapp_path = os.environ["TEST_ZIPAPP"] + + with self._open_zipapp(zipapp_path) as zf: + info = zf.infolist()[0] + if os.getenv("COMPRESSED", "0") == "1": + self.assertEqual(info.compress_type, zipfile.ZIP_DEFLATED) + else: + self.assertEqual(info.compress_type, zipfile.ZIP_STORED) + + namelist = zf.namelist() + + if self._is_bzlmod_enabled(): + self.assertIn("runfiles/_repo_mapping", namelist) + + # On Windows, pyvenv.cfg and bin/python3 are generated at runtime. + if os.name != "nt": + self.assertHasPathMatchingSuffix(namelist, "/pyvenv.cfg") + + # The venv directory name depends on the target name, so find it + # by looking for pyvenv.cfg. + venv_config = next( + (name for name in namelist if name.endswith("/pyvenv.cfg")), None + ) + self.assertIsNotNone(venv_config) + + venv_root = os.path.dirname(venv_config) + + # Verify bin/python3 exists and is a symlink + python_bin = f"{venv_root}/bin/python3" + self.assertZipEntryIsSymlink(zf, python_bin) + + # Verify _bazel_site_init.py exists in site-packages + self.assertHasPathMatchingSuffix( + namelist, "/site-packages/_bazel_site_init.py" + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pypi/argparse/BUILD.bazel b/tests/pypi/argparse/BUILD.bazel new file mode 100644 index 0000000000..b04da685e6 --- /dev/null +++ b/tests/pypi/argparse/BUILD.bazel @@ -0,0 +1,3 @@ +load(":argparse_tests.bzl", "argparse_test_suite") + +argparse_test_suite(name = "argparse_tests") diff --git a/tests/pypi/argparse/argparse_tests.bzl b/tests/pypi/argparse/argparse_tests.bzl new file mode 100644 index 0000000000..f8fd1c9481 --- /dev/null +++ b/tests/pypi/argparse/argparse_tests.bzl @@ -0,0 +1,48 @@ +"" + +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("//python/private/pypi:argparse.bzl", "argparse") # buildifier: disable=bzl-visibility + +_tests = [] + +def _test_index_url(env): + env.expect.that_str(argparse.index_url([], "default")).equals("default") + env.expect.that_str(argparse.index_url([], None)).equals(None) + + env.expect.that_str(argparse.index_url(["-i", "https://example.com/simple"], "default")).equals("https://example.com/simple") + env.expect.that_str(argparse.index_url(["--index-url", "https://example.com/simple"], "default")).equals("https://example.com/simple") + env.expect.that_str(argparse.index_url(["--index-url=https://example.com/simple"], "default")).equals("https://example.com/simple") + + env.expect.that_str(argparse.index_url(["--extra-index-url", "https://extra.com", "-i", "https://index.com"], "default")).equals("https://index.com") + +_tests.append(_test_index_url) + +def _test_extra_index_url(env): + env.expect.that_collection(argparse.extra_index_url([], ["default"])).contains_exactly(["default"]) + env.expect.that_collection(argparse.extra_index_url([], None)).contains_exactly([]) + + env.expect.that_collection(argparse.extra_index_url(["--extra-index-url", "https://extra.com/simple"], [])).contains_exactly(["https://extra.com/simple"]) + env.expect.that_collection(argparse.extra_index_url(["--extra-index-url=https://extra.com/simple"], [])).contains_exactly(["https://extra.com/simple"]) + + env.expect.that_collection(argparse.extra_index_url(["--extra-index-url", "https://first.com", "--extra-index-url", "https://second.com"], [])).contains_exactly(["https://first.com", "https://second.com"]) + +_tests.append(_test_extra_index_url) + +def _test_platform(env): + env.expect.that_collection(argparse.platform([], ["default"])).contains_exactly(["default"]) + env.expect.that_collection(argparse.platform([], None)).contains_exactly([]) + + env.expect.that_collection(argparse.platform(["--platform", "manylinux_2_17_x86_64"], [])).contains_exactly(["manylinux_2_17_x86_64"]) + env.expect.that_collection(argparse.platform(["--platform=manylinux_2_17_x86_64"], [])).contains_exactly(["manylinux_2_17_x86_64"]) + + env.expect.that_collection(argparse.platform(["--platform", "macosx_10_9_x86_64", "--platform", "linux_x86_64"], [])).contains_exactly(["macosx_10_9_x86_64", "linux_x86_64"]) + +_tests.append(_test_platform) + +def argparse_test_suite(name): + """Create the test suite. + + Args: + name: the name of the test suite + """ + test_suite(name = name, basic_tests = _tests) diff --git a/tests/pypi/config_settings/config_settings_tests.bzl b/tests/pypi/config_settings/config_settings_tests.bzl index b3e6ada9e8..c7e51b78b4 100644 --- a/tests/pypi/config_settings/config_settings_tests.bzl +++ b/tests/pypi/config_settings/config_settings_tests.bzl @@ -31,15 +31,8 @@ _subject = rule( ) _flag = struct( - platform = lambda x: ("//command_line_option:platforms", str(Label("//tests/support:" + x))), - pip_whl = lambda x: (str(Label("//python/config_settings:pip_whl")), str(x)), - pip_whl_glibc_version = lambda x: (str(Label("//python/config_settings:pip_whl_glibc_version")), str(x)), - pip_whl_muslc_version = lambda x: (str(Label("//python/config_settings:pip_whl_muslc_version")), str(x)), - pip_whl_osx_version = lambda x: (str(Label("//python/config_settings:pip_whl_osx_version")), str(x)), - pip_whl_osx_arch = lambda x: (str(Label("//python/config_settings:pip_whl_osx_arch")), str(x)), - py_linux_libc = lambda x: (str(Label("//python/config_settings:py_linux_libc")), str(x)), + platform = lambda x: ("//command_line_option:platforms", str(Label("//tests/support/platforms:" + x))), python_version = lambda x: (str(Label("//python/config_settings:python_version")), str(x)), - py_freethreaded = lambda x: (str(Label("//python/config_settings:py_freethreaded")), str(x)), ) def _analysis_test(*, name, dist, want, config_settings = [_flag.platform("linux_aarch64")]): diff --git a/tests/pypi/extension/extension_tests.bzl b/tests/pypi/extension/extension_tests.bzl index 0514e1d95b..97849e882d 100644 --- a/tests/pypi/extension/extension_tests.bzl +++ b/tests/pypi/extension/extension_tests.bzl @@ -16,77 +16,107 @@ load("@rules_testing//lib:test_suite.bzl", "test_suite") load("@rules_testing//lib:truth.bzl", "subjects") -load("//python/private/pypi:extension.bzl", "build_config", "parse_modules") # buildifier: disable=bzl-visibility +load("//python/private/pypi:extension.bzl", "build_config", "default_platforms", "parse_modules") # buildifier: disable=bzl-visibility +load("//python/private/pypi:platform.bzl", _plat = "platform") # buildifier: disable=bzl-visibility load("//python/private/pypi:whl_config_setting.bzl", "whl_config_setting") # buildifier: disable=bzl-visibility +load("//tests/support/mocks:mocks.bzl", "mocks") load(":pip_parse.bzl", _parse = "pip_parse") _tests = [] -def _mock_mctx(*modules, environ = {}, read = None): - return struct( - os = struct( - environ = environ, - name = "unittest", - arch = "exotic", - ), - read = read or (lambda _: """\ +def _pypi_mock_mctx(*modules, os_name = "unittest", arch_name = "exotic", environ = {}, read = None, mock_files = {}): + _ = read # @unused + return mocks.mctx( + modules = list(modules), + os_name = os_name, + arch_name = arch_name, + environ = environ, + mock_files = { + "requirements.txt": """\ simple==0.0.1 \ --hash=sha256:deadbeef \ - --hash=sha256:deadbaaf"""), - modules = [ - struct( - name = modules[0].name, - tags = modules[0].tags, - is_root = modules[0].is_root, - ), - ] + [ - struct( - name = mod.name, - tags = mod.tags, - is_root = False, - ) - for mod in modules[1:] + --hash=sha256:deadbaaf""", + } | mock_files, + ) + +def _default( + *, + arch_name = None, + auth_patterns = None, + config_settings = None, + default_hub = "", + env = None, + index_url = None, + marker = None, + netrc = None, + os_name = None, + platform = None, + pyproject_toml = None, + whl_platform_tags = None, + whl_abi_tags = None): + return struct( + arch_name = arch_name, + auth_patterns = auth_patterns or {}, + config_settings = config_settings, + default_hub = default_hub, + env = env or {}, + index_url = index_url or "", + marker = marker or "", + netrc = netrc, + os_name = os_name, + platform = platform, + pyproject_toml = pyproject_toml, + whl_abi_tags = whl_abi_tags or [], + whl_platform_tags = whl_platform_tags or [], + ) + +# The default value for the default platforms tags use in `_mod`. +_default_tags_default = [ + _default( + platform = "{}_{}{}".format(os, cpu, freethreaded), + os_name = os, + arch_name = cpu, + config_settings = [ + "@platforms//os:{}".format(os), + "@platforms//cpu:{}".format(cpu), ], + whl_abi_tags = ["cp{major}{minor}t"] if freethreaded else ["abi3", "cp{major}{minor}"], + whl_platform_tags = whl_platform_tags, ) + for (os, cpu, freethreaded), whl_platform_tags in { + ("linux", "x86_64", ""): ["linux_x86_64", "manylinux_*_x86_64"], + ("linux", "x86_64", "_freethreaded"): ["linux_x86_64", "manylinux_*_x86_64"], + ("linux", "aarch64", ""): ["linux_aarch64", "manylinux_*_aarch64"], + ("osx", "aarch64", ""): ["macosx_*_arm64"], + ("windows", "aarch64", ""): ["win_arm64"], + }.items() +] -def _mod(*, name, default = [], parse = [], override = [], whl_mods = [], is_root = True): +def _dep(*, name, extra_targets = []): + return struct( + name = name, + extra_targets = extra_targets, + ) + +def _mod(*, name, default = _default_tags_default, parse = [], override = [], whl_mods = [], dep = [], is_root = True): return struct( name = name, tags = struct( parse = parse, override = override, whl_mods = whl_mods, - default = default or [ - _default( - platform = "{}_{}{}".format(os, cpu, freethreaded), - os_name = os, - arch_name = cpu, - config_settings = [ - "@platforms//os:{}".format(os), - "@platforms//cpu:{}".format(cpu), - ], - whl_abi_tags = ["cp{major}{minor}t"] if freethreaded else ["abi3", "cp{major}{minor}"], - whl_platform_tags = whl_platform_tags, - ) - for (os, cpu, freethreaded), whl_platform_tags in { - ("linux", "x86_64", ""): ["linux_x86_64", "manylinux_*_x86_64"], - ("linux", "x86_64", "_freethreaded"): ["linux_x86_64", "manylinux_*_x86_64"], - ("linux", "aarch64", ""): ["linux_aarch64", "manylinux_*_aarch64"], - ("osx", "aarch64", ""): ["macosx_*_arm64"], - ("windows", "aarch64", ""): ["win_arm64"], - }.items() - ], + default = default, + dep = dep, ), is_root = is_root, ) -def _parse_modules(env, enable_pipstar = 0, **kwargs): +def _parse_modules(env, **kwargs): return env.expect.that_struct( - parse_modules( - enable_pipstar = enable_pipstar, - **kwargs - ), + parse_modules(**kwargs), attrs = dict( + declared_deps = subjects.dict, + default_hub = subjects.str, exposed_packages = subjects.dict, hub_group_map = subjects.dict, hub_whl_map = subjects.dict, @@ -95,59 +125,36 @@ def _parse_modules(env, enable_pipstar = 0, **kwargs): ), ) -def _build_config(env, enable_pipstar = 0, **kwargs): +def _build_config(env, **kwargs): return env.expect.that_struct( build_config( - enable_pipstar = enable_pipstar, + enable_pipstar_extract = True, **kwargs ), attrs = dict( auth_patterns = subjects.dict, - enable_pipstar = subjects.bool, netrc = subjects.str, platforms = subjects.dict, ), ) -def _default( - *, - arch_name = None, - auth_patterns = None, - config_settings = None, - env = None, - marker = None, - netrc = None, - os_name = None, - platform = None, - whl_platform_tags = None, - whl_abi_tags = None): - return struct( - arch_name = arch_name, - auth_patterns = auth_patterns or {}, - config_settings = config_settings, - env = env or {}, - marker = marker or "", - netrc = netrc, - os_name = os_name, - platform = platform, - whl_abi_tags = whl_abi_tags or [], - whl_platform_tags = whl_platform_tags or [], - ) - def _test_simple(env): pypi = _parse_modules( env, - module_ctx = _mock_mctx( + module_ctx = _pypi_mock_mctx( _mod( name = "rules_python", parse = [ _parse( hub_name = "pypi", python_version = "3.15", + simpleapi_skip = ["simple"], requirements_lock = "requirements.txt", ), ], ), + os_name = "linux", + arch_name = "x86_64", ), available_interpreters = { "python_3_15_host": "unit_test_interpreter_target", @@ -168,6 +175,7 @@ def _test_simple(env): }}) pypi.whl_libraries().contains_exactly({ "pypi_315_simple": { + "config_load": "@pypi//:config.bzl", "dep_template": "@pypi//{name}:{target}", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "simple==0.0.1 --hash=sha256:deadbeef --hash=sha256:deadbaaf", @@ -177,10 +185,115 @@ def _test_simple(env): _tests.append(_test_simple) +def _test_pip_parse_pyproject_toml(env): + # pip.parse() reads the version from pyproject.toml's requires-python when + # python_version is not set explicitly. + pypi = _parse_modules( + env, + module_ctx = _pypi_mock_mctx( + _mod( + name = "rules_python", + parse = [ + _parse( + hub_name = "pypi", + pyproject_toml = "pyproject.toml", + simpleapi_skip = ["simple"], + requirements_lock = "requirements.txt", + ), + ], + ), + os_name = "linux", + arch_name = "x86_64", + mock_files = { + "pyproject.toml": "[project]\nrequires-python = \"==3.15.19\"\n", + }, + ), + available_interpreters = { + "python_3_15_19_host": "unit_test_interpreter_target", + }, + minor_mapping = {"3.15": "3.15.19"}, + ) + + # Resolves identically to passing python_version = "3.15.19" explicitly: + # the full version drives interpreter selection, hub naming uses major.minor. + pypi.exposed_packages().contains_exactly({"pypi": ["simple"]}) + pypi.hub_whl_map().contains_exactly({"pypi": { + "simple": { + "pypi_315_simple": [ + whl_config_setting( + version = "3.15", + ), + ], + }, + }}) + pypi.whl_libraries().contains_exactly({ + "pypi_315_simple": { + "config_load": "@pypi//:config.bzl", + "dep_template": "@pypi//{name}:{target}", + "python_interpreter_target": "unit_test_interpreter_target", + "requirement": "simple==0.0.1 --hash=sha256:deadbeef --hash=sha256:deadbaaf", + }, + }) + +_tests.append(_test_pip_parse_pyproject_toml) + +def _test_simple_isolated(env): + """Simulate `isolate = True` with parse_modules. + + No pip.default tags, but requirements parsing still produces the expected + hub output. + """ + pypi = _parse_modules( + env, + module_ctx = _pypi_mock_mctx( + _mod( + name = "my_module", + default = [], # no platform tags + parse = [ + _parse( + hub_name = "pypi", + python_version = "3.15", + simpleapi_skip = ["simple"], + requirements_lock = "requirements.txt", + ), + ], + ), + os_name = "linux", + arch_name = "x86_64", + ), + available_interpreters = { + "python_3_15_host": "unit_test_interpreter_target", + }, + minor_mapping = {"3.15": "3.15.19"}, + ) + + pypi.exposed_packages().contains_exactly({"pypi": ["simple"]}) + pypi.hub_group_map().contains_exactly({"pypi": {}}) + pypi.hub_whl_map().contains_exactly({"pypi": { + "simple": { + "pypi_315_simple": [ + whl_config_setting( + version = "3.15", + ), + ], + }, + }}) + pypi.whl_libraries().contains_exactly({ + "pypi_315_simple": { + "config_load": "@pypi//:config.bzl", + "dep_template": "@pypi//{name}:{target}", + "python_interpreter_target": "unit_test_interpreter_target", + "requirement": "simple==0.0.1 --hash=sha256:deadbeef --hash=sha256:deadbaaf", + }, + }) + pypi.whl_mods().contains_exactly({}) + +_tests.append(_test_simple_isolated) + def _test_build_pipstar_platform(env): config = _build_config( env, - module_ctx = _mock_mctx( + module_ctx = _pypi_mock_mctx( _mod( name = "rules_python", default = [ @@ -211,11 +324,9 @@ def _test_build_pipstar_platform(env): ], ), ), - enable_pipstar = True, ) config.auth_patterns().contains_exactly({"foo": "bar"}) config.netrc().equals("my_netrc") - config.enable_pipstar().equals(True) config.platforms().contains_exactly({ "myplat": struct( name = "myplat", @@ -230,10 +341,245 @@ def _test_build_pipstar_platform(env): whl_abi_tags = ["none", "abi3", "cp{major}{minor}"], whl_platform_tags = ["any"], ), + } | { + name: _plat(**values) + for name, values in default_platforms().items() }) _tests.append(_test_build_pipstar_platform) +def _test_multiple_default_tags(env): + """Test that multiple pip.default tags do not trigger duplicate default hub failures. + + Only when multiple tags explicitly define default_hub should it fail. + """ + pypi = _parse_modules( + env, + module_ctx = _pypi_mock_mctx( + _mod( + name = "rules_python", + default = _default_tags_default + [ + _default(platform = "extra_custom_platform"), + ], + parse = [ + _parse( + hub_name = "pypi", + python_version = "3.15", + simpleapi_skip = ["simple"], + requirements_lock = "requirements.txt", + ), + ], + ), + os_name = "linux", + arch_name = "x86_64", + ), + available_interpreters = { + "python_3_15_host": "unit_test_interpreter_target", + }, + minor_mapping = {"3.15": "3.15.19"}, + ) + pypi.exposed_packages().contains_exactly({"pypi": ["simple"]}) + +_tests.append(_test_multiple_default_tags) + +def _test_name_collision_no_env(env): + """Test that a hub named 'pypi' is NOT renamed when the env var is not set.""" + pypi = _parse_modules( + env, + module_ctx = _pypi_mock_mctx( + _mod( + name = "rules_python", + parse = [ + _parse( + hub_name = "pypi", + python_version = "3.15", + simpleapi_skip = ["simple"], + requirements_lock = "requirements.txt", + ), + ], + ), + os_name = "linux", + arch_name = "x86_64", + environ = {}, # Env var NOT set + ), + available_interpreters = { + "python_3_15_host": "unit_test_interpreter_target", + }, + minor_mapping = {"3.15": "3.15.19"}, + ) + + # The hub name remains 'pypi' + pypi.exposed_packages().contains_exactly({"pypi": ["simple"]}) + pypi.default_hub().equals(None) + +_tests.append(_test_name_collision_no_env) + +def _test_name_collision_with_env(env): + """Test that a hub named 'pypi' is silently renamed to module_name_pypi and routed as default_hub when the env var is set.""" + pypi = _parse_modules( + env, + module_ctx = _pypi_mock_mctx( + _mod( + name = "rules_python", + parse = [ + _parse( + hub_name = "pypi", + python_version = "3.15", + simpleapi_skip = ["simple"], + requirements_lock = "requirements.txt", + ), + ], + ), + os_name = "linux", + arch_name = "x86_64", + environ = {"RULES_PYTHON_PYPI_HUB_RESERVED": "1"}, + ), + available_interpreters = { + "python_3_15_host": "unit_test_interpreter_target", + }, + minor_mapping = {"3.15": "3.15.19"}, + ) + + # The hub name is renamed to 'rules_python_pypi' + pypi.exposed_packages().contains_exactly({"rules_python_pypi": ["simple"]}) + + # It is used as the default_hub + pypi.default_hub().equals("rules_python_pypi") + +_tests.append(_test_name_collision_with_env) + +def _test_default_hub_precedence(env): + """Test that pip.default(default_hub = ...) has precedence over the fallback renamed default hub.""" + pypi = _parse_modules( + env, + module_ctx = _pypi_mock_mctx( + _mod( + name = "rules_python", + default = _default_tags_default + [ + _default( + platform = "extra_custom_platform", + default_hub = "other_pypi", + ), + ], + parse = [ + _parse( + hub_name = "pypi", + python_version = "3.15", + simpleapi_skip = ["simple"], + requirements_lock = "requirements.txt", + ), + _parse( + hub_name = "other_pypi", + python_version = "3.15", + simpleapi_skip = ["simple"], + requirements_lock = "requirements.txt", + ), + ], + ), + os_name = "linux", + arch_name = "x86_64", + environ = {"RULES_PYTHON_PYPI_HUB_RESERVED": "1"}, + ), + available_interpreters = { + "python_3_15_host": "unit_test_interpreter_target", + }, + minor_mapping = {"3.15": "3.15.19"}, + ) + + # The hub named 'pypi' is renamed to 'rules_python_pypi' + pypi.exposed_packages().contains_exactly({ + "other_pypi": ["simple"], + "rules_python_pypi": ["simple"], + }) + + # But the default_hub remains 'other_pypi' because pip.default has higher precedence! + pypi.default_hub().equals("other_pypi") + +_tests.append(_test_default_hub_precedence) + +def _test_extension_dep(env): + pypi = _parse_modules( + env, + module_ctx = _pypi_mock_mctx( + _mod( + name = "my_module", + dep = [ + _dep( + name = "declared-pkg", + extra_targets = ["declared-alias"], + ), + ], + ), + os_name = "linux", + arch_name = "x86_64", + ), + available_interpreters = {}, + minor_mapping = {}, + ) + + pypi.declared_deps().contains_exactly({"declared_pkg": {"declared-alias": None}}) + pypi.exposed_packages().contains_exactly({}) + pypi.hub_group_map().contains_exactly({}) + pypi.hub_whl_map().contains_exactly({}) + pypi.whl_libraries().contains_exactly({}) + pypi.whl_mods().contains_exactly({}) + +_tests.append(_test_extension_dep) + +def _test_extension_dep_coexists_with_concrete_hub(env): + pypi = _parse_modules( + env, + module_ctx = _pypi_mock_mctx( + _mod( + name = "my_module", + parse = [ + _parse( + hub_name = "pypi_a", + python_version = "3.15", + simpleapi_skip = ["simple"], + requirements_lock = "requirements.txt", + ), + ], + dep = [ + _dep( + name = "simple", + extra_targets = ["extra-target"], + ), + ], + ), + os_name = "linux", + arch_name = "x86_64", + ), + available_interpreters = { + "python_3_15_host": "unit_test_interpreter_target", + }, + minor_mapping = {"3.15": "3.15.19"}, + ) + + pypi.declared_deps().contains_exactly({"simple": {"extra-target": None}}) + pypi.exposed_packages().contains_exactly({"pypi_a": ["simple"]}) + pypi.hub_group_map().contains_exactly({"pypi_a": {}}) + pypi.hub_whl_map().contains_exactly({"pypi_a": { + "simple": { + "pypi_a_315_simple": [ + whl_config_setting( + version = "3.15", + ), + ], + }, + }}) + pypi.whl_libraries().contains_exactly({ + "pypi_a_315_simple": { + "config_load": "@pypi_a//:config.bzl", + "dep_template": "@pypi_a//{name}:{target}", + "python_interpreter_target": "unit_test_interpreter_target", + "requirement": "simple==0.0.1 --hash=sha256:deadbeef --hash=sha256:deadbaaf", + }, + }) + pypi.whl_mods().contains_exactly({}) + +_tests.append(_test_extension_dep_coexists_with_concrete_hub) + def extension_test_suite(name): """Create the test suite. diff --git a/tests/pypi/extension/pip_parse.bzl b/tests/pypi/extension/pip_parse.bzl index 21569cf04e..7b5bdfdfd6 100644 --- a/tests/pypi/extension/pip_parse.bzl +++ b/tests/pypi/extension/pip_parse.bzl @@ -3,22 +3,23 @@ def pip_parse( *, hub_name, - python_version, + python_version = None, add_libdir_to_library_search_path = False, auth_patterns = {}, download_only = False, enable_implicit_namespace_pkgs = False, environment = {}, envsubst = {}, + experimental_extra_index_urls = [], experimental_index_url = "", experimental_requirement_cycles = {}, - experimental_target_platforms = [], extra_hub_aliases = {}, extra_pip_args = [], isolated = True, netrc = None, parse_all_requirements_files = True, pip_data_exclude = None, + pyproject_toml = None, python_interpreter = None, python_interpreter_target = None, quiet = True, @@ -27,8 +28,10 @@ def pip_parse( requirements_linux = None, requirements_lock = None, requirements_windows = None, + target_platforms = [], simpleapi_skip = [], timeout = 600, + uv_lock = None, whl_modifications = {}, **kwargs): """A simple helper for testing to simulate the PyPI extension parse tag class""" @@ -39,9 +42,10 @@ def pip_parse( enable_implicit_namespace_pkgs = enable_implicit_namespace_pkgs, environment = environment, envsubst = envsubst, + experimental_extra_index_urls = experimental_extra_index_urls, experimental_index_url = experimental_index_url, experimental_requirement_cycles = experimental_requirement_cycles, - experimental_target_platforms = experimental_target_platforms, + target_platforms = target_platforms, extra_hub_aliases = extra_hub_aliases, extra_pip_args = extra_pip_args, hub_name = hub_name, @@ -49,6 +53,7 @@ def pip_parse( netrc = netrc, parse_all_requirements_files = parse_all_requirements_files, pip_data_exclude = pip_data_exclude, + pyproject_toml = pyproject_toml, python_interpreter = python_interpreter, python_interpreter_target = python_interpreter_target, python_version = python_version, @@ -59,12 +64,10 @@ def pip_parse( requirements_lock = requirements_lock, requirements_windows = requirements_windows, timeout = timeout, + uv_lock = uv_lock, whl_modifications = whl_modifications, - # The following are covered by other unit tests - experimental_extra_index_urls = [], parallel_download = False, experimental_index_url_overrides = {}, simpleapi_skip = simpleapi_skip, - _evaluate_markers_srcs = [], **kwargs ) diff --git a/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl b/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl index 225b296ebf..1fd99205b1 100644 --- a/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl +++ b/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl @@ -19,13 +19,21 @@ load("//python/private/pypi:generate_whl_library_build_bazel.bzl", "generate_whl _tests = [] -def _test_all_legacy(env): +def _test_all_workspace(env): want = """\ -load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets") +load("@package_metadata//rules:package_metadata.bzl", "package_metadata") +load("@pypi//:config.bzl", "packages") +load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets_from_requires") package(default_visibility = ["//visibility:public"]) -whl_library_targets( +package_metadata( + name = "package_metadata", + purl = None, + visibility = ["//:__subpackages__"], +) + +whl_library_targets_from_requires( copy_executables = { "exec_src": "exec_dest", }, @@ -38,22 +46,20 @@ whl_library_targets( "data_exclude_all", ], dep_template = "@pypi//{name}:{target}", - dependencies = ["foo"], - dependencies_by_platform = { - "baz": ["bar"], - }, - entry_points = { - "foo": "bar.py", - }, group_deps = [ "foo", "fox", "qux", ], group_name = "qux", + include = packages, name = "foo.whl", + requires_dist = [ + "foo", + "bar-baz", + "qux", + ], srcs_exclude = ["srcs_exclude_all"], - tags = ["tag1"], ) # SOMETHING SPECIAL AT THE END @@ -61,11 +67,7 @@ whl_library_targets( actual = generate_whl_library_build_bazel( dep_template = "@pypi//{name}:{target}", name = "foo.whl", - dependencies = ["foo"], - dependencies_by_platform = {"baz": ["bar"]}, - entry_points = { - "foo": "bar.py", - }, + requires_dist = ["foo", "bar-baz", "qux"], data_exclude = ["exclude_via_attr"], annotation = struct( copy_files = {"file_src": "file_dest"}, @@ -75,21 +77,28 @@ whl_library_targets( srcs_exclude_glob = ["srcs_exclude_all"], additive_build_content = """# SOMETHING SPECIAL AT THE END""", ), + config_load = "@pypi//:config.bzl", group_name = "qux", group_deps = ["foo", "fox", "qux"], - tags = ["tag1"], ) env.expect.that_str(actual.replace("@@", "@")).equals(want) -_tests.append(_test_all_legacy) +_tests.append(_test_all_workspace) def _test_all(env): want = """\ -load("@pypi//:config.bzl", "whl_map") +load("@package_metadata//rules:package_metadata.bzl", "package_metadata") +load("@pypi//:config.bzl", "packages") load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets_from_requires") package(default_visibility = ["//visibility:public"]) +package_metadata( + name = "package_metadata", + purl = None, + visibility = ["//:__subpackages__"], +) + whl_library_targets_from_requires( copy_executables = { "exec_src": "exec_dest", @@ -103,16 +112,13 @@ whl_library_targets_from_requires( "data_exclude_all", ], dep_template = "@pypi//{name}:{target}", - entry_points = { - "foo": "bar.py", - }, group_deps = [ "foo", "fox", "qux", ], group_name = "qux", - include = whl_map, + include = packages, name = "foo.whl", requires_dist = [ "foo", @@ -128,9 +134,6 @@ whl_library_targets_from_requires( dep_template = "@pypi//{name}:{target}", name = "foo.whl", requires_dist = ["foo", "bar-baz", "qux"], - entry_points = { - "foo": "bar.py", - }, data_exclude = ["exclude_via_attr"], annotation = struct( copy_files = {"file_src": "file_dest"}, @@ -140,6 +143,7 @@ whl_library_targets_from_requires( srcs_exclude_glob = ["srcs_exclude_all"], additive_build_content = """# SOMETHING SPECIAL AT THE END""", ), + config_load = "@pypi//:config.bzl", group_name = "qux", group_deps = ["foo", "fox", "qux"], ) @@ -149,11 +153,18 @@ _tests.append(_test_all) def _test_all_with_loads(env): want = """\ -load("@pypi//:config.bzl", "whl_map") +load("@package_metadata//rules:package_metadata.bzl", "package_metadata") +load("@pypi//:config.bzl", "packages") load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets_from_requires") package(default_visibility = ["//visibility:public"]) +package_metadata( + name = "package_metadata", + purl = None, + visibility = ["//:__subpackages__"], +) + whl_library_targets_from_requires( copy_executables = { "exec_src": "exec_dest", @@ -167,16 +178,13 @@ whl_library_targets_from_requires( "data_exclude_all", ], dep_template = "@pypi//{name}:{target}", - entry_points = { - "foo": "bar.py", - }, group_deps = [ "foo", "fox", "qux", ], group_name = "qux", - include = whl_map, + include = packages, name = "foo.whl", requires_dist = [ "foo", @@ -192,9 +200,6 @@ whl_library_targets_from_requires( dep_template = "@pypi//{name}:{target}", name = "foo.whl", requires_dist = ["foo", "bar-baz", "qux"], - entry_points = { - "foo": "bar.py", - }, data_exclude = ["exclude_via_attr"], annotation = struct( copy_files = {"file_src": "file_dest"}, @@ -205,6 +210,7 @@ whl_library_targets_from_requires( additive_build_content = """# SOMETHING SPECIAL AT THE END""", ), group_name = "qux", + config_load = "@pypi//:config.bzl", group_deps = ["foo", "fox", "qux"], ) env.expect.that_str(actual.replace("@@", "@")).equals(want) diff --git a/tests/pypi/hub_builder/hub_builder_tests.bzl b/tests/pypi/hub_builder/hub_builder_tests.bzl index 9f6ee6720d..60017593fb 100644 --- a/tests/pypi/hub_builder/hub_builder_tests.bzl +++ b/tests/pypi/hub_builder/hub_builder_tests.bzl @@ -20,39 +20,43 @@ load("//python/private:repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "REPO_VERBOSITY_EN load("//python/private/pypi:hub_builder.bzl", _hub_builder = "hub_builder") # buildifier: disable=bzl-visibility load("//python/private/pypi:parse_simpleapi_html.bzl", "parse_simpleapi_html") # buildifier: disable=bzl-visibility load("//python/private/pypi:platform.bzl", _plat = "platform") # buildifier: disable=bzl-visibility +load("//python/private/pypi:simpleapi_download.bzl", "simpleapi_download") # buildifier: disable=bzl-visibility load("//python/private/pypi:whl_config_setting.bzl", "whl_config_setting") # buildifier: disable=bzl-visibility load("//tests/pypi/extension:pip_parse.bzl", _parse = "pip_parse") +load("//tests/support/mocks:mocks.bzl", "mocks") _tests = [] -def _mock_mctx(environ = {}, read = None): - return struct( - os = struct( - environ = environ, - name = "unittest", - arch = "exotic", - ), - read = read or (lambda _: """\ +def _mock_mctx(os_name = "unittest", arch_name = "exotic", environ = {}, mock_files = None): + return mocks.mctx( + os_name = os_name, + arch_name = arch_name, + environ = environ, + mock_files = mock_files or { + "requirements.txt": """\ simple==0.0.1 \ --hash=sha256:deadbeef \ - --hash=sha256:deadbaaf"""), + --hash=sha256:deadbaaf""", + }, ) def hub_builder( env, - enable_pipstar = False, + enable_pipstar_extract = True, debug = False, config = None, minor_mapping = {}, - evaluate_markers_fn = None, + whl_overrides = {}, simpleapi_download_fn = None, + log_printer = None, available_interpreters = {}): builder = _hub_builder( name = "pypi", module_name = "unit_test", config = config or struct( # no need to evaluate the markers with the interpreter - enable_pipstar = enable_pipstar, + enable_pipstar_extract = enable_pipstar_extract, + index_url = "https://pypi.org/simple", platforms = { "{}_{}{}".format(os, cpu, freethreaded): _plat( name = "{}_{}{}".format(os, cpu, freethreaded), @@ -76,24 +80,23 @@ def hub_builder( netrc = None, auth_patterns = None, ), - whl_overrides = {}, + whl_overrides = whl_overrides, minor_mapping = minor_mapping or {"3.15": "3.15.19"}, available_interpreters = available_interpreters or { "python_3_15_host": "unit_test_interpreter_target", }, simpleapi_download_fn = simpleapi_download_fn or (lambda *a, **k: {}), - evaluate_markers_fn = evaluate_markers_fn, logger = repo_utils.logger( struct( - os = struct( - environ = { - REPO_DEBUG_ENV_VAR: "1", - REPO_VERBOSITY_ENV_VAR: "TRACE" if debug else "FAIL", - }, - ), + getenv = { + REPO_DEBUG_ENV_VAR: "1", + REPO_VERBOSITY_ENV_VAR: "TRACE" if debug else "FAIL", + }.get, ), "unit-test", + printer = log_printer, ), + simpleapi_cache = {}, ) self = struct( build = lambda: env.expect.that_struct( @@ -113,7 +116,10 @@ def hub_builder( def _test_simple(env): builder = hub_builder(env) builder.pip_parse( - _mock_mctx(), + _mock_mctx( + os_name = "osx", + arch_name = "aarch64", + ), _parse( hub_name = "pypi", python_version = "3.15", @@ -135,6 +141,7 @@ def _test_simple(env): }) pypi.whl_libraries().contains_exactly({ "pypi_315_simple": { + "config_load": "@pypi//:config.bzl", "dep_template": "@pypi//{name}:{target}", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "simple==0.0.1 --hash=sha256:deadbeef --hash=sha256:deadbaaf", @@ -145,19 +152,137 @@ def _test_simple(env): _tests.append(_test_simple) def _test_simple_multiple_requirements(env): - builder = hub_builder(env) + sub_tests = { + ("osx", "aarch64"): "simple==0.0.2 --hash=sha256:deadb00f", + ("windows", "aarch64"): "simple==0.0.1 --hash=sha256:deadbeef", + } + for (host_os, host_arch), want_requirement in sub_tests.items(): + builder = hub_builder(env) + builder.pip_parse( + mocks.mctx( + mock_files = { + "darwin.txt": "simple==0.0.2 --hash=sha256:deadb00f", + "win.txt": "simple==0.0.1 --hash=sha256:deadbeef", + }, + os_name = host_os, + arch_name = host_arch, + ), + _parse( + hub_name = "pypi", + python_version = "3.15", + requirements_darwin = "darwin.txt", + requirements_windows = "win.txt", + ), + ) + pypi = builder.build() + + pypi.exposed_packages().contains_exactly(["simple"]) + pypi.group_map().contains_exactly({}) + pypi.whl_map().contains_exactly({ + "simple": { + "pypi_315_simple": [ + whl_config_setting(version = "3.15"), + ], + }, + }) + pypi.whl_libraries().contains_exactly({ + "pypi_315_simple": { + "config_load": "@pypi//:config.bzl", + "dep_template": "@pypi//{name}:{target}", + "python_interpreter_target": "unit_test_interpreter_target", + "requirement": want_requirement, + }, + }) + pypi.extra_aliases().contains_exactly({}) + +_tests.append(_test_simple_multiple_requirements) + +def _test_simple_extras_vs_no_extras(env): + sub_tests = { + ("osx", "aarch64"): "simple[foo]==0.0.1 --hash=sha256:deadbeef", + ("windows", "aarch64"): "simple==0.0.1 --hash=sha256:deadbeef", + } + for (host_os, host_arch), want_requirement in sub_tests.items(): + builder = hub_builder(env) + builder.pip_parse( + mocks.mctx( + mock_files = { + "darwin.txt": "simple[foo]==0.0.1 --hash=sha256:deadbeef", + "win.txt": "simple==0.0.1 --hash=sha256:deadbeef", + }, + os_name = host_os, + arch_name = host_arch, + ), + _parse( + hub_name = "pypi", + python_version = "3.15", + requirements_darwin = "darwin.txt", + requirements_windows = "win.txt", + ), + ) + pypi = builder.build() + + pypi.exposed_packages().contains_exactly(["simple"]) + pypi.group_map().contains_exactly({}) + pypi.whl_map().contains_exactly({ + "simple": { + "pypi_315_simple": [ + whl_config_setting(version = "3.15"), + ], + }, + }) + pypi.whl_libraries().contains_exactly({ + "pypi_315_simple": { + "config_load": "@pypi//:config.bzl", + "dep_template": "@pypi//{name}:{target}", + "python_interpreter_target": "unit_test_interpreter_target", + "requirement": want_requirement, + }, + }) + pypi.extra_aliases().contains_exactly({}) + +_tests.append(_test_simple_extras_vs_no_extras) + +def _test_simple_extras_vs_no_extras_simpleapi(env): + def mockread_simpleapi(*_, parse_index, **__): + if parse_index: + content = """\ + simple-0.0.1-py3-none-any.whl
+""" + return struct( + output = parse_simpleapi_html( + content = content, + parse_index = parse_index, + ), + success = True, + ) + + builder = hub_builder( + env, + simpleapi_download_fn = lambda *args, **kwargs: simpleapi_download( + read_simpleapi = mockread_simpleapi, + *args, + **kwargs + ), + ) builder.pip_parse( - _mock_mctx( - read = lambda x: { - "darwin.txt": "simple==0.0.2 --hash=sha256:deadb00f", + mocks.mctx( + mock_files = { + "darwin.txt": "simple[foo]==0.0.1 --hash=sha256:deadbeef", "win.txt": "simple==0.0.1 --hash=sha256:deadbeef", - }[x], + }, ), _parse( hub_name = "pypi", python_version = "3.15", requirements_darwin = "darwin.txt", requirements_windows = "win.txt", + experimental_index_url = "https://example.com", + target_platforms = ["osx_aarch64", "windows_aarch64"], ), ) pypi = builder.build() @@ -166,7 +291,7 @@ def _test_simple_multiple_requirements(env): pypi.group_map().contains_exactly({}) pypi.whl_map().contains_exactly({ "simple": { - "pypi_315_simple_osx_aarch64": [ + "pypi_315_simple_py3_none_any_deadbeef_osx_aarch64": [ whl_config_setting( target_platforms = [ "cp315_osx_aarch64", @@ -174,7 +299,7 @@ def _test_simple_multiple_requirements(env): version = "3.15", ), ], - "pypi_315_simple_windows_aarch64": [ + "pypi_315_simple_py3_none_any_deadbeef_windows_aarch64": [ whl_config_setting( target_platforms = [ "cp315_windows_aarch64", @@ -185,20 +310,28 @@ def _test_simple_multiple_requirements(env): }, }) pypi.whl_libraries().contains_exactly({ - "pypi_315_simple_osx_aarch64": { + "pypi_315_simple_py3_none_any_deadbeef_osx_aarch64": { + "config_load": "@pypi//:config.bzl", "dep_template": "@pypi//{name}:{target}", - "python_interpreter_target": "unit_test_interpreter_target", - "requirement": "simple==0.0.2 --hash=sha256:deadb00f", + "filename": "simple-0.0.1-py3-none-any.whl", + "index_url": "https://example.com/simple/", + "requirement": "simple[foo]==0.0.1", + "sha256": "deadbeef", + "urls": ["/simple-0.0.1-py3-none-any.whl"], }, - "pypi_315_simple_windows_aarch64": { + "pypi_315_simple_py3_none_any_deadbeef_windows_aarch64": { + "config_load": "@pypi//:config.bzl", "dep_template": "@pypi//{name}:{target}", - "python_interpreter_target": "unit_test_interpreter_target", - "requirement": "simple==0.0.1 --hash=sha256:deadbeef", + "filename": "simple-0.0.1-py3-none-any.whl", + "index_url": "https://example.com/simple/", + "requirement": "simple==0.0.1", + "sha256": "deadbeef", + "urls": ["/simple-0.0.1-py3-none-any.whl"], }, }) pypi.extra_aliases().contains_exactly({}) -_tests.append(_test_simple_multiple_requirements) +_tests.append(_test_simple_extras_vs_no_extras_simpleapi) def _test_simple_multiple_python_versions(env): builder = hub_builder( @@ -213,13 +346,15 @@ def _test_simple_multiple_python_versions(env): }, ) builder.pip_parse( - _mock_mctx( - read = lambda x: { + mocks.mctx( + mock_files = { "requirements_3_15.txt": """ simple==0.0.1 --hash=sha256:deadbeef old-package==0.0.1 --hash=sha256:deadbaaf """, - }[x], + }, + os_name = "linux", + arch_name = "amd64", ), _parse( hub_name = "pypi", @@ -228,13 +363,15 @@ old-package==0.0.1 --hash=sha256:deadbaaf ), ) builder.pip_parse( - _mock_mctx( - read = lambda x: { + mocks.mctx( + mock_files = { "requirements_3_16.txt": """ simple==0.0.2 --hash=sha256:deadb00f new-package==0.0.1 --hash=sha256:deadb00f2 """, - }[x], + }, + os_name = "linux", + arch_name = "amd64", ), _parse( hub_name = "pypi", @@ -249,48 +386,44 @@ new-package==0.0.1 --hash=sha256:deadb00f2 pypi.whl_map().contains_exactly({ "new_package": { "pypi_316_new_package": [ - whl_config_setting( - version = "3.16", - ), + whl_config_setting(version = "3.16"), ], }, "old_package": { "pypi_315_old_package": [ - whl_config_setting( - version = "3.15", - ), + whl_config_setting(version = "3.15"), ], }, "simple": { "pypi_315_simple": [ - whl_config_setting( - version = "3.15", - ), + whl_config_setting(version = "3.15"), ], "pypi_316_simple": [ - whl_config_setting( - version = "3.16", - ), + whl_config_setting(version = "3.16"), ], }, }) pypi.whl_libraries().contains_exactly({ "pypi_315_old_package": { + "config_load": "@pypi//:config.bzl", "dep_template": "@pypi//{name}:{target}", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "old-package==0.0.1 --hash=sha256:deadbaaf", }, "pypi_315_simple": { + "config_load": "@pypi//:config.bzl", "dep_template": "@pypi//{name}:{target}", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "simple==0.0.1 --hash=sha256:deadbeef", }, "pypi_316_new_package": { + "config_load": "@pypi//:config.bzl", "dep_template": "@pypi//{name}:{target}", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "new-package==0.0.1 --hash=sha256:deadb00f2", }, "pypi_316_simple": { + "config_load": "@pypi//:config.bzl", "dep_template": "@pypi//{name}:{target}", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "simple==0.0.2 --hash=sha256:deadb00f", @@ -301,82 +434,63 @@ new-package==0.0.1 --hash=sha256:deadb00f2 _tests.append(_test_simple_multiple_python_versions) def _test_simple_with_markers(env): - builder = hub_builder( - env, - evaluate_markers_fn = lambda _, requirements, **__: { - key: [ - platform - for platform in platforms - if ("x86_64" in platform and "platform_machine ==" in key) or ("x86_64" not in platform and "platform_machine !=" in key) - ] - for key, platforms in requirements.items() - }, - ) - builder.pip_parse( - _mock_mctx( - read = lambda x: { - "universal.txt": """\ -torch==2.4.1+cpu ; platform_machine == 'x86_64' -torch==2.4.1 ; platform_machine != 'x86_64' \ - --hash=sha256:deadbeef -""", - }[x], - ), - _parse( - hub_name = "pypi", - python_version = "3.15", - requirements_lock = "universal.txt", - ), - ) - pypi = builder.build() + sub_tests = { + ("osx", "aarch64"): "torch==2.4.1 --hash=sha256:deadbeef", + ("linux", "x86_64"): "torch==2.4.1+cpu", + } + for (host_os, host_arch), want_requirement in sub_tests.items(): + builder = hub_builder(env) + builder.pip_parse( + mocks.mctx( + mock_files = { + "universal.txt": """\ + torch==2.4.1+cpu ; platform_machine == 'x86_64' + torch==2.4.1 ; platform_machine != 'x86_64' \ + --hash=sha256:deadbeef + """, + }, + os_name = host_os, + arch_name = host_arch, + ), + _parse( + hub_name = "pypi", + python_version = "3.15", + requirements_lock = "universal.txt", + ), + ) + pypi = builder.build() - pypi.exposed_packages().contains_exactly(["torch"]) - pypi.group_map().contains_exactly({}) - pypi.whl_map().contains_exactly({ - "torch": { - "pypi_315_torch_linux_aarch64_osx_aarch64_windows_aarch64": [ - whl_config_setting( - target_platforms = [ - "cp315_linux_aarch64", - "cp315_osx_aarch64", - "cp315_windows_aarch64", - ], - version = "3.15", - ), - ], - "pypi_315_torch_linux_x86_64_linux_x86_64_freethreaded": [ - whl_config_setting( - target_platforms = [ - "cp315_linux_x86_64", - "cp315_linux_x86_64_freethreaded", - ], - version = "3.15", - ), - ], - }, - }) - pypi.whl_libraries().contains_exactly({ - "pypi_315_torch_linux_aarch64_osx_aarch64_windows_aarch64": { - "dep_template": "@pypi//{name}:{target}", - "python_interpreter_target": "unit_test_interpreter_target", - "requirement": "torch==2.4.1 --hash=sha256:deadbeef", - }, - "pypi_315_torch_linux_x86_64_linux_x86_64_freethreaded": { - "dep_template": "@pypi//{name}:{target}", - "python_interpreter_target": "unit_test_interpreter_target", - "requirement": "torch==2.4.1+cpu", - }, - }) - pypi.extra_aliases().contains_exactly({}) + pypi.exposed_packages().contains_exactly(["torch"]) + pypi.group_map().contains_exactly({}) + pypi.whl_map().contains_exactly({ + "torch": { + "pypi_315_torch": [ + whl_config_setting( + version = "3.15", + ), + ], + }, + }) + pypi.whl_libraries().contains_exactly({ + "pypi_315_torch": { + "config_load": "@pypi//:config.bzl", + "dep_template": "@pypi//{name}:{target}", + "python_interpreter_target": "unit_test_interpreter_target", + "requirement": want_requirement, + }, + }) + pypi.extra_aliases().contains_exactly({}) _tests.append(_test_simple_with_markers) def _test_torch_experimental_index_url(env): - def mocksimpleapi_download(*_, **__): - return { - "torch": parse_simpleapi_html( - url = "https://torch.index", - content = """\ + def mockread_simpleapi(*_, parse_index, **__): + if parse_index: + content = """\ + torch +""" + else: + content = """\ torch-2.4.1+cpu-cp310-cp310-linux_x86_64.whl
torch-2.4.1+cpu-cp310-cp310-win_amd64.whl
torch-2.4.1+cpu-cp311-cp311-linux_x86_64.whl
@@ -397,15 +511,22 @@ def _test_torch_experimental_index_url(env): torch-2.4.1-cp38-none-macosx_11_0_arm64.whl
torch-2.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
torch-2.4.1-cp39-none-macosx_11_0_arm64.whl
-""", +""" + + return struct( + output = parse_simpleapi_html( + content = content, + parse_index = parse_index, ), - } + success = True, + ) builder = hub_builder( env, config = struct( netrc = None, - enable_pipstar = False, + enable_pipstar_extract = True, + index_url = "https://pypi.org/simple", auth_patterns = {}, platforms = { "{}_{}".format(os, cpu): _plat( @@ -421,6 +542,9 @@ def _test_torch_experimental_index_url(env): for (os, cpu), whl_platform_tags in { ("linux", "x86_64"): ["linux_x86_64", "manylinux_*_x86_64"], ("linux", "aarch64"): ["linux_aarch64", "manylinux_*_aarch64"], + # this should be ignored as well because there is no sdist and no whls + # for intel Macs + ("osx", "x86_64"): ["macosx_*_x86_64"], ("osx", "aarch64"): ["macosx_*_arm64"], ("windows", "x86_64"): ["win_amd64"], ("windows", "aarch64"): ["win_arm64"], # this should be ignored @@ -431,20 +555,15 @@ def _test_torch_experimental_index_url(env): "python_3_12_host": "unit_test_interpreter_target", }, minor_mapping = {"3.12": "3.12.19"}, - evaluate_markers_fn = lambda _, requirements, **__: { - # todo once 2692 is merged, this is going to be easier to test. - key: [ - platform - for platform in platforms - if ("x86_64" in platform and "platform_machine ==" in key) or ("x86_64" not in platform and "platform_machine !=" in key) - ] - for key, platforms in requirements.items() - }, - simpleapi_download_fn = mocksimpleapi_download, + simpleapi_download_fn = lambda *args, **kwargs: simpleapi_download( + read_simpleapi = mockread_simpleapi, + *args, + **kwargs + ), ) builder.pip_parse( - _mock_mctx( - read = lambda x: { + mocks.mctx( + mock_files = { "universal.txt": """\ torch==2.4.1 ; platform_machine != 'x86_64' \ --hash=sha256:1495132f30f722af1a091950088baea383fe39903db06b20e6936fd99402803e \ @@ -471,14 +590,23 @@ torch==2.4.1+cpu ; platform_machine == 'x86_64' \ --hash=sha256:c4f2c3c026e876d4dad7629170ec14fff48c076d6c2ae0e354ab3fdc09024f00 # via -r requirements.in """, - }[x], + }, ), _parse( hub_name = "pypi", python_version = "3.12", - download_only = True, experimental_index_url = "https://torch.index", requirements_lock = "universal.txt", + target_platforms = [ + "linux_x86_64", + "linux_aarch64", + # this should be ignored as well because there is no sdist and no whls + # for intel Macs + "osx_x86_64", + "osx_aarch64", + "windows_x86_64", + "windows_aarch64", + ], ), ) pypi = builder.build() @@ -487,79 +615,242 @@ torch==2.4.1+cpu ; platform_machine == 'x86_64' \ pypi.group_map().contains_exactly({}) pypi.whl_map().contains_exactly({ "torch": { - "pypi_312_torch_cp312_cp312_linux_x86_64_8800deef": [ + "pypi_312_torch_cp312_cp312_linux_x86_64_8800deef_linux_x86_64": [ whl_config_setting( - target_platforms = ("cp312_linux_x86_64",), + target_platforms = ["cp312_linux_x86_64"], version = "3.12", ), ], - "pypi_312_torch_cp312_cp312_manylinux_2_17_aarch64_36109432": [ + "pypi_312_torch_cp312_cp312_manylinux_2_17_aarch64_36109432_linux_aarch64": [ whl_config_setting( - target_platforms = ("cp312_linux_aarch64",), + target_platforms = ["cp312_linux_aarch64"], version = "3.12", ), ], - "pypi_312_torch_cp312_cp312_win_amd64_3a570e5c": [ + "pypi_312_torch_cp312_cp312_win_amd64_3a570e5c_windows_x86_64": [ whl_config_setting( - target_platforms = ("cp312_windows_x86_64",), + target_platforms = ["cp312_windows_x86_64"], version = "3.12", ), ], - "pypi_312_torch_cp312_none_macosx_11_0_arm64_72b484d5": [ + "pypi_312_torch_cp312_none_macosx_11_0_arm64_72b484d5_osx_aarch64": [ whl_config_setting( - target_platforms = ("cp312_osx_aarch64",), + target_platforms = ["cp312_osx_aarch64"], version = "3.12", ), ], }, }) pypi.whl_libraries().contains_exactly({ - "pypi_312_torch_cp312_cp312_linux_x86_64_8800deef": { + "pypi_312_torch_cp312_cp312_linux_x86_64_8800deef_linux_x86_64": { + "config_load": "@pypi//:config.bzl", "dep_template": "@pypi//{name}:{target}", - "experimental_target_platforms": ["linux_x86_64"], "filename": "torch-2.4.1+cpu-cp312-cp312-linux_x86_64.whl", - "python_interpreter_target": "unit_test_interpreter_target", + "index_url": "https://torch.index/torch/", "requirement": "torch==2.4.1+cpu", "sha256": "8800deef0026011d502c0c256cc4b67d002347f63c3a38cd8e45f1f445c61364", - "urls": ["https://torch.index/whl/cpu/torch-2.4.1%2Bcpu-cp312-cp312-linux_x86_64.whl"], + "urls": ["/whl/cpu/torch-2.4.1%2Bcpu-cp312-cp312-linux_x86_64.whl"], }, - "pypi_312_torch_cp312_cp312_manylinux_2_17_aarch64_36109432": { + "pypi_312_torch_cp312_cp312_manylinux_2_17_aarch64_36109432_linux_aarch64": { + "config_load": "@pypi//:config.bzl", "dep_template": "@pypi//{name}:{target}", - "experimental_target_platforms": ["linux_aarch64"], "filename": "torch-2.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", - "python_interpreter_target": "unit_test_interpreter_target", + "index_url": "https://torch.index/torch/", "requirement": "torch==2.4.1", "sha256": "36109432b10bd7163c9b30ce896f3c2cca1b86b9765f956a1594f0ff43091e2a", - "urls": ["https://torch.index/whl/cpu/torch-2.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"], + "urls": ["/whl/cpu/torch-2.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"], }, - "pypi_312_torch_cp312_cp312_win_amd64_3a570e5c": { + "pypi_312_torch_cp312_cp312_win_amd64_3a570e5c_windows_x86_64": { + "config_load": "@pypi//:config.bzl", "dep_template": "@pypi//{name}:{target}", - "experimental_target_platforms": ["windows_x86_64"], "filename": "torch-2.4.1+cpu-cp312-cp312-win_amd64.whl", - "python_interpreter_target": "unit_test_interpreter_target", + "index_url": "https://torch.index/torch/", "requirement": "torch==2.4.1+cpu", "sha256": "3a570e5c553415cdbddfe679207327b3a3806b21c6adea14fba77684d1619e97", - "urls": ["https://torch.index/whl/cpu/torch-2.4.1%2Bcpu-cp312-cp312-win_amd64.whl"], + "urls": ["/whl/cpu/torch-2.4.1%2Bcpu-cp312-cp312-win_amd64.whl"], }, - "pypi_312_torch_cp312_none_macosx_11_0_arm64_72b484d5": { + "pypi_312_torch_cp312_none_macosx_11_0_arm64_72b484d5_osx_aarch64": { + "config_load": "@pypi//:config.bzl", "dep_template": "@pypi//{name}:{target}", - "experimental_target_platforms": ["osx_aarch64"], "filename": "torch-2.4.1-cp312-none-macosx_11_0_arm64.whl", - "python_interpreter_target": "unit_test_interpreter_target", + "index_url": "https://torch.index/torch/", "requirement": "torch==2.4.1", "sha256": "72b484d5b6cec1a735bf3fa5a1c4883d01748698c5e9cfdbeb4ffab7c7987e0d", - "urls": ["https://torch.index/whl/cpu/torch-2.4.1-cp312-none-macosx_11_0_arm64.whl"], + "urls": ["/whl/cpu/torch-2.4.1-cp312-none-macosx_11_0_arm64.whl"], }, }) pypi.extra_aliases().contains_exactly({}) _tests.append(_test_torch_experimental_index_url) +def _test_index_url_precedence(env): + for test in [ + struct( + requirements_txt = "simple==0.0.1 --hash=sha256:deadb00f", + experimental_index_url = "https://experimental.example.com/simple", + experimental_extra_index_urls = [], + expect_index_url = "https://experimental.example.com/simple", + expect_extra_index_urls = [], + expect_url = "experimental.example.com/simple/", + ), + struct( + requirements_txt = """\ +--index-url=https://file.example.com/simple +simple==0.0.1 --hash=sha256:deadb00f +""", + experimental_index_url = "https://experimental.example.com/simple", + experimental_extra_index_urls = [], + expect_index_url = "https://file.example.com/simple", + expect_extra_index_urls = [], + expect_url = "file.example.com/simple/", + ), + struct( + requirements_txt = "simple==0.0.1 --hash=sha256:deadb00f", + experimental_index_url = "", + experimental_extra_index_urls = [], + expect_index_url = "https://pypi.org/simple", + expect_extra_index_urls = [], + expect_url = "pypi.org/simple/", + ), + struct( + requirements_txt = """\ +--extra-index-url=https://extra1.example.com/simple +--extra-index-url=https://extra2.example.com/simple +simple==0.0.1 --hash=sha256:deadb00f +""", + experimental_index_url = "", + experimental_extra_index_urls = [ + "https://ignored.example.com/simple", + ], + expect_index_url = "https://pypi.org/simple", + expect_extra_index_urls = [ + "https://extra1.example.com/simple", + "https://extra2.example.com/simple", + ], + expect_url = "pypi.org/simple/", + ), + # Regression: an unsubstituted ``$VAR`` template with the env var + # unset must expand to "" and fall back to the config default, + # rather than activating the experimental index-url path. + struct( + requirements_txt = "simple==0.0.1 --hash=sha256:deadb00f", + experimental_index_url = "$RULES_PYTHON_PIP_INDEX_URL", + experimental_extra_index_urls = [], + envsubst = ["RULES_PYTHON_PIP_INDEX_URL"], + environ = {}, + expect_index_url = "https://pypi.org/simple", + expect_extra_index_urls = [], + expect_url = "pypi.org/simple/", + ), + # When the env var is set, the resolved value drives the + # experimental index-url path. + struct( + requirements_txt = "simple==0.0.1 --hash=sha256:deadb00f", + experimental_index_url = "${RULES_PYTHON_PIP_INDEX_URL:-}", + experimental_extra_index_urls = [], + envsubst = ["RULES_PYTHON_PIP_INDEX_URL"], + environ = {"RULES_PYTHON_PIP_INDEX_URL": "https://from-env.example.com/simple"}, + expect_index_url = "https://from-env.example.com/simple", + expect_extra_index_urls = [], + expect_url = "from-env.example.com/simple/", + ), + ]: + got_kwargs = {} + + def mock_simpleapi_download(*_, **kwargs): + got_kwargs.update(kwargs) + return { + "simple": struct( + whls = { + "deadb00f": struct( + yanked = None, + filename = "simple-0.0.1-py3-none-any.whl", + sha256 = "deadb00f", + url = test.expect_url, + ), + }, + sdists = {}, + sha256s_by_version = {}, + index_url = test.expect_index_url, + ), + } + + builder = hub_builder( + env, + simpleapi_download_fn = mock_simpleapi_download, + ) + builder.pip_parse( + _mock_mctx( + environ = getattr(test, "environ", {}), + mock_files = { + "requirements.txt": test.requirements_txt, + }, + ), + _parse( + hub_name = "pypi", + python_version = "3.15", + experimental_index_url = test.experimental_index_url, + experimental_extra_index_urls = test.experimental_extra_index_urls, + envsubst = getattr(test, "envsubst", []), + requirements_lock = "requirements.txt", + target_platforms = [ + "linux_x86_64", + "osx_aarch64", + ], + ), + ) + pypi = builder.build() + + pypi.exposed_packages().contains_exactly(["simple"]) + pypi.whl_map().contains_exactly({ + "simple": { + "pypi_315_simple_py3_none_any_deadb00f": [ + whl_config_setting( + target_platforms = ("cp315_linux_x86_64", "cp315_osx_aarch64"), + version = "3.15", + ), + ], + }, + }) + want_whl_library = { + "config_load": "@pypi//:config.bzl", + "dep_template": "@pypi//{name}:{target}", + "filename": "simple-0.0.1-py3-none-any.whl", + "index_url": test.expect_index_url, + "requirement": "simple==0.0.1", + "sha256": "deadb00f", + "urls": [test.expect_url], + } + if getattr(test, "envsubst", []): + want_whl_library["envsubst"] = test.envsubst + pypi.whl_libraries().contains_exactly({ + "pypi_315_simple_py3_none_any_deadb00f": want_whl_library, + }) + pypi.extra_aliases().contains_exactly({}) + + env.expect.that_dict(got_kwargs).contains_exactly({ + "attr": struct( + auth_patterns = {}, + envsubst = getattr(test, "envsubst", []), + extra_index_urls = test.expect_extra_index_urls, + index_url = test.expect_index_url, + index_url_overrides = {}, + netrc = None, + sources = { + "simple": ["0.0.1"], + }, + ), + "cache": {}, + "parallel_download": False, + }) + +_tests.append(_test_index_url_precedence) + def _test_download_only_multiple(env): builder = hub_builder(env) builder.pip_parse( - _mock_mctx( - read = lambda x: { + mocks.mctx( + mock_files = { "requirements.linux_x86_64.txt": """\ --platform=manylinux_2_17_x86_64 --python-version=315 @@ -580,7 +871,7 @@ extra==0.0.1 \ simple==0.0.3 \ --hash=sha256:deadbaaf """, - }[x], + }, ), _parse( hub_name = "pypi", @@ -590,6 +881,10 @@ simple==0.0.3 \ "requirements.linux_x86_64.txt": "linux_x86_64", "requirements.osx_aarch64.txt": "osx_aarch64", }, + target_platforms = [ + "linux_x86_64", + "osx_aarch64", + ], ), ) pypi = builder.build() @@ -619,15 +914,15 @@ simple==0.0.3 \ }) pypi.whl_libraries().contains_exactly({ "pypi_315_extra": { + "config_load": "@pypi//:config.bzl", "dep_template": "@pypi//{name}:{target}", "download_only": True, - # TODO @aignas 2025-04-20: ensure that this is in the hub repo - # "experimental_target_platforms": ["cp315_linux_x86_64"], "extra_pip_args": ["--platform=manylinux_2_17_x86_64", "--python-version=315", "--implementation=cp", "--abi=cp315"], "python_interpreter_target": "unit_test_interpreter_target", "requirement": "extra==0.0.1 --hash=sha256:deadb00f", }, "pypi_315_simple_linux_x86_64": { + "config_load": "@pypi//:config.bzl", "dep_template": "@pypi//{name}:{target}", "download_only": True, "extra_pip_args": ["--platform=manylinux_2_17_x86_64", "--python-version=315", "--implementation=cp", "--abi=cp315"], @@ -635,6 +930,7 @@ simple==0.0.3 \ "requirement": "simple==0.0.1 --hash=sha256:deadbeef", }, "pypi_315_simple_osx_aarch64": { + "config_load": "@pypi//:config.bzl", "dep_template": "@pypi//{name}:{target}", "download_only": True, "extra_pip_args": ["--platform=macosx_10_9_arm64", "--python-version=315", "--implementation=cp", "--abi=cp315"], @@ -654,10 +950,25 @@ def _test_simple_get_index(env): got_simpleapi_download_args.extend(args) got_simpleapi_download_kwargs.update(kwargs) return { + "plat_pkg": struct( + whls = { + "deadb44f": struct( + yanked = None, + filename = "plat-pkg-0.0.4-py3-none-linux_x86_64.whl", + sha256 = "deadb44f", + url = "example2.org/index/plat_pkg/", + ), + }, + sdists = {}, + sha256s_by_version = { + "0.0.4": ["deadb44f"], + }, + index_url = "https://pypi.org/simple", + ), "simple": struct( whls = { "deadb00f": struct( - yanked = False, + yanked = None, filename = "simple-0.0.1-py3-none-any.whl", sha256 = "deadb00f", url = "example2.org", @@ -665,17 +976,18 @@ def _test_simple_get_index(env): }, sdists = { "deadbeef": struct( - yanked = False, + yanked = None, filename = "simple-0.0.1.tar.gz", sha256 = "deadbeef", url = "example.org", ), }, + index_url = "https://pypi.org/simple", ), "some_other_pkg": struct( whls = { "deadb33f": struct( - yanked = False, + yanked = None, filename = "some-other-pkg-0.0.1-py3-none-any.whl", sha256 = "deadb33f", url = "example2.org/index/some_other_pkg/", @@ -686,16 +998,22 @@ def _test_simple_get_index(env): "0.0.1": ["deadb33f"], "0.0.3": ["deadbeef"], }, + index_url = "https://with_index_url", ), } builder = hub_builder( env, simpleapi_download_fn = mocksimpleapi_download, + whl_overrides = { + "direct_without_sha": { + "my_patch": 1, + }, + }, ) builder.pip_parse( - _mock_mctx( - read = lambda x: { + mocks.mctx( + mock_files = { "requirements.txt": """ simple==0.0.1 \ --hash=sha256:deadbeef \ @@ -704,20 +1022,27 @@ some_pkg==0.0.1 @ example-direct.org/some_pkg-0.0.1-py3-none-any.whl \ --hash=sha256:deadbaaf direct_without_sha==0.0.1 @ example-direct.org/direct_without_sha-0.0.1-py3-none-any.whl some_other_pkg==0.0.1 +plat_pkg==0.0.4 pip_fallback==0.0.1 direct_sdist_without_sha @ some-archive/any-name.tar.gz git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef """, - }[x], + }, ), _parse( hub_name = "pypi", python_version = "3.15", requirements_lock = "requirements.txt", - experimental_index_url = "pypi.org", extra_pip_args = [ "--extra-args-for-sdist-building", ], + target_platforms = [ + "linux_aarch64", + "linux_x86_64", + "linux_x86_64_freethreaded", + "osx_aarch64", + "windows_aarch64", + ], ), ) pypi = builder.build() @@ -727,6 +1052,7 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef "direct_without_sha", "git_dep", "pip_fallback", + "plat_pkg", "simple", "some_other_pkg", "some_pkg", @@ -775,6 +1101,17 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef ), ], }, + "plat_pkg": { + "pypi_315_plat_py3_none_linux_x86_64_deadb44f": [ + whl_config_setting( + target_platforms = [ + "cp315_linux_x86_64", + "cp315_linux_x86_64_freethreaded", + ], + version = "3.15", + ), + ], + }, "simple": { "pypi_315_simple_py3_none_any_deadb00f": [ whl_config_setting( @@ -820,13 +1157,8 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef }) pypi.whl_libraries().contains_exactly({ "pypi_315_any_name": { + "config_load": "@pypi//:config.bzl", "dep_template": "@pypi//{name}:{target}", - "experimental_target_platforms": [ - "linux_aarch64", - "linux_x86_64", - "osx_aarch64", - "windows_aarch64", - ], "extra_pip_args": ["--extra-args-for-sdist-building"], "filename": "any-name.tar.gz", "python_interpreter_target": "unit_test_interpreter_target", @@ -835,69 +1167,59 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef "urls": ["some-archive/any-name.tar.gz"], }, "pypi_315_direct_without_sha_0_0_1_py3_none_any": { + "config_load": "@pypi//:config.bzl", "dep_template": "@pypi//{name}:{target}", - "experimental_target_platforms": [ - "linux_aarch64", - "linux_x86_64", - "osx_aarch64", - "windows_aarch64", - ], "filename": "direct_without_sha-0.0.1-py3-none-any.whl", - "python_interpreter_target": "unit_test_interpreter_target", "requirement": "direct_without_sha==0.0.1", "sha256": "", "urls": ["example-direct.org/direct_without_sha-0.0.1-py3-none-any.whl"], + "whl_patches": {"my_patch": "1"}, }, "pypi_315_git_dep": { + "config_load": "@pypi//:config.bzl", "dep_template": "@pypi//{name}:{target}", "extra_pip_args": ["--extra-args-for-sdist-building"], "python_interpreter_target": "unit_test_interpreter_target", "requirement": "git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef", }, "pypi_315_pip_fallback": { + "config_load": "@pypi//:config.bzl", "dep_template": "@pypi//{name}:{target}", "extra_pip_args": ["--extra-args-for-sdist-building"], "python_interpreter_target": "unit_test_interpreter_target", "requirement": "pip_fallback==0.0.1", }, + "pypi_315_plat_py3_none_linux_x86_64_deadb44f": { + "config_load": "@pypi//:config.bzl", + "dep_template": "@pypi//{name}:{target}", + "filename": "plat-pkg-0.0.4-py3-none-linux_x86_64.whl", + "index_url": "https://pypi.org/simple", + "requirement": "plat_pkg==0.0.4", + "sha256": "deadb44f", + "urls": ["example2.org/index/plat_pkg/"], + }, "pypi_315_simple_py3_none_any_deadb00f": { + "config_load": "@pypi//:config.bzl", "dep_template": "@pypi//{name}:{target}", - "experimental_target_platforms": [ - "linux_aarch64", - "linux_x86_64", - "osx_aarch64", - "windows_aarch64", - ], "filename": "simple-0.0.1-py3-none-any.whl", - "python_interpreter_target": "unit_test_interpreter_target", + "index_url": "https://pypi.org/simple", "requirement": "simple==0.0.1", "sha256": "deadb00f", "urls": ["example2.org"], }, "pypi_315_some_pkg_py3_none_any_deadbaaf": { + "config_load": "@pypi//:config.bzl", "dep_template": "@pypi//{name}:{target}", - "experimental_target_platforms": [ - "linux_aarch64", - "linux_x86_64", - "osx_aarch64", - "windows_aarch64", - ], "filename": "some_pkg-0.0.1-py3-none-any.whl", - "python_interpreter_target": "unit_test_interpreter_target", "requirement": "some_pkg==0.0.1", "sha256": "deadbaaf", "urls": ["example-direct.org/some_pkg-0.0.1-py3-none-any.whl"], }, "pypi_315_some_py3_none_any_deadb33f": { + "config_load": "@pypi//:config.bzl", "dep_template": "@pypi//{name}:{target}", - "experimental_target_platforms": [ - "linux_aarch64", - "linux_x86_64", - "osx_aarch64", - "windows_aarch64", - ], "filename": "some-other-pkg-0.0.1-py3-none-any.whl", - "python_interpreter_target": "unit_test_interpreter_target", + "index_url": "https://with_index_url", "requirement": "some_other_pkg==0.0.1", "sha256": "deadb33f", "urls": ["example2.org/index/some_other_pkg/"], @@ -910,10 +1232,15 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef auth_patterns = {}, envsubst = {}, extra_index_urls = [], - index_url = "pypi.org", + index_url = "https://pypi.org/simple", index_url_overrides = {}, netrc = None, - sources = ["simple", "pip_fallback", "some_other_pkg"], + sources = { + "pip_fallback": ["0.0.1"], + "plat_pkg": ["0.0.4"], + "simple": ["0.0.1"], + "some_other_pkg": ["0.0.1"], + }, ), "cache": {}, "parallel_download": False, @@ -923,66 +1250,129 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef _tests.append(_test_simple_get_index) def _test_optimum_sys_platform_extra(env): + sub_tests = { + ("osx", "aarch64"): "optimum[onnxruntime]==1.17.1", + ("linux", "aarch64"): "optimum[onnxruntime-gpu]==1.17.1", + } + for (host_os, host_arch), want_requirement in sub_tests.items(): + builder = hub_builder( + env, + ) + builder.pip_parse( + mocks.mctx( + mock_files = { + "universal.txt": """\ +optimum[onnxruntime]==1.17.1 ; sys_platform == 'darwin' +optimum[onnxruntime-gpu]==1.17.1 ; sys_platform == 'linux' +""", + }, + os_name = host_os, + arch_name = host_arch, + ), + _parse( + hub_name = "pypi", + python_version = "3.15", + requirements_lock = "universal.txt", + ), + ) + pypi = builder.build() + + pypi.exposed_packages().contains_exactly(["optimum"]) + pypi.group_map().contains_exactly({}) + pypi.whl_map().contains_exactly({ + "optimum": { + "pypi_315_optimum": [ + whl_config_setting(version = "3.15"), + ], + }, + }) + pypi.whl_libraries().contains_exactly({ + "pypi_315_optimum": { + "config_load": "@pypi//:config.bzl", + "dep_template": "@pypi//{name}:{target}", + "python_interpreter_target": "unit_test_interpreter_target", + "requirement": want_requirement, + }, + }) + pypi.extra_aliases().contains_exactly({}) + +_tests.append(_test_optimum_sys_platform_extra) + +def _test_pipstar_platforms(env): builder = hub_builder( env, - evaluate_markers_fn = lambda _, requirements, **__: { - key: [ - platform - for platform in platforms - if ("darwin" in key and "osx" in platform) or ("linux" in key and "linux" in platform) - ] - for key, platforms in requirements.items() - }, + config = struct( + enable_pipstar_extract = True, + index_url = "https://pypi.org/simple", + netrc = None, + auth_patterns = {}, + platforms = { + "my{}{}".format(os, cpu): _plat( + name = "my{}{}".format(os, cpu), + os_name = os, + arch_name = cpu, + marker = "python_version ~= \"3.13\"", + config_settings = [ + "@platforms//os:{}".format(os), + "@platforms//cpu:{}".format(cpu), + ], + ) + for os, cpu in [ + ("linux", "x86_64"), + ("osx", "aarch64"), + ] + }, + ), ) builder.pip_parse( - _mock_mctx( - read = lambda x: { + mocks.mctx( + mock_files = { "universal.txt": """\ optimum[onnxruntime]==1.17.1 ; sys_platform == 'darwin' optimum[onnxruntime-gpu]==1.17.1 ; sys_platform == 'linux' """, - }[x], + }, ), _parse( hub_name = "pypi", python_version = "3.15", requirements_lock = "universal.txt", + target_platforms = ["mylinuxx86_64", "myosxaarch64"], ), ) pypi = builder.build() - # FIXME @aignas 2025-09-07: we should expose the `optimum` package - pypi.exposed_packages().contains_exactly([]) + pypi.exposed_packages().contains_exactly(["optimum"]) pypi.group_map().contains_exactly({}) pypi.whl_map().contains_exactly({ "optimum": { - "pypi_315_optimum_linux_aarch64_linux_x86_64_linux_x86_64_freethreaded": [ + "pypi_315_optimum_mylinuxx86_64": [ whl_config_setting( version = "3.15", target_platforms = [ - "cp315_linux_aarch64", - "cp315_linux_x86_64", - "cp315_linux_x86_64_freethreaded", + "cp315_mylinuxx86_64", ], ), ], - "pypi_315_optimum_osx_aarch64": [ + "pypi_315_optimum_myosxaarch64": [ whl_config_setting( version = "3.15", target_platforms = [ - "cp315_osx_aarch64", + "cp315_myosxaarch64", ], ), ], }, }) pypi.whl_libraries().contains_exactly({ - "pypi_315_optimum_linux_aarch64_linux_x86_64_linux_x86_64_freethreaded": { + "pypi_315_optimum_mylinuxx86_64": { + "config_load": "@pypi//:config.bzl", "dep_template": "@pypi//{name}:{target}", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "optimum[onnxruntime-gpu]==1.17.1", }, - "pypi_315_optimum_osx_aarch64": { + "pypi_315_optimum_myosxaarch64": { + "config_load": "@pypi//:config.bzl", "dep_template": "@pypi//{name}:{target}", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "optimum[onnxruntime]==1.17.1", @@ -990,14 +1380,14 @@ optimum[onnxruntime-gpu]==1.17.1 ; sys_platform == 'linux' }) pypi.extra_aliases().contains_exactly({}) -_tests.append(_test_optimum_sys_platform_extra) +_tests.append(_test_pipstar_platforms) -def _test_pipstar_platforms(env): +def _test_pipstar_platforms_limit(env): builder = hub_builder( env, - enable_pipstar = True, config = struct( - enable_pipstar = True, + enable_pipstar_extract = True, + index_url = "https://pypi.org/simple", netrc = None, auth_patterns = {}, platforms = { @@ -1019,18 +1409,21 @@ def _test_pipstar_platforms(env): ), ) builder.pip_parse( - _mock_mctx( - read = lambda x: { + mocks.mctx( + os_name = "linux", + arch_name = "amd64", + mock_files = { "universal.txt": """\ optimum[onnxruntime]==1.17.1 ; sys_platform == 'darwin' optimum[onnxruntime-gpu]==1.17.1 ; sys_platform == 'linux' """, - }[x], + }, ), _parse( hub_name = "pypi", python_version = "3.15", requirements_lock = "universal.txt", + target_platforms = ["my{os}{arch}"], ), ) pypi = builder.build() @@ -1039,39 +1432,85 @@ optimum[onnxruntime-gpu]==1.17.1 ; sys_platform == 'linux' pypi.group_map().contains_exactly({}) pypi.whl_map().contains_exactly({ "optimum": { - "pypi_315_optimum_mylinuxx86_64": [ - whl_config_setting( - version = "3.15", - target_platforms = [ - "cp315_mylinuxx86_64", - ], - ), - ], - "pypi_315_optimum_myosxaarch64": [ - whl_config_setting( - version = "3.15", - target_platforms = [ - "cp315_myosxaarch64", - ], - ), + "pypi_315_optimum": [ + whl_config_setting(version = "3.15"), ], }, }) pypi.whl_libraries().contains_exactly({ - "pypi_315_optimum_mylinuxx86_64": { + "pypi_315_optimum": { + "config_load": "@pypi//:config.bzl", "dep_template": "@pypi//{name}:{target}", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "optimum[onnxruntime-gpu]==1.17.1", }, - "pypi_315_optimum_myosxaarch64": { - "dep_template": "@pypi//{name}:{target}", - "python_interpreter_target": "unit_test_interpreter_target", - "requirement": "optimum[onnxruntime]==1.17.1", - }, }) pypi.extra_aliases().contains_exactly({}) -_tests.append(_test_pipstar_platforms) +_tests.append(_test_pipstar_platforms_limit) + +def _test_err_duplicate_repos(env): + logs = {} + log_printer = lambda key, message: logs.setdefault(key.strip(), []).append(message) + builder = hub_builder( + env, + available_interpreters = { + "python_3_15_1_host": "unit_test_interpreter_target_1", + "python_3_15_2_host": "unit_test_interpreter_target_2", + }, + log_printer = log_printer, + ) + builder.pip_parse( + _mock_mctx( + os_name = "osx", + arch_name = "aarch64", + mock_files = { + "requirements.txt": "foo==0.0.1", + }, + ), + _parse( + hub_name = "pypi", + python_version = "3.15.1", + requirements_lock = "requirements.txt", + ), + ) + builder.pip_parse( + _mock_mctx( + os_name = "osx", + arch_name = "aarch64", + mock_files = { + "requirements.txt": "foo==0.0.1", + }, + ), + _parse( + hub_name = "pypi", + python_version = "3.15.2", + requirements_lock = "requirements.txt", + ), + ) + pypi = builder.build() + + pypi.exposed_packages().contains_exactly([]) + pypi.group_map().contains_exactly({}) + pypi.whl_map().contains_exactly({}) + pypi.whl_libraries().contains_exactly({}) + pypi.extra_aliases().contains_exactly({}) + env.expect.that_dict(logs).keys().contains_exactly(["rules_python:unit-test FAIL:"]) + env.expect.that_collection(logs["rules_python:unit-test FAIL:"]).contains_exactly([ + """\ +Attempting to create a duplicate library pypi_315_foo for foo with different arguments. Already existing declaration has: + common: { + "dep_template": "@pypi//{name}:{target}", + "config_load": "@pypi//:config.bzl", + "requirement": "foo==0.0.1", + } + different: { + "python_interpreter_target": ("unit_test_interpreter_target_1", "unit_test_interpreter_target_2"), + }\ +""", + ]).in_order() + +_tests.append(_test_err_duplicate_repos) def hub_builder_test_suite(name): """Create the test suite. diff --git a/tests/pypi/parse_requirements/parse_requirements_tests.bzl b/tests/pypi/parse_requirements/parse_requirements_tests.bzl index bd0078bfa4..576a31ae5d 100644 --- a/tests/pypi/parse_requirements/parse_requirements_tests.bzl +++ b/tests/pypi/parse_requirements/parse_requirements_tests.bzl @@ -16,18 +16,12 @@ load("@rules_testing//lib:test_suite.bzl", "test_suite") load("//python/private:repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "REPO_VERBOSITY_ENV_VAR", "repo_utils") # buildifier: disable=bzl-visibility -load("//python/private/pypi:evaluate_markers.bzl", "evaluate_markers") # buildifier: disable=bzl-visibility load("//python/private/pypi:parse_requirements.bzl", "select_requirement", _parse_requirements = "parse_requirements") # buildifier: disable=bzl-visibility load("//python/private/pypi:pep508_env.bzl", pep508_env = "env") # buildifier: disable=bzl-visibility +load("//tests/support/mocks:mocks.bzl", "mocks") def _mock_ctx(): testdata = { - "requirements_different_package_version": """\ -foo==0.0.1+local \ - --hash=sha256:deadbeef -foo==0.0.1 \ - --hash=sha256:deadb00f -""", "requirements_direct": """\ foo[extra] @ https://some-url/package.whl """, @@ -39,6 +33,14 @@ foo @ https://github.com/org/foo/downloads/foo-1.1.tar.gz foo[extra]==0.0.1 \ --hash=sha256:deadbeef +""", + "requirements_foo": """\ +foo==0.0.1 \ + --hash=sha256:deadb00f +""", + "requirements_foo_local": """\ +foo==0.0.1+local \ + --hash=sha256:deadbeef """, "requirements_git": """ foo @ git+https://github.com/org/foo.git@deadbeef @@ -65,7 +67,7 @@ foo==0.0.1 --hash=sha256:deadbeef foo[extra]==0.0.1 --hash=sha256:deadbeef """, "requirements_marker": """\ -foo[extra]==0.0.1 ;marker --hash=sha256:deadbeef +foo[extra]==0.0.1 ; os_name == 'nt' --hash=sha256:deadbeef bar==0.0.1 --hash=sha256:deadbeef """, "requirements_multi_version": """\ @@ -73,9 +75,11 @@ foo==0.0.1; python_full_version < '3.10.0' \ --hash=sha256:deadbeef foo==0.0.2; python_full_version >= '3.10.0' \ --hash=sha256:deadb11f +boo==0.0.4; python_full_version < '3.10.0' \ + --hash=sha256:deadbaaf """, "requirements_optional_hash": """ -foo==0.0.4 @ https://example.org/foo-0.0.4.whl +bar==0.0.4 @ https://example.org/bar-0.0.4.whl foo==0.0.5 @ https://example.org/foo-0.0.5.whl --hash=sha256:deadbeef """, "requirements_osx": """\ @@ -93,33 +97,79 @@ foo==0.0.3 --hash=sha256:deadbaaf foo[extra]==0.0.2 --hash=sha256:deadbeef bar==0.0.1 --hash=sha256:deadb00f """, + "uv_lock_empty": """{"package":[]}""", + "uv_lock_foo": """{"package":[{"dependencies":[{"extra":"extra","name":"bar"}],"name":"foo","source":{"registry":"https://pypi.org/simple"},"version":"0.0.1","wheels":[{"hash":"sha256:deadbeef","url":"https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl"}]}]}""", + "uv_lock_foo_bar": """{"package":[{"name":"bar","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"sdist":{"hash":"sha256:deadb00f","url":"https://files.pythonhosted.org/packages/bar-0.0.1.tar.gz"}},{"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"wheels":[{"hash":"sha256:deadbeef","url":"https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl"}]}]}""", + "uv_lock_foo_dep_extra": """{"package":[{"name":"bar","version":"0.0.2","source":{"registry":"https://pypi.org/simple"},"wheels":[{"hash":"sha256:deadbeef","url":"https://files.pythonhosted.org/packages/bar-0.0.2-py3-none-any.whl"}]},{"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"dependencies":[{"name":"bar","extra":["extra1"]}],"wheels":[{"hash":"sha256:baadbeef","url":"https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl"}]}]}""", + "uv_lock_foo_multi_versions": """{"package":[{"name":"foo","source":{"registry":"https://pypi.org/simple"},"version":"0.0.1","wheels":[{"hash":"sha256:deadbeef","url":"https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl"}]},{"name":"foo","source":{"registry":"https://pypi.org/simple"},"version":"0.0.2","wheels":[{"hash":"sha256:deadb11f","url":"https://files.pythonhosted.org/packages/foo-0.0.2-py3-none-any.whl"}]}]}""", + "uv_lock_foo_multi_wheel_dedup": """{"package":[{"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"wheels":[{"hash":"sha256:aaa","url":"https://files.pythonhosted.org/packages/foo-0.0.1-cp39-cp39-manylinux_2_17_x86_64.whl"},{"hash":"sha256:bbb","url":"https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl"}]}]}""", + "uv_lock_foo_only": """{"package":[{"name":"foo","source":{"registry":"https://pypi.org/simple"},"version":"0.0.2"}]}""", + "uv_lock_foo_optional_deps": """{"package":[{"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"optional-dependencies":{"extra1":[],"extra2":[]},"wheels":[{"hash":"sha256:deadbeef","url":"https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl"}]}]}""", + "uv_lock_foo_requires_dist_extras": """{"package":[{"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"wheels":[{"hash":"sha256:deadbeef","url":"https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl"}]},{"name":"root-pkg","source":{"virtual":"."},"version":"0.0.0","dependencies":[{"name":"foo"}],"metadata":{"requires-dist":[{"name":"foo","extras":["all"]}]}}]}""", + "uv_lock_foo_resolution_markers_dedup": """{"package":[{"name":"foo","source":{"registry":"https://pypi.org/simple"},"version":"0.0.1","resolution-markers":["sys_platform == 'linux'"],"wheels":[{"hash":"sha256:aaa","url":"https://files.pythonhosted.org/packages/foo-0.0.1-cp39-cp39-manylinux_2_17_x86_64.whl"},{"hash":"sha256:bbb","url":"https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl"}]},{"name":"foo","source":{"registry":"https://pypi.org/simple"},"version":"0.0.2","resolution-markers":["sys_platform == 'darwin'"],"wheels":[{"hash":"sha256:ccc","url":"https://files.pythonhosted.org/packages/foo-0.0.2-cp39-cp39-macosx_11_0_arm64.whl"},{"hash":"sha256:ddd","url":"https://files.pythonhosted.org/packages/foo-0.0.2-py3-none-any.whl"}]}]}""", + "uv_lock_foo_sdist": """{"package":[{"name":"foo","sdist":{"hash":"sha256:feedcafe","url":"https://files.pythonhosted.org/packages/foo-0.0.1.tar.gz"},"source":{"registry":"https://pypi.org/simple"},"version":"0.0.1","wheels":[{"hash":"sha256:deadbeef","url":"https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl"}]}]}""", + "uv_lock_foo_virtual": """{"package":[{"name":"foo","source":{"registry":"https://pypi.org/simple"},"version":"0.0.1","wheels":[{"hash":"sha256:deadbeef","url":"https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl"}]},{"name":"virtual-pkg","source":{"virtual":true},"version":"0.0.0"}]}""", + "uv_lock_foo_with_extras": """{"package":[{"name":"foo","provides-extras":["extra"],"source":{"registry":"https://pypi.org/simple"},"version":"0.0.1","wheels":[{"hash":"sha256:deadbeef","url":"https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl"}]}]}""", + "uv_lock_git_vcs": """{"package":[{"name":"foo","source":{"git":"https://github.com/org/foo.git"},"version":"0.1.0"}]}""", + "uv_lock_rules_python_pkg": """{"package":[{"name":"rules_python","source":{"registry":"https://pypi.org/simple"},"version":"0.0.1","wheels":[{"hash":"sha256:deadbeef","url":"https://files.pythonhosted.org/packages/rules_python-0.0.1-py3-none-any.whl"}]}]}""", } - return struct( - os = struct( - name = "linux", - arch = "x86_64", - ), - read = lambda x: testdata[x], + return mocks.mctx( + os_name = "linux", + arch_name = "x86_64", + mock_files = testdata, ) _tests = [] +def _make_platforms(platform_names): + """Create minimal platform structs for testing, matching py3-none-any wheels.""" + platforms = {} + for name in platform_names: + platforms[name] = struct( + env = pep508_env(python_version = "3.11.0", os = "linux", arch = "x86_64"), + whl_abi_tags = ["none"], + whl_platform_tags = ["any"], + ) + return platforms + def parse_requirements(debug = False, **kwargs): + """Get requirements by calling the original parse_requirements. + + Args: + debug: If True, set verbosity to TRACE. + **kwargs: forwarded to the underlying function. + + Returns: + The result of the underlying parse_requirements call. + """ + kwargs.setdefault("toml_decode", json.decode) + + # Provide default platforms when not specified. + if "platforms" not in kwargs: + if "requirements_by_platform" in kwargs: + platform_names = {} + for _plats in kwargs["requirements_by_platform"].values(): + for _p in _plats: + platform_names[_p] = None + platform_names = sorted(platform_names) + kwargs["platforms"] = _make_platforms(platform_names) + elif "uv_lock" in kwargs: + kwargs["platforms"] = _make_platforms(["linux_x86_64"]) + return _parse_requirements( ctx = _mock_ctx(), logger = repo_utils.logger(struct( - os = struct( - environ = { - REPO_DEBUG_ENV_VAR: "1", - REPO_VERBOSITY_ENV_VAR: "TRACE" if debug else "INFO", - }, - ), + getenv = { + REPO_DEBUG_ENV_VAR: "1", + REPO_VERBOSITY_ENV_VAR: "TRACE" if debug else "INFO", + }.get, ), "unit-test"), **kwargs ) def _test_simple(env): + """Test basic parsing of a single ``requirements_lock`` file.""" got = parse_requirements( requirements_by_platform = { "requirements_lock": ["linux_x86_64", "windows_x86_64"], @@ -128,6 +178,7 @@ def _test_simple(env): env.expect.that_collection(got).contains_exactly([ struct( name = "foo", + index_url = "", is_exposed = True, is_multiple_versions = False, srcs = [ @@ -142,7 +193,7 @@ def _test_simple(env): url = "", filename = "", sha256 = "", - yanked = False, + yanked = None, ), ], ), @@ -161,6 +212,7 @@ def _test_direct_urls_integration(env): env.expect.that_collection(got).contains_exactly([ struct( name = "foo", + index_url = "", is_exposed = True, is_multiple_versions = True, srcs = [ @@ -172,7 +224,7 @@ def _test_direct_urls_integration(env): sha256 = "", target_platforms = ["osx_x86_64"], url = "https://github.com/org/foo/downloads/foo-1.1.tar.gz", - yanked = False, + yanked = None, ), struct( distribution = "foo", @@ -182,7 +234,7 @@ def _test_direct_urls_integration(env): sha256 = "", target_platforms = ["linux_x86_64"], url = "https://some-url/package.whl", - yanked = False, + yanked = None, ), ], ), @@ -190,7 +242,50 @@ def _test_direct_urls_integration(env): _tests.append(_test_direct_urls_integration) +def _test_direct_urls_no_extract(env): + """Check that URL requirements are not dropped when extract_url_srcs=False.""" + got = parse_requirements( + requirements_by_platform = { + "requirements_direct": ["linux_x86_64"], + "requirements_direct_sdist": ["osx_x86_64"], + }, + extract_url_srcs = False, + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + index_url = "", + is_exposed = True, + is_multiple_versions = True, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + filename = "", + requirement_line = "foo @ https://github.com/org/foo/downloads/foo-1.1.tar.gz", + sha256 = "", + target_platforms = ["osx_x86_64"], + url = "", + yanked = None, + ), + struct( + distribution = "foo", + extra_pip_args = [], + filename = "", + requirement_line = "foo[extra] @ https://some-url/package.whl", + sha256 = "", + target_platforms = ["linux_x86_64"], + url = "", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_direct_urls_no_extract) + def _test_extra_pip_args(env): + """Test that ``extra_pip_args`` are merged with per-requirement-file args.""" got = parse_requirements( requirements_by_platform = { "requirements_extra_args": ["linux_x86_64"], @@ -200,6 +295,7 @@ def _test_extra_pip_args(env): env.expect.that_collection(got).contains_exactly([ struct( name = "foo", + index_url = "", is_exposed = True, is_multiple_versions = False, srcs = [ @@ -213,7 +309,7 @@ def _test_extra_pip_args(env): url = "", filename = "", sha256 = "", - yanked = False, + yanked = None, ), ], ), @@ -222,6 +318,7 @@ def _test_extra_pip_args(env): _tests.append(_test_extra_pip_args) def _test_dupe_requirements(env): + """Test that duplicate requirement entries are deduplicated.""" got = parse_requirements( requirements_by_platform = { "requirements_lock_dupe": ["linux_x86_64"], @@ -230,6 +327,7 @@ def _test_dupe_requirements(env): env.expect.that_collection(got).contains_exactly([ struct( name = "foo", + index_url = "", is_exposed = True, is_multiple_versions = False, srcs = [ @@ -241,7 +339,7 @@ def _test_dupe_requirements(env): url = "", filename = "", sha256 = "", - yanked = False, + yanked = None, ), ], ), @@ -250,6 +348,7 @@ def _test_dupe_requirements(env): _tests.append(_test_dupe_requirements) def _test_multi_os(env): + """Test per-OS requirements parsing with ``select_requirement``.""" got = parse_requirements( requirements_by_platform = { "requirements_linux": ["linux_x86_64"], @@ -260,6 +359,7 @@ def _test_multi_os(env): env.expect.that_collection(got).contains_exactly([ struct( name = "bar", + index_url = "", is_exposed = False, is_multiple_versions = False, srcs = [ @@ -271,12 +371,13 @@ def _test_multi_os(env): url = "", filename = "", sha256 = "", - yanked = False, + yanked = None, ), ], ), struct( name = "foo", + index_url = "", is_exposed = True, is_multiple_versions = True, srcs = [ @@ -288,7 +389,7 @@ def _test_multi_os(env): url = "", filename = "", sha256 = "", - yanked = False, + yanked = None, ), struct( distribution = "foo", @@ -298,7 +399,7 @@ def _test_multi_os(env): url = "", filename = "", sha256 = "", - yanked = False, + yanked = None, ), ], ), @@ -313,6 +414,7 @@ def _test_multi_os(env): _tests.append(_test_multi_os) def _test_multi_os_legacy(env): + """Test download-only per-OS requirements parsing.""" got = parse_requirements( requirements_by_platform = { "requirements_linux_download_only": ["cp39_linux_x86_64"], @@ -323,6 +425,7 @@ def _test_multi_os_legacy(env): env.expect.that_collection(got).contains_exactly([ struct( name = "bar", + index_url = "", is_exposed = False, is_multiple_versions = False, srcs = [ @@ -334,12 +437,13 @@ def _test_multi_os_legacy(env): url = "", filename = "", sha256 = "", - yanked = False, + yanked = None, ), ], ), struct( name = "foo", + index_url = "", is_exposed = True, is_multiple_versions = True, srcs = [ @@ -351,7 +455,7 @@ def _test_multi_os_legacy(env): url = "", filename = "", sha256 = "", - yanked = False, + yanked = None, ), struct( distribution = "foo", @@ -361,7 +465,7 @@ def _test_multi_os_legacy(env): url = "", filename = "", sha256 = "", - yanked = False, + yanked = None, ), ], ), @@ -370,6 +474,7 @@ def _test_multi_os_legacy(env): _tests.append(_test_multi_os_legacy) def _test_select_requirement_none_platform(env): + """Test that ``select_requirement`` returns the first src when platform is ``None``.""" got = select_requirement( [ struct( @@ -384,24 +489,29 @@ def _test_select_requirement_none_platform(env): _tests.append(_test_select_requirement_none_platform) def _test_env_marker_resolution(env): - def _mock_eval_markers(_, input): - ret = { - "foo[extra]==0.0.1 ;marker --hash=sha256:deadbeef": ["cp311_windows_x86_64"], - } - - env.expect.that_collection(input.keys()).contains_exactly(ret.keys()) - env.expect.that_collection(input.values()[0]).contains_exactly(["cp311_linux_super_exotic", "cp311_windows_x86_64"]) - return ret + """Test environment marker resolution with platform env information.""" got = parse_requirements( requirements_by_platform = { "requirements_marker": ["cp311_linux_super_exotic", "cp311_windows_x86_64"], }, - evaluate_markers = _mock_eval_markers, + platforms = { + "cp311_linux_super_exotic": struct( + env = pep508_env(os = "linux", arch = "x86_64", python_version = "3.11.0"), + whl_abi_tags = [], + whl_platform_tags = [], + ), + "cp311_windows_x86_64": struct( + env = pep508_env(os = "windows", arch = "x86_64", python_version = "3.11.0"), + whl_abi_tags = [], + whl_platform_tags = [], + ), + }, ) env.expect.that_collection(got).contains_exactly([ struct( name = "bar", + index_url = "", is_exposed = True, is_multiple_versions = False, srcs = [ @@ -413,12 +523,13 @@ def _test_env_marker_resolution(env): url = "", filename = "", sha256 = "", - yanked = False, + yanked = None, ), ], ), struct( name = "foo", + index_url = "", is_exposed = False, is_multiple_versions = False, srcs = [ @@ -430,7 +541,7 @@ def _test_env_marker_resolution(env): url = "", filename = "", sha256 = "", - yanked = False, + yanked = None, ), ], ), @@ -439,14 +550,17 @@ def _test_env_marker_resolution(env): _tests.append(_test_env_marker_resolution) def _test_different_package_version(env): + """Test that different package versions across platforms are handled.""" got = parse_requirements( requirements_by_platform = { - "requirements_different_package_version": ["linux_x86_64"], + "requirements_foo": ["linux_aarch64"], + "requirements_foo_local": ["linux_x86_64"], }, ) env.expect.that_collection(got).contains_exactly([ struct( name = "foo", + index_url = "", is_exposed = True, is_multiple_versions = True, srcs = [ @@ -454,11 +568,11 @@ def _test_different_package_version(env): distribution = "foo", extra_pip_args = [], requirement_line = "foo==0.0.1 --hash=sha256:deadb00f", - target_platforms = ["linux_x86_64"], + target_platforms = ["linux_aarch64"], url = "", filename = "", sha256 = "", - yanked = False, + yanked = None, ), struct( distribution = "foo", @@ -468,7 +582,7 @@ def _test_different_package_version(env): url = "", filename = "", sha256 = "", - yanked = False, + yanked = None, ), ], ), @@ -476,28 +590,79 @@ def _test_different_package_version(env): _tests.append(_test_different_package_version) -def _test_optional_hash(env): +def _test_different_package_extras(env): + """Test that different extras across platforms are handled.""" got = parse_requirements( requirements_by_platform = { - "requirements_optional_hash": ["linux_x86_64"], + "requirements_foo": ["linux_aarch64"], + "requirements_lock": ["linux_x86_64"], }, ) env.expect.that_collection(got).contains_exactly([ struct( name = "foo", + index_url = "", is_exposed = True, is_multiple_versions = True, srcs = [ struct( distribution = "foo", extra_pip_args = [], - requirement_line = "foo==0.0.4", + requirement_line = "foo==0.0.1 --hash=sha256:deadb00f", + target_platforms = ["linux_aarch64"], + url = "", + filename = "", + sha256 = "", + yanked = None, + ), + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo[extra]==0.0.1 --hash=sha256:deadbeef", + target_platforms = ["linux_x86_64"], + url = "", + filename = "", + sha256 = "", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_different_package_extras) + +def _test_optional_hash(env): + """Test parsing of requirements with optional hashes and URLs.""" + got = parse_requirements( + requirements_by_platform = { + "requirements_optional_hash": ["linux_x86_64"], + }, + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "bar", + index_url = "", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "bar", + extra_pip_args = [], + requirement_line = "bar==0.0.4", target_platforms = ["linux_x86_64"], - url = "https://example.org/foo-0.0.4.whl", - filename = "foo-0.0.4.whl", + url = "https://example.org/bar-0.0.4.whl", + filename = "bar-0.0.4.whl", sha256 = "", - yanked = False, + yanked = None, ), + ], + ), + struct( + name = "foo", + index_url = "", + is_exposed = True, + is_multiple_versions = False, + srcs = [ struct( distribution = "foo", extra_pip_args = [], @@ -506,7 +671,7 @@ def _test_optional_hash(env): url = "https://example.org/foo-0.0.5.whl", filename = "foo-0.0.5.whl", sha256 = "deadbeef", - yanked = False, + yanked = None, ), ], ), @@ -515,6 +680,7 @@ def _test_optional_hash(env): _tests.append(_test_optional_hash) def _test_git_sources(env): + """Test parsing of git-sourced requirements.""" got = parse_requirements( requirements_by_platform = { "requirements_git": ["linux_x86_64"], @@ -523,6 +689,7 @@ def _test_git_sources(env): env.expect.that_collection(got).contains_exactly([ struct( name = "foo", + index_url = "", is_exposed = True, is_multiple_versions = False, srcs = [ @@ -534,7 +701,7 @@ def _test_git_sources(env): url = "", filename = "", sha256 = "", - yanked = False, + yanked = None, ), ], ), @@ -543,6 +710,7 @@ def _test_git_sources(env): _tests.append(_test_git_sources) def _test_overlapping_shas_with_index_results(env): + """Test that index results with overlapping shas are matched to the correct platform.""" got = parse_requirements( requirements_by_platform = { "requirements_linux": ["cp39_linux_x86_64"], @@ -568,14 +736,15 @@ def _test_overlapping_shas_with_index_results(env): whl_platform_tags = ["macosx_*_x86_64"], ), }, - get_index_urls = lambda _, __: { + get_index_urls = lambda _, __, **kwargs: { "foo": struct( + index_url = "https://example.com", sdists = { "5d15t": struct( url = "sdist", sha256 = "5d15t", filename = "foo-0.0.1.tar.gz", - yanked = False, + yanked = None, ), }, whls = { @@ -583,13 +752,13 @@ def _test_overlapping_shas_with_index_results(env): url = "super2", sha256 = "deadb11f", filename = "foo-0.0.1-py3-none-macosx_14_0_x86_64.whl", - yanked = False, + yanked = None, ), "deadbaaf": struct( url = "super2", sha256 = "deadbaaf", filename = "foo-0.0.1-py3-none-any.whl", - yanked = False, + yanked = None, ), }, ), @@ -598,9 +767,10 @@ def _test_overlapping_shas_with_index_results(env): env.expect.that_collection(got).contains_exactly([ struct( + name = "foo", + index_url = "https://example.com", is_exposed = True, is_multiple_versions = True, - name = "foo", srcs = [ struct( distribution = "foo", @@ -610,7 +780,7 @@ def _test_overlapping_shas_with_index_results(env): sha256 = "deadbaaf", target_platforms = ["cp39_linux_x86_64"], url = "super2", - yanked = False, + yanked = None, ), struct( distribution = "foo", @@ -620,7 +790,7 @@ def _test_overlapping_shas_with_index_results(env): sha256 = "deadb11f", target_platforms = ["cp39_osx_x86_64"], url = "super2", - yanked = False, + yanked = None, ), ], ), @@ -629,6 +799,7 @@ def _test_overlapping_shas_with_index_results(env): _tests.append(_test_overlapping_shas_with_index_results) def _test_get_index_urls_different_versions(env): + """Test that different versions from index URLs are matched correctly per platform.""" got = parse_requirements( requirements_by_platform = { "requirements_multi_version": [ @@ -656,44 +827,52 @@ def _test_get_index_urls_different_versions(env): whl_platform_tags = ["any"], ), }, - get_index_urls = lambda _, __: { + get_index_urls = lambda _, __, **kwargs: { "foo": struct( + index_url = "", sdists = {}, whls = { "deadb11f": struct( url = "super2", sha256 = "deadb11f", filename = "foo-0.0.2-py3-none-any.whl", - yanked = False, + yanked = None, ), "deadbaaf": struct( url = "super2", sha256 = "deadbaaf", filename = "foo-0.0.1-py3-none-any.whl", - yanked = False, + yanked = None, ), }, ), }, - evaluate_markers = lambda _, requirements: evaluate_markers( - requirements = requirements, - platforms = { - "cp310_linux_x86_64": struct( - env = {"python_full_version": "3.10.0"}, - ), - "cp39_linux_x86_64": struct( - env = {"python_full_version": "3.9.0"}, - ), - }, - ), - debug = True, ) env.expect.that_collection(got).contains_exactly([ struct( + name = "boo", + index_url = "", + is_exposed = False, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "boo", + extra_pip_args = [], + filename = "", + requirement_line = "boo==0.0.4 --hash=sha256:deadbaaf", + sha256 = "", + target_platforms = ["cp39_linux_x86_64"], + url = "", + yanked = None, + ), + ], + ), + struct( + name = "foo", + index_url = "", is_exposed = True, is_multiple_versions = True, - name = "foo", srcs = [ struct( distribution = "foo", @@ -703,7 +882,7 @@ def _test_get_index_urls_different_versions(env): sha256 = "", target_platforms = ["cp39_linux_x86_64"], url = "", - yanked = False, + yanked = None, ), struct( distribution = "foo", @@ -713,7 +892,7 @@ def _test_get_index_urls_different_versions(env): sha256 = "deadb11f", target_platforms = ["cp310_linux_x86_64"], url = "super2", - yanked = False, + yanked = None, ), ], ), @@ -721,7 +900,53 @@ def _test_get_index_urls_different_versions(env): _tests.append(_test_get_index_urls_different_versions) +def _test_get_index_urls_cross_platform(env): + """Verifies that distributions from all requirement files are passed to ``get_index_urls``. + + This ensures the lockfile facts are platform-independent. + """ + calls = [] + + def _get_index_urls(_, distributions, **__): + calls.append({k: list(v) for k, v in distributions.items()}) + return {} + + parse_requirements( + requirements_by_platform = { + "requirements_osx": ["cp39_osx_x86_64"], + # requirements_windows has no matching platforms (simulating + # a macOS build where windows-specific files aren't used). + "requirements_windows": [], + }, + platforms = { + "cp39_osx_x86_64": struct( + env = pep508_env( + python_version = "3.9.0", + os = "osx", + arch = "x86_64", + ), + whl_abi_tags = ["none"], + whl_platform_tags = ["macosx_*_x86_64"], + ), + }, + get_index_urls = _get_index_urls, + ) + + # distributions must include packages from ALL files, even those with + # no matching platforms: + # - foo: 0.0.2 from requirements_windows, 0.0.3 from requirements_osx + # - bar: 0.0.1 from requirements_windows only + env.expect.that_collection(calls).contains_exactly([ + { + "bar": ["0.0.1"], + "foo": ["0.0.2", "0.0.3"], + }, + ]) + +_tests.append(_test_get_index_urls_cross_platform) + def _test_get_index_urls_single_py_version(env): + """Test index URL matching when only a single Python version is used.""" got = parse_requirements( requirements_by_platform = { "requirements_multi_version": [ @@ -739,35 +964,28 @@ def _test_get_index_urls_single_py_version(env): whl_platform_tags = ["any"], ), }, - get_index_urls = lambda _, __: { + get_index_urls = lambda _, __, **kwargs: { "foo": struct( + index_url = "", sdists = {}, whls = { "deadb11f": struct( url = "super2", sha256 = "deadb11f", filename = "foo-0.0.2-py3-none-any.whl", - yanked = False, + yanked = None, ), }, ), }, - evaluate_markers = lambda _, requirements: evaluate_markers( - requirements = requirements, - platforms = { - "cp310_linux_x86_64": struct( - env = {"python_full_version": "3.10.0"}, - ), - }, - ), - debug = True, ) env.expect.that_collection(got).contains_exactly([ struct( - is_exposed = True, - is_multiple_versions = True, name = "foo", + index_url = "", + is_exposed = True, + is_multiple_versions = False, srcs = [ struct( distribution = "foo", @@ -777,7 +995,7 @@ def _test_get_index_urls_single_py_version(env): sha256 = "deadb11f", target_platforms = ["cp310_linux_x86_64"], url = "super2", - yanked = False, + yanked = None, ), ], ), @@ -785,6 +1003,631 @@ def _test_get_index_urls_single_py_version(env): _tests.append(_test_get_index_urls_single_py_version) +def _test_get_index_urls_all_versions(env): + """Test that all versions from all requirement files are passed to ``get_index_urls``.""" + calls = [] + + def _get_index_urls(_, distributions, **__): + calls.append({k: list(v) for k, v in distributions.items()}) + return {} + + parse_requirements( + requirements_by_platform = { + "requirements_multi_version": ["cp39_linux_x86_64"], + }, + platforms = { + "cp39_linux_x86_64": struct( + env = pep508_env( + python_version = "3.9.0", + os = "linux", + arch = "x86_64", + ), + whl_abi_tags = ["none"], + whl_platform_tags = ["any"], + ), + }, + get_index_urls = _get_index_urls, + ) + + env.expect.that_collection(calls).contains_exactly([ + { + # boo should be also passed even though it is present on one platform. + "boo": ["0.0.4"], + # Both versions 0.0.1 and 0.0.2 should be passed to get_index_urls, even + # though only 0.0.1 matches the cp39_linux_x86_64 platform markers. + "foo": ["0.0.1", "0.0.2"], + }, + ]) + +_tests.append(_test_get_index_urls_all_versions) + +def _test_uv_lock_consistent(env): + """Test that uv_lock with requirements_by_platform uses correct platforms.""" + got = parse_requirements( + requirements_by_platform = { + "requirements_lock": ["linux_x86_64", "windows_x86_64"], + }, + uv_lock = "uv_lock_foo_with_extras", + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + index_url = "https://pypi.org/simple/foo", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo[extra]==0.0.1", + target_platforms = ["linux_x86_64", "windows_x86_64"], + filename = "foo-0.0.1-py3-none-any.whl", + sha256 = "deadbeef", + url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_uv_lock_consistent) + +def _test_uv_lock_primary_source(env): + """Test that uv.lock can be used as the sole source without requirements files.""" + got = parse_requirements( + uv_lock = "uv_lock_foo_sdist", + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + index_url = "https://pypi.org/simple/foo", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo==0.0.1", + target_platforms = ["linux_x86_64"], + filename = "foo-0.0.1-py3-none-any.whl", + sha256 = "deadbeef", + url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_uv_lock_primary_source) + +def _test_uv_lock_primary_source_multiple_versions(env): + """Test that uv.lock with multiple versions of the same package works.""" + got = parse_requirements( + uv_lock = "uv_lock_foo_multi_versions", + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + index_url = "https://pypi.org/simple/foo", + is_exposed = True, + is_multiple_versions = True, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo==0.0.1", + target_platforms = ["linux_x86_64"], + filename = "foo-0.0.1-py3-none-any.whl", + sha256 = "deadbeef", + url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", + yanked = None, + ), + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo==0.0.2", + target_platforms = ["linux_x86_64"], + filename = "foo-0.0.2-py3-none-any.whl", + sha256 = "deadb11f", + url = "https://files.pythonhosted.org/packages/foo-0.0.2-py3-none-any.whl", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_uv_lock_primary_source_multiple_versions) + +def _test_uv_lock_primary_source_with_extras(env): + """Test that uv.lock extras are included in requirement lines.""" + got = parse_requirements( + uv_lock = "uv_lock_foo_with_extras", + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + index_url = "https://pypi.org/simple/foo", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo[extra]==0.0.1", + target_platforms = ["linux_x86_64"], + filename = "foo-0.0.1-py3-none-any.whl", + sha256 = "deadbeef", + url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_uv_lock_primary_source_with_extras) + +def _test_uv_lock_primary_source_includes_virtual(env): + """Test that virtual packages in uv.lock are included.""" + got = parse_requirements( + uv_lock = "uv_lock_foo_virtual", + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + index_url = "https://pypi.org/simple/foo", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo==0.0.1", + target_platforms = ["linux_x86_64"], + filename = "foo-0.0.1-py3-none-any.whl", + sha256 = "deadbeef", + url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", + yanked = None, + ), + ], + ), + struct( + name = "virtual_pkg", + index_url = "", + is_exposed = True, + is_multiple_versions = False, + srcs = [], + ), + ]) + +_tests.append(_test_uv_lock_primary_source_includes_virtual) + +def _test_uv_lock_cross_consistent(env): + """Test that the uv.lock and requirements work together for cross-platform.""" + got = parse_requirements( + requirements_by_platform = { + "requirements_lock": ["linux_x86_64", "windows_x86_64"], + }, + uv_lock = "uv_lock_foo_with_extras", + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + index_url = "https://pypi.org/simple/foo", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo[extra]==0.0.1", + target_platforms = ["linux_x86_64", "windows_x86_64"], + filename = "foo-0.0.1-py3-none-any.whl", + sha256 = "deadbeef", + url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_uv_lock_cross_consistent) + +def _test_uv_lock_vcs_entry(env): + """Test that VCS entries in uv.lock are handled without crashing.""" + got = parse_requirements( + uv_lock = "uv_lock_git_vcs", + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + index_url = "", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo==0.1.0", + target_platforms = ["linux_x86_64"], + filename = "foo.git", + sha256 = "", + url = "https://github.com/org/foo.git", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_uv_lock_vcs_entry) + +def _test_uv_lock_rules_python_pkg_not_skipped(env): + """Test that 'rules_python' package is not skipped from uv.lock.""" + got = parse_requirements( + uv_lock = "uv_lock_rules_python_pkg", + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "rules_python", + index_url = "https://pypi.org/simple/rules-python", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "rules_python", + extra_pip_args = [], + requirement_line = "rules_python==0.0.1", + target_platforms = ["linux_x86_64"], + filename = "rules_python-0.0.1-py3-none-any.whl", + sha256 = "deadbeef", + url = "https://files.pythonhosted.org/packages/rules_python-0.0.1-py3-none-any.whl", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_uv_lock_rules_python_pkg_not_skipped) + +def _test_uv_lock_no_consistency_check(env): + """Test that uv.lock is used as the primary source when both uv.lock and requirements exist.""" + got = parse_requirements( + requirements_by_platform = { + "requirements_lock": ["linux_x86_64"], + }, + uv_lock = "uv_lock_foo", + ) + + # The result comes from uv.lock (no extras since uv_lock_foo doesn't have provides-extras) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + index_url = "https://pypi.org/simple/foo", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo==0.0.1", + target_platforms = ["linux_x86_64"], + filename = "foo-0.0.1-py3-none-any.whl", + sha256 = "deadbeef", + url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_uv_lock_no_consistency_check) + +def _test_uv_lock_multiple_packages(env): + """Test that multiple packages from uv.lock are all returned.""" + got = parse_requirements( + uv_lock = "uv_lock_foo_bar", + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "bar", + index_url = "https://pypi.org/simple/bar", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "bar", + extra_pip_args = [], + requirement_line = "bar==0.0.1", + target_platforms = ["linux_x86_64"], + filename = "bar-0.0.1.tar.gz", + sha256 = "deadb00f", + url = "https://files.pythonhosted.org/packages/bar-0.0.1.tar.gz", + yanked = None, + ), + ], + ), + struct( + name = "foo", + index_url = "https://pypi.org/simple/foo", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo==0.0.1", + target_platforms = ["linux_x86_64"], + filename = "foo-0.0.1-py3-none-any.whl", + sha256 = "deadbeef", + url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_uv_lock_multiple_packages) + +def _test_uv_lock_with_extra_pip_args(env): + """Test that extra_pip_args are passed through with uv.lock.""" + got = parse_requirements( + uv_lock = "uv_lock_foo", + extra_pip_args = ["--index-url=example.org"], + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + index_url = "https://pypi.org/simple/foo", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = ["--index-url=example.org"], + requirement_line = "foo==0.0.1", + target_platforms = ["linux_x86_64"], + filename = "foo-0.0.1-py3-none-any.whl", + sha256 = "deadbeef", + url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_uv_lock_with_extra_pip_args) + +def _test_uv_lock_multi_os_with_requirements(env): + """Test that uv.lock works with requirements_by_platform for multi-platform.""" + got = parse_requirements( + requirements_by_platform = { + "requirements_foo": ["linux_aarch64"], + "requirements_lock": ["linux_x86_64", "windows_x86_64"], + }, + uv_lock = "uv_lock_foo", + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + index_url = "https://pypi.org/simple/foo", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo==0.0.1", + target_platforms = ["linux_aarch64", "linux_x86_64", "windows_x86_64"], + filename = "foo-0.0.1-py3-none-any.whl", + sha256 = "deadbeef", + url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_uv_lock_multi_os_with_requirements) + +def _test_uv_lock_extras_optional_deps(env): + """Test that extras from optional-dependencies in uv.lock are included.""" + got = parse_requirements( + uv_lock = "uv_lock_foo_optional_deps", + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + index_url = "https://pypi.org/simple/foo", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo[extra1,extra2]==0.0.1", + target_platforms = ["linux_x86_64"], + filename = "foo-0.0.1-py3-none-any.whl", + sha256 = "deadbeef", + url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_uv_lock_extras_optional_deps) + +def _test_uv_lock_extras_dep_edge(env): + """Test that dep extra edges in uv.lock add extras to the dependency.""" + got = parse_requirements( + uv_lock = "uv_lock_foo_dep_extra", + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "bar", + index_url = "https://pypi.org/simple/bar", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "bar", + extra_pip_args = [], + requirement_line = "bar[extra1]==0.0.2", + target_platforms = ["linux_x86_64"], + filename = "bar-0.0.2-py3-none-any.whl", + sha256 = "deadbeef", + url = "https://files.pythonhosted.org/packages/bar-0.0.2-py3-none-any.whl", + yanked = None, + ), + ], + ), + struct( + name = "foo", + index_url = "https://pypi.org/simple/foo", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo==0.0.1", + target_platforms = ["linux_x86_64"], + filename = "foo-0.0.1-py3-none-any.whl", + sha256 = "baadbeef", + url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_uv_lock_extras_dep_edge) + +def _test_uv_lock_wheel_dedup_single_version(env): + """Test that overlapping wheels for a single version are deduplicated to one per platform.""" + got = parse_requirements( + uv_lock = "uv_lock_foo_multi_wheel_dedup", + platforms = { + "cp39_linux_x86_64": struct( + env = pep508_env(python_version = "3.9.0", os = "linux", arch = "x86_64"), + whl_abi_tags = ["none", "abi3", "cp39"], + whl_platform_tags = ["any", "linux_x86_64", "manylinux_*_x86_64"], + ), + }, + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + index_url = "https://pypi.org/simple/foo", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo==0.0.1", + target_platforms = ["cp39_linux_x86_64"], + filename = "foo-0.0.1-cp39-cp39-manylinux_2_17_x86_64.whl", + sha256 = "aaa", + url = "https://files.pythonhosted.org/packages/foo-0.0.1-cp39-cp39-manylinux_2_17_x86_64.whl", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_uv_lock_wheel_dedup_single_version) + +def _test_uv_lock_wheel_dedup_resolution_markers(env): + """Test that resolution-markers filtering and wheel dedup work together. + + Two versions of foo with resolution-markers for different platforms. + Each version has a platform-specific wheel and a generic py3-none-any wheel. + The dedup should pick the platform-specific wheel for each platform and + the resolution-markers should split versions across platforms. + """ + got = parse_requirements( + uv_lock = "uv_lock_foo_resolution_markers_dedup", + platforms = { + "cp39_linux_x86_64": struct( + env = pep508_env(python_version = "3.9.0", os = "linux", arch = "x86_64"), + whl_abi_tags = ["none", "abi3", "cp39"], + whl_platform_tags = ["any", "linux_x86_64", "manylinux_*_x86_64"], + ), + "cp39_osx_aarch64": struct( + env = pep508_env(python_version = "3.9.0", os = "osx", arch = "aarch64"), + whl_abi_tags = ["none", "abi3", "cp39"], + whl_platform_tags = ["any", "macosx_*_arm64"], + ), + }, + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + index_url = "https://pypi.org/simple/foo", + is_exposed = True, + is_multiple_versions = True, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo==0.0.1", + target_platforms = ["cp39_linux_x86_64"], + filename = "foo-0.0.1-cp39-cp39-manylinux_2_17_x86_64.whl", + sha256 = "aaa", + url = "https://files.pythonhosted.org/packages/foo-0.0.1-cp39-cp39-manylinux_2_17_x86_64.whl", + yanked = None, + ), + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo==0.0.2", + target_platforms = ["cp39_osx_aarch64"], + filename = "foo-0.0.2-cp39-cp39-macosx_11_0_arm64.whl", + sha256 = "ccc", + url = "https://files.pythonhosted.org/packages/foo-0.0.2-cp39-cp39-macosx_11_0_arm64.whl", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_uv_lock_wheel_dedup_resolution_markers) + +def _test_uv_lock_requires_dist_extras(env): + """Test that extras from metadata.requires-dist appear in requirement_line.""" + got = parse_requirements( + uv_lock = "uv_lock_foo_requires_dist_extras", + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + index_url = "https://pypi.org/simple/foo", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo[all]==0.0.1", + target_platforms = ["linux_x86_64"], + filename = "foo-0.0.1-py3-none-any.whl", + sha256 = "deadbeef", + url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", + yanked = None, + ), + ], + ), + struct( + name = "root_pkg", + index_url = "", + is_exposed = True, + is_multiple_versions = False, + srcs = [], + ), + ]) + +_tests.append(_test_uv_lock_requires_dist_extras) + def parse_requirements_test_suite(name): """Create the test suite. diff --git a/tests/pypi/parse_simpleapi_html/parse_simpleapi_html_tests.bzl b/tests/pypi/parse_simpleapi_html/parse_simpleapi_html_tests.bzl index b96d02f990..c84140f459 100644 --- a/tests/pypi/parse_simpleapi_html/parse_simpleapi_html_tests.bzl +++ b/tests/pypi/parse_simpleapi_html/parse_simpleapi_html_tests.bzl @@ -42,6 +42,29 @@ def _generate_html(*items): ]), ) +def _test_index(env): + # buildifier: disable=unsorted-dict-items + tests = [ + ( + [ + struct(attrs = ['href="/simple/foo/"'], filename = "foo"), + struct(attrs = ['href="./b-ar/"'], filename = "b-._.-aR"), + ], + { + "b_ar": "./b-ar/", + "foo": "/simple/foo/", + }, + ), + ] + + for (input, want) in tests: + html = _generate_html(*input) + got = parse_simpleapi_html(content = html, parse_index = True) + + env.expect.that_dict(got).contains_exactly(want) + +_tests.append(_test_index) + def _test_sdist(env): # buildifier: disable=unsorted-dict-items tests = [ @@ -52,13 +75,12 @@ def _test_sdist(env): 'data-requires-python=">=3.7"', ], filename = "foo-0.0.1.tar.gz", - url = "foo", ), struct( filename = "foo-0.0.1.tar.gz", sha256 = "deadbeefasource", url = "https://example.org/full-url/foo-0.0.1.tar.gz", - yanked = False, + yanked = None, version = "0.0.1", ), ), @@ -67,23 +89,58 @@ def _test_sdist(env): attrs = [ 'href="https://example.org/full-url/foo-0.0.1.tar.gz#sha256=deadbeefasource"', 'data-requires-python=">=3.7"', + "data-yanked", + ], + filename = "foo-0.0.1.tar.gz", + ), + struct( + filename = "foo-0.0.1.tar.gz", + sha256 = "deadbeefasource", + url = "https://example.org/full-url/foo-0.0.1.tar.gz", + version = "0.0.1", + yanked = "", + ), + ), + ( + struct( + attrs = [ + 'href="https://example.org/full-url/foo-0.0.1.tar.gz#sha256=deadbeefasource"', + 'data-requires-python="<=3.7"', + "data-yanked=\"Something with "quotes" over two lines\"", ], filename = "foo-0.0.1.tar.gz", - url = "foo", ), struct( filename = "foo-0.0.1.tar.gz", sha256 = "deadbeefasource", url = "https://example.org/full-url/foo-0.0.1.tar.gz", version = "0.0.1", - yanked = False, + # NOTE @aignas 2026-03-09: we preserve the white space + yanked = "Something \nwith \"quotes\"\nover two lines", + ), + ), + ( + struct( + attrs = [ + 'href="https://example.org/full-url/foo-0.0.1.tar.gz#sha256=deadbeefasource"', + 'data-requires-python=">=3.7"', + 'data-yanked=""', + ], + filename = "foo-0.0.1.tar.gz", + ), + struct( + filename = "foo-0.0.1.tar.gz", + sha256 = "deadbeefasource", + url = "https://example.org/full-url/foo-0.0.1.tar.gz", + version = "0.0.1", + yanked = "", ), ), ] for (input, want) in tests: html = _generate_html(input) - got = parse_simpleapi_html(url = input.url, content = html) + got = parse_simpleapi_html(content = html) env.expect.that_collection(got.sdists).has_size(1) env.expect.that_collection(got.whls).has_size(0) env.expect.that_collection(got.sha256s_by_version).has_size(1) @@ -96,7 +153,7 @@ def _test_sdist(env): filename = subjects.str, sha256 = subjects.str, url = subjects.str, - yanked = subjects.bool, + yanked = subjects.str, version = subjects.str, ), ) @@ -120,7 +177,6 @@ def _test_whls(env): 'data-core-metadata="sha256=deadb00f"', ], filename = "foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", - url = "foo", ), struct( filename = "foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", @@ -129,19 +185,18 @@ def _test_whls(env): sha256 = "deadbeef", url = "https://example.org/full-url/foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", version = "0.0.2", - yanked = False, + yanked = None, ), ), ( struct( attrs = [ 'href="https://example.org/full-url/foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=deadbeef"', - 'data-requires-python=">=3.7"', + 'data-requires-python=">=3.7"', 'data-dist-info-metadata="sha256=deadb00f"', 'data-core-metadata="sha256=deadb00f"', ], filename = "foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", - url = "foo", ), struct( filename = "foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", @@ -150,7 +205,7 @@ def _test_whls(env): sha256 = "deadbeef", url = "https://example.org/full-url/foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", version = "0.0.2", - yanked = False, + yanked = None, ), ), ( @@ -161,7 +216,6 @@ def _test_whls(env): 'data-core-metadata="sha256=deadb00f"', ], filename = "foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", - url = "foo", ), struct( filename = "foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", @@ -170,7 +224,7 @@ def _test_whls(env): sha256 = "deadbeef", version = "0.0.2", url = "https://example.org/full-url/foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", - yanked = False, + yanked = None, ), ), ( @@ -181,7 +235,6 @@ def _test_whls(env): 'data-dist-info-metadata="sha256=deadb00f"', ], filename = "foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", - url = "foo", ), struct( filename = "foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", @@ -190,7 +243,7 @@ def _test_whls(env): sha256 = "deadbeef", version = "0.0.2", url = "https://example.org/full-url/foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", - yanked = False, + yanked = None, ), ), ( @@ -200,7 +253,6 @@ def _test_whls(env): 'data-requires-python=">=3.7"', ], filename = "foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", - url = "foo", ), struct( filename = "foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", @@ -209,125 +261,14 @@ def _test_whls(env): sha256 = "deadbeef", url = "https://example.org/full-url/foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", version = "0.0.2", - yanked = False, - ), - ), - ( - struct( - attrs = [ - 'href="../../foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl#sha256=deadbeef"', - 'data-requires-python=">=3.7"', - 'data-dist-info-metadata="sha256=deadb00f"', - ], - filename = "foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", - url = "https://example.org/python-wheels/bar/foo/", - ), - struct( - filename = "foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", - metadata_sha256 = "deadb00f", - metadata_url = "https://example.org/python-wheels/foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata", - sha256 = "deadbeef", - version = "0.0.2", - url = "https://example.org/python-wheels/foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", - yanked = False, - ), - ), - ( - struct( - attrs = [ - 'href="/whl/torch-2.0.0-cp38-cp38-manylinux2014_aarch64.whl#sha256=deadbeef"', - ], - filename = "torch-2.0.0-cp38-cp38-manylinux2014_aarch64.whl", - url = "https://download.pytorch.org/whl/cpu/torch", - ), - struct( - filename = "torch-2.0.0-cp38-cp38-manylinux2014_aarch64.whl", - metadata_sha256 = "", - metadata_url = "", - sha256 = "deadbeef", - url = "https://download.pytorch.org/whl/torch-2.0.0-cp38-cp38-manylinux2014_aarch64.whl", - version = "2.0.0", - yanked = False, - ), - ), - ( - struct( - attrs = [ - 'href="/whl/torch-2.0.0-cp38-cp38-manylinux2014_aarch64.whl#sha256=notdeadbeef"', - ], - filename = "torch-2.0.0-cp38-cp38-manylinux2014_aarch64.whl", - url = "http://download.pytorch.org/whl/cpu/torch", - ), - struct( - filename = "torch-2.0.0-cp38-cp38-manylinux2014_aarch64.whl", - metadata_sha256 = "", - metadata_url = "", - sha256 = "notdeadbeef", - url = "http://download.pytorch.org/whl/torch-2.0.0-cp38-cp38-manylinux2014_aarch64.whl", - version = "2.0.0", - yanked = False, - ), - ), - ( - struct( - attrs = [ - 'href="1.0.0/mypy_extensions-1.0.0-py3-none-any.whl#sha256=deadbeef"', - ], - filename = "mypy_extensions-1.0.0-py3-none-any.whl", - url = "https://example.org/simple/mypy_extensions", - ), - struct( - filename = "mypy_extensions-1.0.0-py3-none-any.whl", - metadata_sha256 = "", - metadata_url = "", - version = "1.0.0", - sha256 = "deadbeef", - url = "https://example.org/simple/mypy_extensions/1.0.0/mypy_extensions-1.0.0-py3-none-any.whl", - yanked = False, - ), - ), - ( - struct( - attrs = [ - 'href="unknown://example.com/mypy_extensions-1.0.0-py3-none-any.whl#sha256=deadbeef"', - ], - filename = "mypy_extensions-1.0.0-py3-none-any.whl", - url = "https://example.org/simple/mypy_extensions", - ), - struct( - filename = "mypy_extensions-1.0.0-py3-none-any.whl", - metadata_sha256 = "", - metadata_url = "", - sha256 = "deadbeef", - version = "1.0.0", - url = "https://example.org/simple/mypy_extensions/unknown://example.com/mypy_extensions-1.0.0-py3-none-any.whl", - yanked = False, - ), - ), - ( - struct( - attrs = [ - 'href="/whl/cpu/torch-2.6.0%2Bcpu-cp39-cp39-manylinux_2_28_aarch64.whl#sha256=deadbeef"', - ], - filename = "torch-2.6.0+cpu-cp39-cp39-manylinux_2_28_aarch64.whl", - url = "https://example.org/", - ), - struct( - filename = "torch-2.6.0+cpu-cp39-cp39-manylinux_2_28_aarch64.whl", - metadata_sha256 = "", - metadata_url = "", - sha256 = "deadbeef", - version = "2.6.0+cpu", - # A URL with % could occur if directly written in requirements. - url = "https://example.org/whl/cpu/torch-2.6.0%2Bcpu-cp39-cp39-manylinux_2_28_aarch64.whl", - yanked = False, + yanked = None, ), ), ] for (input, want) in tests: html = _generate_html(input) - got = parse_simpleapi_html(url = input.url, content = html) + got = parse_simpleapi_html(content = html) env.expect.that_collection(got.sdists).has_size(0) env.expect.that_collection(got.whls).has_size(1) if not got: @@ -341,7 +282,7 @@ def _test_whls(env): metadata_url = subjects.str, sha256 = subjects.str, url = subjects.str, - yanked = subjects.bool, + yanked = subjects.str, version = subjects.str, ), ) diff --git a/tests/pypi/patch_whl/BUILD.bazel b/tests/pypi/patch_whl/BUILD.bazel index d6c4f47b36..cfdc3cc12a 100644 --- a/tests/pypi/patch_whl/BUILD.bazel +++ b/tests/pypi/patch_whl/BUILD.bazel @@ -1,3 +1,14 @@ +load("//python:py_test.bzl", "py_test") +load("//tests/support:support.bzl", "SUPPORTS_BZLMOD") load(":patch_whl_tests.bzl", "patch_whl_test_suite") patch_whl_test_suite(name = "patch_whl_tests") + +py_test( + name = "patch_whl_patch_test", + srcs = ["patch_whl_patch_test.py"], + target_compatible_with = SUPPORTS_BZLMOD, + deps = [ + "@patch_whl_pkg//:pkg", + ], +) diff --git a/tests/pypi/patch_whl/patch_whl_patch_test.py b/tests/pypi/patch_whl/patch_whl_patch_test.py new file mode 100644 index 0000000000..56dfe1aa55 --- /dev/null +++ b/tests/pypi/patch_whl/patch_whl_patch_test.py @@ -0,0 +1,17 @@ +"""Test that wheel patching works end-to-end.""" + +import unittest + +import pkg + + +class PatchWhlTest(unittest.TestCase): + def test_patched(self): + self.assertEqual(pkg.PATCHED, True) + + def test_data_unchanged(self): + self.assertEqual(pkg.DATA, "hello") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pypi/patch_whl/patch_whl_tests.bzl b/tests/pypi/patch_whl/patch_whl_tests.bzl index f93fe459c9..5010f03dc0 100644 --- a/tests/pypi/patch_whl/patch_whl_tests.bzl +++ b/tests/pypi/patch_whl/patch_whl_tests.bzl @@ -15,7 +15,7 @@ "" load("@rules_testing//lib:test_suite.bzl", "test_suite") -load("//python/private/pypi:patch_whl.bzl", "patched_whl_name") # buildifier: disable=bzl-visibility +load("//python/private/pypi:patch_whl.bzl", "fix_record_content", "patched_whl_name") # buildifier: disable=bzl-visibility _tests = [] @@ -31,6 +31,104 @@ def _test_simple_local_version(env): _tests.append(_test_simple_local_version) +def _test_fix_record_adds_missing(env): + record = """\ +foo/__init__.py,sha256=abc,123 +foo-1.0.dist-info/RECORD,, +""" + all_files = { + "foo-1.0.dist-info/METADATA": True, + "foo-1.0.dist-info/RECORD": True, + "foo/__init__.py": True, + "foo/bar.py": True, + } + result = fix_record_content( + record_content = record, + all_files = all_files, + record_rel = "foo-1.0.dist-info/RECORD", + ) + env.expect.that_str(result).contains("foo/bar.py,sha256=0,0") + env.expect.that_str(result).contains("foo-1.0.dist-info/METADATA,sha256=0,0") + +_tests.append(_test_fix_record_adds_missing) + +def _test_fix_record_no_missing(env): + record = """\ +foo/__init__.py,sha256=abc,123 +foo/bar.py,sha256=def,456 +foo-1.0.dist-info/RECORD,, +""" + all_files = { + "foo-1.0.dist-info/RECORD": True, + "foo/__init__.py": True, + "foo/bar.py": True, + } + result = fix_record_content( + record_content = record, + all_files = all_files, + record_rel = "foo-1.0.dist-info/RECORD", + ) + env.expect.that_bool(result == None).equals(True) + +_tests.append(_test_fix_record_no_missing) + +def _test_fix_record_preserves_quoting(env): + record = '''\ +"foo/__init__.py",sha256=abc,123 +"foo-1.0.dist-info/RECORD",, +''' + all_files = { + "foo-1.0.dist-info/RECORD": True, + "foo/__init__.py": True, + "foo/bar.py": True, + } + result = fix_record_content( + record_content = record, + all_files = all_files, + record_rel = "foo-1.0.dist-info/RECORD", + ) + env.expect.that_str(result).contains('"foo/bar.py",sha256=0,0') + +_tests.append(_test_fix_record_preserves_quoting) + +def _test_fix_record_skips_excluded(env): + record = """\ +foo/__init__.py,sha256=abc,123 +foo-1.0.dist-info/RECORD,, +""" + all_files = { + "foo-1.0.dist-info/INSTALLER": True, + "foo-1.0.dist-info/RECORD": True, + "foo/__init__.py": True, + } + result = fix_record_content( + record_content = record, + all_files = all_files, + record_rel = "foo-1.0.dist-info/RECORD", + ) + env.expect.that_bool(result == None).equals(True) + +_tests.append(_test_fix_record_skips_excluded) + +def _test_fix_record_skips_whl_files(env): + record = """\ +foo/__init__.py,sha256=abc,123 +foo-1.0.dist-info/RECORD,, +""" + all_files = { + "foo-1.0.dist-info/RECORD": True, + "foo-1.0.whl": True, + "foo/__init__.py": True, + } + result = fix_record_content( + record_content = record, + all_files = all_files, + record_rel = "foo-1.0.dist-info/RECORD", + ) + env.expect.that_bool(result == None).equals(True) + +_tests.append(_test_fix_record_skips_whl_files) + def patch_whl_test_suite(name): """Create the test suite. diff --git a/tests/pypi/patch_whl/testdata/BUILD.bazel b/tests/pypi/patch_whl/testdata/BUILD.bazel new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/pypi/patch_whl/testdata/patches/BUILD.bazel b/tests/pypi/patch_whl/testdata/patches/BUILD.bazel new file mode 100644 index 0000000000..85f0d05223 --- /dev/null +++ b/tests/pypi/patch_whl/testdata/patches/BUILD.bazel @@ -0,0 +1 @@ +exports_files(["modify_pkg.patch"]) diff --git a/tests/pypi/patch_whl/testdata/patches/modify_pkg.patch b/tests/pypi/patch_whl/testdata/patches/modify_pkg.patch new file mode 100644 index 0000000000..ba8cca54b5 --- /dev/null +++ b/tests/pypi/patch_whl/testdata/patches/modify_pkg.patch @@ -0,0 +1,6 @@ +--- pkg.py ++++ pkg.py +@@ -1,2 +1,2 @@ +-PATCHED = False ++PATCHED = True + DATA = "hello" diff --git a/tests/pypi/patch_whl/testdata/pkg/BUILD.bazel b/tests/pypi/patch_whl/testdata/pkg/BUILD.bazel new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/pypi/patch_whl/testdata/pkg/pkg-1.0.dist-info/METADATA b/tests/pypi/patch_whl/testdata/pkg/pkg-1.0.dist-info/METADATA new file mode 100644 index 0000000000..a102dd73d4 --- /dev/null +++ b/tests/pypi/patch_whl/testdata/pkg/pkg-1.0.dist-info/METADATA @@ -0,0 +1,2 @@ +Name: pkg +Version: 1.0 diff --git a/tests/pypi/patch_whl/testdata/pkg/pkg-1.0.dist-info/RECORD b/tests/pypi/patch_whl/testdata/pkg/pkg-1.0.dist-info/RECORD new file mode 100644 index 0000000000..dba333a1c1 --- /dev/null +++ b/tests/pypi/patch_whl/testdata/pkg/pkg-1.0.dist-info/RECORD @@ -0,0 +1,4 @@ +pkg.py,sha256=abc,123 +pkg-1.0.dist-info/METADATA,sha256=def,456 +pkg-1.0.dist-info/WHEEL,sha256=ghi,789 +pkg-1.0.dist-info/RECORD,, diff --git a/tests/pypi/patch_whl/testdata/pkg/pkg-1.0.dist-info/WHEEL b/tests/pypi/patch_whl/testdata/pkg/pkg-1.0.dist-info/WHEEL new file mode 100644 index 0000000000..a64521a1cc --- /dev/null +++ b/tests/pypi/patch_whl/testdata/pkg/pkg-1.0.dist-info/WHEEL @@ -0,0 +1 @@ +Wheel-Version: 1.0 diff --git a/tests/pypi/patch_whl/testdata/pkg/pkg.py b/tests/pypi/patch_whl/testdata/pkg/pkg.py new file mode 100644 index 0000000000..0985d5a3dd --- /dev/null +++ b/tests/pypi/patch_whl/testdata/pkg/pkg.py @@ -0,0 +1,2 @@ +PATCHED = False +DATA = "hello" diff --git a/tests/pypi/pep508/deps_tests.bzl b/tests/pypi/pep508/deps_tests.bzl index aaa3b2f7dd..e88acb8c56 100644 --- a/tests/pypi/pep508/deps_tests.bzl +++ b/tests/pypi/pep508/deps_tests.bzl @@ -90,6 +90,7 @@ def test_self_dependencies_can_come_in_any_order(env): "baz; extra == 'feat'", "foo[feat2]; extra == 'all'", "foo[feat]; extra == 'feat2'", + "foo[feat3]; extra == 'all'", "zdep; extra == 'all'", ], extras = ["all"], @@ -100,6 +101,24 @@ def test_self_dependencies_can_come_in_any_order(env): _tests.append(test_self_dependencies_can_come_in_any_order) +def test_self_include_deps_from_previously_visited(env): + got = deps( + "foo", + requires_dist = [ + "bar", + "baz; extra == 'feat'", + "foo[dev]; extra == 'all'", + "foo[feat]; extra == 'feat2'", + "dev_dep; extra == 'dev'", + ], + extras = ["feat2"], + ) + + env.expect.that_collection(got.deps).contains_exactly(["bar", "baz"]) + env.expect.that_dict(got.deps_select).contains_exactly({}) + +_tests.append(test_self_include_deps_from_previously_visited) + def _test_can_get_deps_based_on_specific_python_version(env): requires_dist = [ "bar", @@ -161,6 +180,76 @@ def test_all_markers_are_added(env): _tests.append(test_all_markers_are_added) +def test_extra_with_conditional_and_unconditional_markers(env): + requires_dist = [ + "bar", + 'baz!=1.56.0; sys_platform == "darwin" and extra == "client"', + 'baz; extra == "client"', + ] + + got = deps( + "foo", + extras = ["client"], + requires_dist = requires_dist, + ) + + env.expect.that_collection(got.deps).contains_exactly(["bar", "baz"]) + env.expect.that_dict(got.deps_select).contains_exactly({}) + +_tests.append(test_extra_with_conditional_and_unconditional_markers) + +def test_span_all_python_versions(env): + requires_dist = [ + "bar>=0.4.0; python_version >= \"3.13.0\"", + "bar>=0.3.0; python_version ~= \"3.12.0\"", + "bar>=0.2.0; python_version ~= \"3.11.0\"", + "bar>=0.1.0; python_version < \"3.11\"", + ] + + got = deps( + "foo", + requires_dist = requires_dist, + ) + + env.expect.that_collection(got.deps).contains_exactly([]) + env.expect.that_dict(got.deps_select).contains_exactly({ + "bar": "(python_version < \"3.11\") or (python_version >= \"3.13.0\") or (python_version ~= \"3.11.0\") or (python_version ~= \"3.12.0\")", + }) + +_tests.append(test_span_all_python_versions) + +def test_extras_with_hyphens_are_normalized(env): + """Test that extras with hyphens in marker expressions are normalized. + + When wheel METADATA uses hyphens in marker expressions + (e.g., extra == "db-backend") but the extras from requirement parsing + are already normalized (e.g., "db_backend"), the deps should still + resolve because marker evaluation normalizes per PEP 685. + + Args: + env: the test environment. + """ + requires_dist = [ + "bar", + 'baz-lib; extra == "db-backend"', + 'qux-async; extra == "async-driver"', + ] + + got = deps( + "foo", + extras = ["db_backend", "async_driver"], + requires_dist = requires_dist, + ) + + env.expect.that_collection(got.deps).contains_exactly([ + "bar", + "baz_lib", + "qux_async", + ]) + env.expect.that_dict(got.deps_select).contains_exactly({}) + +_tests.append(test_extras_with_hyphens_are_normalized) + def deps_test_suite(name): # buildifier: disable=function-docstring test_suite( name = name, diff --git a/tests/pypi/pep508/evaluate_tests.bzl b/tests/pypi/pep508/evaluate_tests.bzl index 7843f88e89..7c7b5d8173 100644 --- a/tests/pypi/pep508/evaluate_tests.bzl +++ b/tests/pypi/pep508/evaluate_tests.bzl @@ -312,9 +312,10 @@ _MISC_EXPRESSIONS = [ # https://packaging.python.org/en/latest/specifications/version-specifiers/#compatible-release _expr_case('python_version ~= "2.2"', True, {"python_version": "2.3"}), _expr_case('python_version ~= "2.2"', False, {"python_version": "2.1"}), + _expr_case('python_version ~= "2.2.0"', True, {"python_version": "2.2"}), _expr_case('python_version ~= "2.2.post3"', False, {"python_version": "2.2"}), - _expr_case('python_version ~= "2.2.post3"', True, {"python_version": "2.3"}), _expr_case('python_version ~= "2.2.post3"', False, {"python_version": "3.0"}), + _expr_case('python_version ~= "2.2.post3"', True, {"python_version": "2.3"}), _expr_case('python_version ~= "1!2.2"', False, {"python_version": "2.7"}), _expr_case('python_version ~= "0!2.2"', True, {"python_version": "2.7"}), _expr_case('python_version ~= "1!2.2"', True, {"python_version": "1!2.7"}), diff --git a/tests/pypi/pep508/requirement_tests.bzl b/tests/pypi/pep508/requirement_tests.bzl index 9afb43a437..2ce45922da 100644 --- a/tests/pypi/pep508/requirement_tests.bzl +++ b/tests/pypi/pep508/requirement_tests.bzl @@ -23,9 +23,10 @@ def _test_requirement_line_parsing(env): " name1[ foo ] ": ("name1", ["foo"], None, ""), "Name[foo]": ("name", ["foo"], None, ""), "name [fred,bar] @ http://foo.com ; python_version=='2.7'": ("name", ["fred", "bar"], None, "python_version=='2.7'"), - "name; (os_name=='a' or os_name=='b') and os_name=='c'": ("name", [""], None, "(os_name=='a' or os_name=='b') and os_name=='c'"), - "name@http://foo.com": ("name", [""], None, ""), - "name[ Foo123 ]": ("name", ["Foo123"], None, ""), + "name; (os_name=='a' or os_name=='b') and os_name=='c'": ("name", [], None, "(os_name=='a' or os_name=='b') and os_name=='c'"), + "name@http://foo.com": ("name", [], None, ""), + "name[ Foo123 ]": ("name", ["foo123"], None, ""), + "name[extra-one,extra-two.three]==1.0": ("name", ["extra_one", "extra_two_three"], "1.0", ""), "name[extra]@http://foo.com": ("name", ["extra"], None, ""), "name[foo]": ("name", ["foo"], None, ""), "name[quux, strange];python_version<'2.7' and platform_version=='2'": ("name", ["quux", "strange"], None, "python_version<'2.7' and platform_version=='2'"), diff --git a/tests/pypi/pypi_cache/BUILD.bazel b/tests/pypi/pypi_cache/BUILD.bazel new file mode 100644 index 0000000000..03c20623cd --- /dev/null +++ b/tests/pypi/pypi_cache/BUILD.bazel @@ -0,0 +1,5 @@ +load(":pypi_cache_tests.bzl", "pypi_cache_test_suite") + +pypi_cache_test_suite( + name = "pypi_cache_tests", +) diff --git a/tests/pypi/pypi_cache/pypi_cache_tests.bzl b/tests/pypi/pypi_cache/pypi_cache_tests.bzl new file mode 100644 index 0000000000..14c12ae6d2 --- /dev/null +++ b/tests/pypi/pypi_cache/pypi_cache_tests.bzl @@ -0,0 +1,496 @@ +"" + +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("@rules_testing//lib:truth.bzl", "subjects") +load("//python/private/pypi:pypi_cache.bzl", "pypi_cache") # buildifier: disable=bzl-visibility +load("//tests/support/mocks:mocks.bzl", "mocks") + +_tests = [] + +def _cache(env, **kwargs): + cache = pypi_cache(**kwargs) + + attrs = { + "sdists": subjects.dict, + "sha256s_by_version": subjects.dict, + "whls": subjects.dict, + } + + def _expect(value): + if not value: + return env.expect.that_str(value) + + if type(value) == "dict": + return env.expect.that_dict(value) + + return env.expect.that_struct( + value, + attrs = attrs, + ) + + return struct( + setdefault = lambda *args, **kwargs: _expect( + cache.setdefault(*args, **kwargs), + ), + get = lambda *args, **kwargs: _expect( + cache.get(*args, **kwargs), + ), + get_facts = lambda: env.expect.that_dict(cache.get_facts()), + ) + +def _test_memory_cache_hit(env): + """Verifies that the cache returns stored values for the same real_url.""" + store = {} + + # We pass None for module_ctx to focus solely on memory_cache behavior + cache = _cache(env, mctx = None, store = store) + + # Mocked parsed result from a PyPI-like index + fake_result = struct( + sdists = { + "sha_1": struct(version = "1.0.0", filename = "pkg-1.0.0.tar.gz"), + }, + whls = { + "sha_2": struct(version = "1.1.0", filename = "pkg-1.1.0-py3-none-any.whl"), + }, + sha256s_by_version = { + "1.0.0": ["sha_1"], + "1.1.0": ["sha_2"], + }, + ) + + # Key format: (index_url, real_url, versions) + key = ("https://{PYPI_INDEX_URL}/pkg", "https://pypi.org/simple/pkg", ["1.0.0", "1.1.0"]) + + # When set the cache + cache.setdefault(key, fake_result) + + # And get a value back + got = cache.get(key) + + got.sdists().contains_exactly(fake_result.sdists) + got.whls().contains_exactly(fake_result.whls) + got.sha256s_by_version().contains_exactly(fake_result.sha256s_by_version) + + # A different key with fewer versions + key = ("https://{PYPI_INDEX_URL}/pkg", "https://pypi.org/simple/pkg", ["1.0.0"]) + + got = cache.get(key) + got.sdists().contains_exactly(fake_result.sdists) + got.whls().contains_exactly({}) + got.sha256s_by_version().contains_exactly({"1.0.0": ["sha_1"]}) + + # A key with no matches + key = ("https://{PYPI_INDEX_URL}/pkg", "https://pypi.org/simple/pkg", ["1.2.0"]) + + cache.get(key).equals(None) + +_tests.append(_test_memory_cache_hit) + +def _test_pypi_cache_writes_to_facts(env): + """Verifies that setting a value in the cache also populates the facts store.""" + mock_ctx = mocks.mctx(facts = {}) + cache = _cache(env, mctx = mock_ctx) + + fake_result = struct( + sdists = { + "sha_sdist": struct( + version = "1.0.0", + filename = "pkg-1.0.0.tar.gz", + url = "https://pypi.org/files/pkg-1.0.0.tar.gz", + yanked = "", + ), + }, + whls = { + "sha_whl": struct( + version = "1.0.0", + filename = "pkg-1.0.0-py3-none-any.whl", + url = "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl", + yanked = "Security issue", + ), + # This won't get stored + "sha_whl_2": struct( + version = "1.1.0", + filename = "pkg-1.1.0-py3-none-any.whl", + url = "https://pypi.org/files/pkg-1.1.0-py3-none-any.whl", + yanked = None, + ), + }, + sha256s_by_version = { + "1.0.0": ["sha_sdist", "sha_whl"], + "1.1.0": ["sha_whl_2"], + }, + ) + + key = ("https://{PYPI_INDEX_URL}/pkg/", "https://pypi.org/simple/pkg/", ["1.0.0"]) + + # When we set the cache + cache.setdefault(key, fake_result) + + # Then the key returns us the same items + got = cache.get(key) + got.whls().contains_exactly({ + "sha_whl": fake_result.whls["sha_whl"], + }) + got.sdists().contains_exactly(fake_result.sdists) + got.sha256s_by_version().contains_exactly({ + "1.0.0": fake_result.sha256s_by_version["1.0.0"], + }) + + # Then when we get facts at the end + cache.get_facts().contains_exactly({ + "dist_hashes": { + # We are not using the real index URL, because we may have credentials in here + "https://{PYPI_INDEX_URL}": { + "pkg": { + "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl": "sha_whl", + "https://pypi.org/files/pkg-1.0.0.tar.gz": "sha_sdist", + }, + }, + }, + "dist_yanked": { + "https://{PYPI_INDEX_URL}": { + "pkg": { + "sha_sdist": "", + "sha_whl": "Security issue", + }, + }, + }, + "fact_version": "v1", # Facts version + }) + + # When we get the other items cached in memory, they get written to facts + got = cache.get((key[0], key[1], ["1.1.0"])) + got.whls().contains_exactly({ + "sha_whl_2": fake_result.whls["sha_whl_2"], + }) + got.sdists().contains_exactly({}) + got.sha256s_by_version().contains_exactly({ + "1.1.0": fake_result.sha256s_by_version["1.1.0"], + }) + + # Then when we get facts at the end + cache.get_facts().contains_exactly({ + "dist_hashes": { + # We are not using the real index URL, because we may have credentials in here + "https://{PYPI_INDEX_URL}": { + "pkg": { + "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl": "sha_whl", + "https://pypi.org/files/pkg-1.0.0.tar.gz": "sha_sdist", + "https://pypi.org/files/pkg-1.1.0-py3-none-any.whl": "sha_whl_2", + }, + }, + }, + "dist_yanked": { + "https://{PYPI_INDEX_URL}": { + "pkg": { + "sha_sdist": "", + "sha_whl": "Security issue", + }, + }, + }, + "fact_version": "v1", # Facts version + }) + +_tests.append(_test_pypi_cache_writes_to_facts) + +def _test_pypi_cache_reads_from_facts(env): + """Verifies that setting a value in the cache also populates the facts store.""" + mock_ctx = mocks.mctx(facts = { + "dist_hashes": { + # We are not using the real index URL, because we may have credentials in here + "https://{PYPI_INDEX_URL}": { + "pkg": { + "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl": "sha_whl", + "https://pypi.org/files/pkg-1.0.0.tar.gz": "sha_sdist", + }, + }, + }, + "dist_yanked": { + "https://{PYPI_INDEX_URL}": { + "pkg": { + "sha_sdist": "", + "sha_whl": "Security issue", + }, + }, + }, + "fact_version": "v1", # Facts version + }) + cache = _cache(env, mctx = mock_ctx) + + key = ("https://{PYPI_INDEX_URL}/pkg/", "https://pypi.org/simple/pkg/", ["1.0.0"]) + + # Then we would get empty facts because we haven't accessed any of the known facts. + # This simulates the dropping of the facts of requirements that are no longer needed. + cache.get_facts().contains_exactly({}) + + # When we get the + got = cache.get(key) + + expected_result = struct( + sdists = { + "sha_sdist": struct( + sha256 = "sha_sdist", + version = "1.0.0", + filename = "pkg-1.0.0.tar.gz", + metadata_url = "", + metadata_sha256 = "", + url = "https://pypi.org/files/pkg-1.0.0.tar.gz", + yanked = "", + ), + }, + whls = { + "sha_whl": struct( + sha256 = "sha_whl", + version = "1.0.0", + filename = "pkg-1.0.0-py3-none-any.whl", + url = "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl", + metadata_url = "", + metadata_sha256 = "", + yanked = "Security issue", + ), + }, + sha256s_by_version = { + "1.0.0": ["sha_sdist", "sha_whl"], + }, + ) + + got.whls().contains_exactly(expected_result.whls) + got.sdists().contains_exactly(expected_result.sdists) + got.sha256s_by_version().contains_exactly(expected_result.sha256s_by_version) + + # Then when we store the same facts back again, because we accessed the cached keys. + cache.get_facts().contains_exactly(mock_ctx.facts) + + # When we request more than what we have, we will return nothing + key = ("https://{PYPI_INDEX_URL}/pkg/", "https://pypi.org/simple/pkg/", ["1.0.0", "1.1.0"]) + got = cache.get(key) + got.equals(None) + +_tests.append(_test_pypi_cache_reads_from_facts) + +def _test_pypi_cache_reads_from_facts_drops_unaccessed_dists(env): + """Verifies that dists for unaccessed versions are dropped from computed facts.""" + mock_ctx = mocks.mctx(facts = { + "dist_hashes": { + "https://{PYPI_INDEX_URL}": { + "pkg": { + "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl": "sha_whl_1.0.0", + "https://pypi.org/files/pkg-1.0.0.tar.gz": "sha_sdist_1.0.0", + "https://pypi.org/files/pkg-1.1.0-py3-none-any.whl": "sha_whl_1.1.0", + "https://pypi.org/files/pkg-1.1.0.tar.gz": "sha_sdist_1.1.0", + }, + }, + }, + "fact_version": "v1", + }) + cache = _cache(env, mctx = mock_ctx) + + # Request only version 1.0.0; version 1.1.0 is NOT requested + key = ("https://{PYPI_INDEX_URL}/pkg/", "https://pypi.org/simple/pkg/", ["1.0.0"]) + got = cache.get(key) + + expected = struct( + sdists = { + "sha_sdist_1.0.0": struct( + sha256 = "sha_sdist_1.0.0", + version = "1.0.0", + filename = "pkg-1.0.0.tar.gz", + metadata_url = "", + metadata_sha256 = "", + url = "https://pypi.org/files/pkg-1.0.0.tar.gz", + yanked = None, + ), + }, + whls = { + "sha_whl_1.0.0": struct( + sha256 = "sha_whl_1.0.0", + version = "1.0.0", + filename = "pkg-1.0.0-py3-none-any.whl", + metadata_url = "", + metadata_sha256 = "", + url = "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl", + yanked = None, + ), + }, + sha256s_by_version = { + "1.0.0": ["sha_sdist_1.0.0", "sha_whl_1.0.0"], + }, + ) + got.whls().contains_exactly(expected.whls) + got.sdists().contains_exactly(expected.sdists) + got.sha256s_by_version().contains_exactly(expected.sha256s_by_version) + + # get_facts() must only contain version 1.0.0 data; 1.1.0 is dropped + cache.get_facts().contains_exactly({ + "dist_hashes": { + "https://{PYPI_INDEX_URL}": { + "pkg": { + "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl": "sha_whl_1.0.0", + "https://pypi.org/files/pkg-1.0.0.tar.gz": "sha_sdist_1.0.0", + }, + }, + }, + "fact_version": "v1", + }) + +_tests.append(_test_pypi_cache_reads_from_facts_drops_unaccessed_dists) + +def _test_memory_cache_index_urls(env): + """Verifies that the cache returns stored values for index_urls.""" + store = {} + cache = _cache(env, mctx = None, store = store) + + fake_result = { + "pkg-a": "https://pypi.org/simple/pkg-a/", + "pkg_b": "https://pypi.org/simple/pkg-b/", + } + + key = ("https://pypi.org/simple/", "https://pypi.org/simple/", {"pkg-a": None, "pkg_b": None}) + + cache.setdefault(key, fake_result) + + got = cache.get(key) + got.contains_exactly(fake_result) + + key = ("https://pypi.org/simple/", "https://pypi.org/simple/", {"pkg-a": None}) + got = cache.get(key) + got.contains_exactly({"pkg-a": "https://pypi.org/simple/pkg-a/"}) + + key = ("https://pypi.org/simple/", "https://pypi.org/simple/", {"pkg-c": None}) + cache.get(key).equals(None) + +_tests.append(_test_memory_cache_index_urls) + +def _test_pypi_cache_writes_index_urls_to_facts(env): + """Verifies that setting index_urls in the cache also populates the facts store.""" + mock_ctx = mocks.mctx(facts = {}) + cache = _cache(env, mctx = mock_ctx) + + fake_result = { + "pkg-a": "https://pypi.org/simple/pkg-a/", + "pkg_b": "https://pypi.org/simple/pkg-b/", + } + + key = ("https://pypi.org/simple/", "https://pypi.org/simple/", {"pkg-a": None}) + + cache.setdefault(key, fake_result) + + cache.get_facts().contains_exactly({ + "fact_version": "v1", + "index_urls": { + "https://pypi.org/simple/": { + "pkg-a": "https://pypi.org/simple/pkg-a/", + }, + }, + }) + + key = ("https://pypi.org/simple/", "https://pypi.org/simple/", {"pkg_b": None}) + cache.setdefault(key, fake_result) + + cache.get_facts().contains_exactly({ + "fact_version": "v1", + "index_urls": { + "https://pypi.org/simple/": { + "pkg-a": "https://pypi.org/simple/pkg-a/", + "pkg_b": "https://pypi.org/simple/pkg-b/", + }, + }, + }) + +_tests.append(_test_pypi_cache_writes_index_urls_to_facts) + +def _test_pypi_cache_reads_index_urls_from_facts(env): + """Verifies that reading index_urls from facts works correctly.""" + mock_ctx = mocks.mctx(facts = { + "fact_version": "v1", + "index_urls": { + "https://pypi.org/simple/": { + "pkg-a": "https://pypi.org/simple/pkg-a/", + "pkg-b": "https://pypi.org/simple/pkg-b/", + }, + }, + }) + cache = _cache(env, mctx = mock_ctx) + + key = ("https://pypi.org/simple/", "https://pypi.org/simple/", {"pkg-a": None}) + got = cache.get(key) + got.contains_exactly({"pkg-a": "https://pypi.org/simple/pkg-a/"}) + + key = ("https://pypi.org/simple/", "https://pypi.org/simple/", {"pkg-a": None, "pkg-b": None}) + got = cache.get(key) + got.contains_exactly({ + "pkg-a": "https://pypi.org/simple/pkg-a/", + "pkg-b": "https://pypi.org/simple/pkg-b/", + }) + + key = ("https://pypi.org/simple/", "https://pypi.org/simple/", {"pkg-c": None}) + cache.get(key).equals(None) + + cache.get_facts().contains_exactly(mock_ctx.facts) + +_tests.append(_test_pypi_cache_reads_index_urls_from_facts) + +def _test_pypi_cache_reads_index_urls_from_facts_incomplete(env): + """Verifies that incomplete index_urls facts returns None (forces fresh download).""" + mock_ctx = mocks.mctx(facts = { + "fact_version": "v1", + "index_urls": { + "https://pypi.org/simple/": { + "pkg-a": "https://pypi.org/simple/pkg-a/", + }, + }, + }) + cache = _cache(env, mctx = mock_ctx) + + # Request pkg-a and pkg-b, but facts only have pkg-a + key = ("https://pypi.org/simple/", "https://pypi.org/simple/", {"pkg-a": None, "pkg-b": None}) + cache.get(key).equals(None) + +_tests.append(_test_pypi_cache_reads_index_urls_from_facts_incomplete) + +def _test_pypi_cache_reads_index_urls_from_facts_drops_unaccessed(env): + """Verifies get_facts() drops unaccessed index_urls entries. + + When known_facts has packages that are not requested, they should not + appear in computed_facts. This ensures stale facts (from packages + removed from all requirements files) get cleaned up from the lockfile. + """ + mock_ctx = mocks.mctx(facts = { + "fact_version": "v1", + "index_urls": { + "https://pypi.org/simple/": { + "pkg-a": "https://pypi.org/simple/pkg-a/", + "pkg-b": "https://pypi.org/simple/pkg-b/", + "pkg-c": "https://pypi.org/simple/pkg-c/", + }, + }, + }) + cache = _cache(env, mctx = mock_ctx) + + # Request only a subset of packages; pkg-c is not requested + key = ("https://pypi.org/simple/", "https://pypi.org/simple/", {"pkg-a": None, "pkg-b": None}) + got = cache.get(key) + got.contains_exactly({ + "pkg-a": "https://pypi.org/simple/pkg-a/", + "pkg-b": "https://pypi.org/simple/pkg-b/", + }) + + # get_facts() must only return the requested (accessed) subset; pkg-c is dropped + cache.get_facts().contains_exactly({ + "fact_version": "v1", + "index_urls": { + "https://pypi.org/simple/": { + "pkg-a": "https://pypi.org/simple/pkg-a/", + "pkg-b": "https://pypi.org/simple/pkg-b/", + }, + }, + }) + +_tests.append(_test_pypi_cache_reads_index_urls_from_facts_drops_unaccessed) + +def pypi_cache_test_suite(name): + test_suite( + name = name, + basic_tests = _tests, + ) diff --git a/tests/pypi/requirements_files_by_platform/requirements_files_by_platform_tests.bzl b/tests/pypi/requirements_files_by_platform/requirements_files_by_platform_tests.bzl index 6688d72ffe..b1176e6a15 100644 --- a/tests/pypi/requirements_files_by_platform/requirements_files_by_platform_tests.bzl +++ b/tests/pypi/requirements_files_by_platform/requirements_files_by_platform_tests.bzl @@ -37,6 +37,7 @@ requirements_files_by_platform = lambda **kwargs: _sut( ) def _test_fail_no_requirements(env): + """Verify that omitting all requirements attributes produces an error.""" errors = [] requirements_files_by_platform( fail_fn = errors.append, @@ -47,6 +48,7 @@ A 'requirements_lock' attribute must be specified, a platform-specific lockfiles _tests.append(_test_fail_no_requirements) def _test_fail_duplicate_platforms(env): + """Verify that a platform mapped to multiple requirements files errors.""" errors = [] requirements_files_by_platform( requirements_by_platform = { @@ -61,6 +63,7 @@ def _test_fail_duplicate_platforms(env): _tests.append(_test_fail_duplicate_platforms) def _test_fail_download_only_bad_attr(env): + """Verify that ``--platform`` pip args require a single ``requirements_lock``.""" errors = [] requirements_files_by_platform( requirements_linux = "requirements_linux", @@ -78,6 +81,7 @@ def _test_fail_download_only_bad_attr(env): _tests.append(_test_fail_download_only_bad_attr) def _test_simple(env): + """Test basic mapping of a single ``requirements_lock`` to all platforms.""" for got in [ requirements_files_by_platform( requirements_lock = "requirements_lock", @@ -104,6 +108,7 @@ def _test_simple(env): _tests.append(_test_simple) def _test_simple_limited(env): + """Test that limiting the platform list restricts the output mapping.""" for got in [ requirements_files_by_platform( requirements_lock = "requirements_lock", @@ -115,6 +120,12 @@ def _test_simple_limited(env): }, platforms = ["linux_x86_64", "osx_x86_64"], ), + requirements_files_by_platform( + requirements_by_platform = { + "requirements_lock": "linux_x86_64,osx_aarch64,osx_x86_64", + }, + platforms = ["linux_x86_64", "osx_x86_64", "windows_x86_64"], + ), ]: env.expect.that_dict(got).contains_exactly({ "requirements_lock": [ @@ -126,6 +137,7 @@ def _test_simple_limited(env): _tests.append(_test_simple_limited) def _test_simple_with_python_version(env): + """Test that ``python_version`` prefixes platform names with ``cpNNN_``.""" for got in [ requirements_files_by_platform( requirements_lock = "requirements_lock", @@ -163,6 +175,7 @@ def _test_simple_with_python_version(env): _tests.append(_test_simple_with_python_version) def _test_multi_os(env): + """Test per-OS requirements files mapping each OS group correctly.""" for got in [ requirements_files_by_platform( requirements_linux = "requirements_linux", @@ -197,6 +210,7 @@ def _test_multi_os(env): _tests.append(_test_multi_os) def _test_multi_os_download_only_platform(env): + """Test that ``--platform`` pip args narrow platforms to the host OS.""" got = requirements_files_by_platform( requirements_lock = "requirements_linux", extra_pip_args = [ @@ -213,12 +227,24 @@ def _test_multi_os_download_only_platform(env): _tests.append(_test_multi_os_download_only_platform) def _test_os_arch_requirements_with_default(env): + """Test combining specific OS/arch requirements with a fallback ``requirements_lock``.""" got = requirements_files_by_platform( requirements_by_platform = { "requirements_exotic": "linux_super_exotic", "requirements_linux": "linux_x86_64,linux_aarch64", }, requirements_lock = "requirements_lock", + platforms = [ + "linux_super_exotic", + "linux_x86_64", + "linux_aarch64", + "linux_arm", + "linux_ppc", + "linux_s390x", + "osx_aarch64", + "osx_x86_64", + "windows_x86_64", + ], ) env.expect.that_dict(got).contains_exactly({ "requirements_exotic": ["linux_super_exotic"], @@ -235,6 +261,64 @@ def _test_os_arch_requirements_with_default(env): _tests.append(_test_os_arch_requirements_with_default) +def _test_host_only_lockfile(env): + """Host-only: single requirements_lock with only the host platform. + + Verifies no extra empty-platform files leak into the return dict. + """ + got = requirements_files_by_platform( + requirements_lock = "requirements_lock", + platforms = ["osx_x86_64"], + ) + env.expect.that_dict(got).contains_exactly({ + "requirements_lock": ["osx_x86_64"], + }) + +_tests.append(_test_host_only_lockfile) + +def _test_host_only_multiple_os(env): + """Host-only with per-OS files but only host platform configured. + + Files with no matching platforms should appear with empty platform + lists so parse_requirements can read all packages for index URLs. + """ + got = requirements_files_by_platform( + requirements_linux = "requirements_linux", + requirements_osx = "requirements_osx", + requirements_windows = "requirements_windows", + platforms = ["osx_x86_64"], + ) + env.expect.that_dict(got).contains_exactly({ + # Per-OS files with no matching platforms get empty lists + "requirements_linux": [], + # The matching OS file gets its platforms + "requirements_osx": ["osx_x86_64"], + "requirements_windows": [], + }) + +_tests.append(_test_host_only_multiple_os) + +def _test_host_only_os_with_fallback(env): + """Host-only with per-OS files + fallback lock, host platform only. + + The fallback should not appear since the matching OS file covers + the only platform; unmatched files get empty lists. + """ + got = requirements_files_by_platform( + requirements_linux = "requirements_linux", + requirements_osx = "requirements_osx", + requirements_lock = "requirements_lock", + platforms = ["osx_x86_64"], + ) + env.expect.that_dict(got).contains_exactly({ + "requirements_linux": [], + "requirements_osx": ["osx_x86_64"], + # Fallback lock is not used because osx file already covers + # the only platform + }) + +_tests.append(_test_host_only_os_with_fallback) + def requirements_files_by_platform_test_suite(name): """Create the test suite. diff --git a/tests/pypi/select_whl/select_whl_tests.bzl b/tests/pypi/select_whl/select_whl_tests.bzl index 1c28fcca5f..8d2170ed7f 100644 --- a/tests/pypi/select_whl/select_whl_tests.bzl +++ b/tests/pypi/select_whl/select_whl_tests.bzl @@ -84,12 +84,10 @@ def _select_whl(whls, debug = False, **kwargs): for f in whls ], logger = repo_utils.logger(struct( - os = struct( - environ = { - REPO_DEBUG_ENV_VAR: "1", - REPO_VERBOSITY_ENV_VAR: "TRACE" if debug else "INFO", - }, - ), + getenv = { + REPO_DEBUG_ENV_VAR: "1", + REPO_VERBOSITY_ENV_VAR: "TRACE" if debug else "INFO", + }.get, ), "unit-test"), **kwargs ) @@ -451,7 +449,6 @@ def _test_multiple_musllinux_exact_params(env): whl_abi_tags = ["none"], python_version = "3.12", limit = 2, - debug = True, ) _match( env, diff --git a/tests/pypi/simpleapi_download/simpleapi_download_tests.bzl b/tests/pypi/simpleapi_download/simpleapi_download_tests.bzl index 8dc307235a..4e86b76e10 100644 --- a/tests/pypi/simpleapi_download/simpleapi_download_tests.bzl +++ b/tests/pypi/simpleapi_download/simpleapi_download_tests.bzl @@ -15,135 +15,164 @@ "" load("@rules_testing//lib:test_suite.bzl", "test_suite") -load("//python/private/pypi:simpleapi_download.bzl", "simpleapi_download", "strip_empty_path_segments") # buildifier: disable=bzl-visibility +load("//python/private/pypi:pypi_cache.bzl", "pypi_cache") # buildifier: disable=bzl-visibility +load("//python/private/pypi:simpleapi_download.bzl", "simpleapi_download") # buildifier: disable=bzl-visibility +load("//tests/support/mocks:mocks.bzl", "mocks") _tests = [] def _test_simple(env): calls = [] - def read_simpleapi(ctx, url, attr, cache, get_auth, block): - _ = ctx # buildifier: disable=unused-variable - _ = attr - _ = cache - _ = get_auth - env.expect.that_bool(block).equals(False) - calls.append(url) - if "foo" in url and "main" in url: + def read_simpleapi(ctx, url, versions, attr, cache, get_auth, block, parse_index): + if parse_index: return struct( - output = "", - success = False, - ) - else: - return struct( - output = "data from {}".format(url), success = True, + output = { + "bar": "/bar/", + "baz": "/baz/", + } if "main" in url else { + "foo": "/foo/", + }, ) + _ = ctx, attr, cache, get_auth, versions # buildifier: disable=unused-variable + env.expect.that_bool(block).equals(False) + calls.append(url) + return struct( + output = struct( + sdists = {"deadbeef": url.strip("/").split("/")[-1]}, + whls = {"deadb33f": url.strip("/").split("/")[-1]}, + sha256s_by_version = {"fizz": url.strip("/").split("/")[-1]}, + ), + success = True, + ) + contents = simpleapi_download( - ctx = struct( - os = struct(environ = {}), - report_progress = lambda _: None, - ), + ctx = mocks.mctx(), attr = struct( index_url_overrides = {}, - index_url = "main", - extra_index_urls = ["extra"], - sources = ["foo", "bar", "baz"], + index_url = "https://main.com", + extra_index_urls = ["https://extra.com"], + sources = {"bar": None, "baz": None, "foo": None}, envsubst = [], ), - cache = {}, + cache = pypi_cache(), parallel_download = True, read_simpleapi = read_simpleapi, ) env.expect.that_collection(calls).contains_exactly([ - "extra/foo/", - "main/bar/", - "main/baz/", - "main/foo/", + "https://extra.com/foo/", + "https://main.com/bar/", + "https://main.com/baz/", ]) env.expect.that_dict(contents).contains_exactly({ - "bar": "data from main/bar/", - "baz": "data from main/baz/", - "foo": "data from extra/foo/", + "bar": struct( + index_url = "https://main.com/bar/", + sdists = {"deadbeef": "bar"}, + sha256s_by_version = {"fizz": "bar"}, + whls = {"deadb33f": "bar"}, + ), + "baz": struct( + index_url = "https://main.com/baz/", + sdists = {"deadbeef": "baz"}, + sha256s_by_version = {"fizz": "baz"}, + whls = {"deadb33f": "baz"}, + ), + "foo": struct( + index_url = "https://extra.com/foo/", + sdists = {"deadbeef": "foo"}, + sha256s_by_version = {"fizz": "foo"}, + whls = {"deadb33f": "foo"}, + ), }) _tests.append(_test_simple) -def _test_fail(env): +def _test_index_overrides(env): calls = [] fails = [] - def read_simpleapi(ctx, url, attr, cache, get_auth, block): - _ = ctx # buildifier: disable=unused-variable - _ = attr - _ = cache - _ = get_auth - env.expect.that_bool(block).equals(False) - calls.append(url) - if "foo" in url: - return struct( - output = "", - success = False, - ) - if "bar" in url: + def read_simpleapi(ctx, *, url, versions, attr, cache, get_auth, block, parse_index): + if parse_index: return struct( - output = "", - success = False, - ) - else: - return struct( - output = "data from {}".format(url), success = True, + output = { + # normalized + "ba_z": "/ba-z/", + "bar": "/bar/", + "foo": "/foo-should-fail/", + } if "main" in url else { + "foo": "/foo/", + }, ) - simpleapi_download( - ctx = struct( - os = struct(environ = {}), - report_progress = lambda _: None, - ), + _ = ctx, attr, cache, get_auth, versions # buildifier: disable=unused-variable + env.expect.that_bool(block).equals(False) + calls.append(url) + return struct( + output = struct( + sdists = {"deadbeef": url.strip("/").split("/")[-1]}, + whls = {"deadb33f": url.strip("/").split("/")[-1]}, + sha256s_by_version = {"fizz": url.strip("/").split("/")[-1]}, + ), + success = True, + ) + + contents = simpleapi_download( + ctx = mocks.mctx(), attr = struct( index_url_overrides = { - "foo": "invalid", + "foo": "https://extra.com", }, - index_url = "main", - extra_index_urls = ["extra"], - sources = ["foo", "bar", "baz"], + index_url = "https://main.com", + extra_index_urls = [], + sources = {"ba_z": None, "bar": None, "foo": None}, envsubst = [], ), - cache = {}, + cache = pypi_cache(), parallel_download = True, read_simpleapi = read_simpleapi, _fail = fails.append, ) - env.expect.that_collection(fails).contains_exactly([ - """ -Failed to download metadata of the following packages from urls: -{ - "foo": "invalid", - "bar": ["main", "extra"], -} - -If you would like to skip downloading metadata for these packages please add 'simpleapi_skip=[ - "foo", - "bar", -]' to your 'pip.parse' call. -""", - ]) + env.expect.that_collection(fails).contains_exactly([]) env.expect.that_collection(calls).contains_exactly([ - "invalid/foo/", - "main/bar/", - "main/baz/", - "invalid/foo/", - "extra/bar/", + "https://main.com/bar/", + "https://main.com/ba-z/", + "https://extra.com/foo/", ]) + env.expect.that_dict(contents).contains_exactly({ + "ba_z": struct( + index_url = "https://main.com/ba-z/", + sdists = {"deadbeef": "ba-z"}, + sha256s_by_version = {"fizz": "ba-z"}, + whls = {"deadb33f": "ba-z"}, + ), + "bar": struct( + index_url = "https://main.com/bar/", + sdists = {"deadbeef": "bar"}, + sha256s_by_version = {"fizz": "bar"}, + whls = {"deadb33f": "bar"}, + ), + "foo": struct( + index_url = "https://extra.com/foo/", + sdists = {"deadbeef": "foo"}, + sha256s_by_version = {"fizz": "foo"}, + whls = {"deadb33f": "foo"}, + ), + }) -_tests.append(_test_fail) +_tests.append(_test_index_overrides) def _test_download_url(env): downloads = {} + reads = [ + "", + "", + "", + ] def download(url, output, **kwargs): _ = kwargs # buildifier: disable=unused-variable @@ -152,20 +181,22 @@ def _test_download_url(env): simpleapi_download( ctx = struct( - os = struct(environ = {}), + getenv = {}.get, download = download, report_progress = lambda _: None, - read = lambda i: "contents of " + i, + # We will first add a download to the list, so this is a poor man's `next(foo)` + # implementation + read = lambda i: reads[len(downloads) - 1], path = lambda i: "path/for/" + i, ), attr = struct( index_url_overrides = {}, index_url = "https://example.com/main/simple/", extra_index_urls = [], - sources = ["foo", "bar", "baz"], + sources = {"bar": ["1.0"], "baz": ["1.0"], "foo": ["1.0"]}, envsubst = [], ), - cache = {}, + cache = pypi_cache(), parallel_download = False, get_auth = lambda ctx, urls, ctx_attr: struct(), ) @@ -180,6 +211,18 @@ _tests.append(_test_download_url) def _test_download_url_parallel(env): downloads = {} + reads = [ + # The first read is the index which seeds the downloads later + """ + bar + baz + foo + """, + "", + "", + "", + "", + ] def download(url, output, **kwargs): _ = kwargs # buildifier: disable=unused-variable @@ -188,34 +231,88 @@ def _test_download_url_parallel(env): simpleapi_download( ctx = struct( - os = struct(environ = {}), + getenv = {}.get, download = download, report_progress = lambda _: None, - read = lambda i: "contents of " + i, + # We will first add a download to the list, so this is a poor man's `next(foo)` + # implementation. We use 2 because we will enqueue 2 downloads in parallel. + read = lambda i: reads[len(downloads) - 2], path = lambda i: "path/for/" + i, ), attr = struct( index_url_overrides = {}, - index_url = "https://example.com/main/simple/", - extra_index_urls = [], - sources = ["foo", "bar", "baz"], + index_url = "https://example.com/default/simple/", + extra_index_urls = ["https://example.com/extra/simple/"], + sources = {"bar": None, "baz": None, "foo": None}, envsubst = [], ), - cache = {}, + cache = pypi_cache(), parallel_download = True, get_auth = lambda ctx, urls, ctx_attr: struct(), ) env.expect.that_dict(downloads).contains_exactly({ - "https://example.com/main/simple/bar/": "path/for/https___example_com_main_simple_bar.html", - "https://example.com/main/simple/baz/": "path/for/https___example_com_main_simple_baz.html", - "https://example.com/main/simple/foo/": "path/for/https___example_com_main_simple_foo.html", + "https://example.com/default/simple/": "path/for/https___example_com_default_simple.html", + "https://example.com/extra/simple/": "path/for/https___example_com_extra_simple.html", + "https://example.com/extra/simple/bar/": "path/for/https___example_com_extra_simple_bar.html", + "https://example.com/extra/simple/baz/": "path/for/https___example_com_extra_simple_baz.html", + "https://example.com/extra/simple/foo/": "path/for/https___example_com_extra_simple_foo.html", }) _tests.append(_test_download_url_parallel) +def _test_download_url_parallel_with_overrides(env): + downloads = {} + reads = [ + "", + "", + "", + ] + + def download(url, output, **kwargs): + _ = kwargs # buildifier: disable=unused-variable + downloads[url[0]] = output + return struct(wait = lambda: struct(success = True)) + + simpleapi_download( + ctx = struct( + getenv = {}.get, + download = download, + report_progress = lambda _: None, + # We will first add a download to the list, so this is a poor man's `next(foo)` + # implementation. We use 2 because we will enqueue 2 downloads in parallel. + read = lambda i: reads[len(downloads) - 2], + path = lambda i: "path/for/" + i, + ), + attr = struct( + index_url_overrides = { + "bar": "https://example.com/extra/simple/", + }, + index_url = "https://example.com/default/simple/", + extra_index_urls = [], + sources = {"bar": None, "baz": None, "foo": None}, + envsubst = [], + ), + cache = pypi_cache(), + parallel_download = True, + get_auth = lambda ctx, urls, ctx_attr: struct(), + ) + + env.expect.that_dict(downloads).contains_exactly({ + "https://example.com/default/simple/baz/": "path/for/https___example_com_default_simple_baz.html", + "https://example.com/default/simple/foo/": "path/for/https___example_com_default_simple_foo.html", + "https://example.com/extra/simple/bar/": "path/for/https___example_com_extra_simple_bar.html", + }) + +_tests.append(_test_download_url_parallel_with_overrides) + def _test_download_envsubst_url(env): downloads = {} + reads = [ + "", + "", + "", + ] def download(url, output, **kwargs): _ = kwargs # buildifier: disable=unused-variable @@ -224,20 +321,22 @@ def _test_download_envsubst_url(env): simpleapi_download( ctx = struct( - os = struct(environ = {"INDEX_URL": "https://example.com/main/simple/"}), + getenv = {"INDEX_URL": "https://example.com/main/simple/"}.get, download = download, report_progress = lambda _: None, - read = lambda i: "contents of " + i, + # We will first add a download to the list, so this is a poor man's `next(foo)` + # implementation + read = lambda i: reads[len(downloads) - 1], path = lambda i: "path/for/" + i, ), attr = struct( index_url_overrides = {}, index_url = "$INDEX_URL", extra_index_urls = [], - sources = ["foo", "bar", "baz"], + sources = {"bar": None, "baz": None, "foo": None}, envsubst = ["INDEX_URL"], ), - cache = {}, + cache = pypi_cache(), parallel_download = False, get_auth = lambda ctx, urls, ctx_attr: struct(), ) @@ -250,16 +349,6 @@ def _test_download_envsubst_url(env): _tests.append(_test_download_envsubst_url) -def _test_strip_empty_path_segments(env): - env.expect.that_str(strip_empty_path_segments("no/scheme//is/unchanged")).equals("no/scheme//is/unchanged") - env.expect.that_str(strip_empty_path_segments("scheme://with/no/empty/segments")).equals("scheme://with/no/empty/segments") - env.expect.that_str(strip_empty_path_segments("scheme://with//empty/segments")).equals("scheme://with/empty/segments") - env.expect.that_str(strip_empty_path_segments("scheme://with///multiple//empty/segments")).equals("scheme://with/multiple/empty/segments") - env.expect.that_str(strip_empty_path_segments("scheme://with//trailing/slash/")).equals("scheme://with/trailing/slash/") - env.expect.that_str(strip_empty_path_segments("scheme://with/trailing/slashes///")).equals("scheme://with/trailing/slashes/") - -_tests.append(_test_strip_empty_path_segments) - def simpleapi_download_test_suite(name): """Create the test suite. diff --git a/tests/pypi/urllib/BUILD.bazel b/tests/pypi/urllib/BUILD.bazel new file mode 100644 index 0000000000..a6405684a7 --- /dev/null +++ b/tests/pypi/urllib/BUILD.bazel @@ -0,0 +1,3 @@ +load(":urllib_tests.bzl", "urllib_test_suite") + +urllib_test_suite(name = "urllib_tests") diff --git a/tests/pypi/urllib/urllib_tests.bzl b/tests/pypi/urllib/urllib_tests.bzl new file mode 100644 index 0000000000..40c48dc854 --- /dev/null +++ b/tests/pypi/urllib/urllib_tests.bzl @@ -0,0 +1,48 @@ +"" + +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("//python/private/pypi:urllib.bzl", "urllib") # buildifier: disable=bzl-visibility + +_tests = [] + +def _test_absolute_url(env): + # Already absolute + for already_absolute in [ + "file://foo", + "https://foo.com", + "http://foo.com", + ]: + env.expect.that_str(urllib.absolute_url("https://ignored", already_absolute)).equals(already_absolute) + + # Simple with empty path segments + env.expect.that_str(urllib.absolute_url("https://example.com//", "file.whl")).equals("https://example.com/file.whl") + env.expect.that_str(urllib.absolute_url("https://example.com//a/b//", "../../file.whl")).equals("https://example.com/file.whl") + env.expect.that_str(urllib.absolute_url("https://example.com//a/b//", "/file.whl")).equals("https://example.com/file.whl") + + # Relative URLs + env.expect.that_str(urllib.absolute_url("https://example.com/relative", "file.whl")).equals("https://example.com/relative/file.whl") + env.expect.that_str(urllib.absolute_url("https://example.com/relative/", "file.whl")).equals("https://example.com/relative/file.whl") + env.expect.that_str(urllib.absolute_url("https://example.com/relative/", "../relative/file.whl")).equals("https://example.com/relative/file.whl") + + # Relative URL for files + env.expect.that_str(urllib.absolute_url("file://{PYPI_BAZEL_WORKSPACE_ROOT}", "vendor/distro/file.whl")).equals("file://{PYPI_BAZEL_WORKSPACE_ROOT}/vendor/distro/file.whl") + +_tests.append(_test_absolute_url) + +def _test_strip_empty_path_segments(env): + env.expect.that_str(urllib.strip_empty_path_segments("no/scheme//is/unchanged")).equals("no/scheme//is/unchanged") + env.expect.that_str(urllib.strip_empty_path_segments("scheme://with/no/empty/segments")).equals("scheme://with/no/empty/segments") + env.expect.that_str(urllib.strip_empty_path_segments("scheme://with//empty/segments")).equals("scheme://with/empty/segments") + env.expect.that_str(urllib.strip_empty_path_segments("scheme://with///multiple//empty/segments")).equals("scheme://with/multiple/empty/segments") + env.expect.that_str(urllib.strip_empty_path_segments("scheme://with//trailing/slash/")).equals("scheme://with/trailing/slash/") + env.expect.that_str(urllib.strip_empty_path_segments("scheme://with/trailing/slashes///")).equals("scheme://with/trailing/slashes/") + +_tests.append(_test_strip_empty_path_segments) + +def urllib_test_suite(name): + """Create the test suite. + + Args: + name: the name of the test suite + """ + test_suite(name = name, basic_tests = _tests) diff --git a/tests/pypi/version_from_filename/BUILD.bazel b/tests/pypi/version_from_filename/BUILD.bazel new file mode 100644 index 0000000000..e9d50dc6b8 --- /dev/null +++ b/tests/pypi/version_from_filename/BUILD.bazel @@ -0,0 +1,3 @@ +load(":version_from_filename_tests.bzl", "version_from_filename_test_suite") + +version_from_filename_test_suite(name = "version_from_filename_tests") diff --git a/tests/pypi/version_from_filename/version_from_filename_tests.bzl b/tests/pypi/version_from_filename/version_from_filename_tests.bzl new file mode 100644 index 0000000000..fab921fd4f --- /dev/null +++ b/tests/pypi/version_from_filename/version_from_filename_tests.bzl @@ -0,0 +1,56 @@ +"" + +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("//python/private/pypi:version_from_filename.bzl", "version_from_filename") # buildifier: disable=bzl-visibility + +_tests = [] + +def _test_wheel_version_extraction(env): + # Case 1: wheel + env.expect.that_str(version_from_filename("foo-1.2.3-py3-none-any.whl")).equals("1.2.3") + +_tests.append(_test_wheel_version_extraction) + +def _test_sdist_version_extraction(env): + # Case 1: Standard sdist + env.expect.that_str(version_from_filename("foo-1.2.3.tar.gz")).equals("1.2.3") + + # Case 2: PEP 625 - Project name has underscores (normalized from dashes) + # If the package is 'my-pkg', the sdist might be 'my_pkg-1.0.0.tar.gz' + env.expect.that_str(version_from_filename("my_pkg-1.0.0.tar.gz")).equals("1.0.0") + + # Case 3: Project name has multiple underscores + env.expect.that_str(version_from_filename("very_long_project_name-0.5.0.zip")).equals("0.5.0") + + # Case 4: Legacy sdist with hyphens in name + # Note: Modern tools normalize this, but we should support the hyphen split + env.expect.that_str(version_from_filename("complex-name-1.2.3.tar.gz")).equals("1.2.3") + + # Case 5: Version contains an underscore (e.g. local versions) + env.expect.that_str(version_from_filename("pkg-1.2.3_post1.tar.gz")).equals("1.2.3_post1") + + # Case 6: custom compression + env.expect.that_str(version_from_filename("pkg-1.2.3_post1.tar.xz")).equals("1.2.3_post1") + +_tests.append(_test_sdist_version_extraction) + +def _test_sdist_version_extraction_fail(env): + failures = [] + + # Case 1: 7z + env.expect.that_str(version_from_filename("foo-1.2.3.7z")).equals(None) + env.expect.that_str(version_from_filename("foo-1.2.3.7z", _fail = failures.append)).equals(None) + env.expect.that_collection(failures).contains_exactly(["Unsupported sdist extension: foo-1.2.3.7z"]) + + # Case 2: egg + failures.clear() + env.expect.that_str(version_from_filename("foo-1.2.3-py3.egg", _fail = failures.append)).equals(None) + env.expect.that_collection(failures).contains_exactly(["Unsupported sdist extension: foo-1.2.3-py3.egg"]) + +_tests.append(_test_sdist_version_extraction_fail) + +def version_from_filename_test_suite(name): + test_suite( + name = name, + basic_tests = _tests, + ) diff --git a/tests/pypi/whl_installer/BUILD.bazel b/tests/pypi/whl_installer/BUILD.bazel index 060d2bce62..5a2efb1260 100644 --- a/tests/pypi/whl_installer/BUILD.bazel +++ b/tests/pypi/whl_installer/BUILD.bazel @@ -1,10 +1,5 @@ load("//python:py_test.bzl", "py_test") -alias( - name = "lib", - actual = "//python/private/pypi/whl_installer:lib", -) - py_test( name = "arguments_test", size = "small", @@ -12,42 +7,6 @@ py_test( "arguments_test.py", ], deps = [ - ":lib", - ], -) - -py_test( - name = "platform_test", - size = "small", - srcs = [ - "platform_test.py", - ], - data = ["//examples/wheel:minimal_with_py_package"], - deps = [ - ":lib", - ], -) - -py_test( - name = "wheel_installer_test", - size = "small", - srcs = [ - "wheel_installer_test.py", - ], - data = ["//examples/wheel:minimal_with_py_package"], - deps = [ - ":lib", - ], -) - -py_test( - name = "wheel_test", - size = "small", - srcs = [ - "wheel_test.py", - ], - data = ["//examples/wheel:minimal_with_py_package"], - deps = [ - ":lib", + "//python/private/pypi/whl_installer:lib", ], ) diff --git a/tests/pypi/whl_installer/arguments_test.py b/tests/pypi/whl_installer/arguments_test.py index 2352d8e48b..c874445029 100644 --- a/tests/pypi/whl_installer/arguments_test.py +++ b/tests/pypi/whl_installer/arguments_test.py @@ -15,7 +15,7 @@ import json import unittest -from python.private.pypi.whl_installer import arguments, wheel +from python.private.pypi.whl_installer import arguments class ArgumentsTestCase(unittest.TestCase): @@ -48,18 +48,6 @@ def test_deserialize_structured_args(self) -> None: self.assertEqual(args["environment"], {"PIP_DO_SOMETHING": "True"}) self.assertEqual(args["extra_pip_args"], []) - def test_platform_aggregation(self) -> None: - parser = arguments.parser() - args = parser.parse_args( - args=[ - "--platform=linux_*", - "--platform=osx_*", - "--platform=windows_*", - "--requirement=foo", - ] - ) - self.assertEqual(set(wheel.Platform.all()), arguments.get_platforms(args)) - if __name__ == "__main__": unittest.main() diff --git a/tests/pypi/whl_installer/platform_test.py b/tests/pypi/whl_installer/platform_test.py deleted file mode 100644 index ad65650779..0000000000 --- a/tests/pypi/whl_installer/platform_test.py +++ /dev/null @@ -1,97 +0,0 @@ -import unittest -from random import shuffle - -from python.private.pypi.whl_installer.platform import ( - OS, - Arch, - Platform, - host_interpreter_version, -) - - -class MinorVersionTest(unittest.TestCase): - def test_host(self): - host = host_interpreter_version() - self.assertIsNotNone(host) - - -class PlatformTest(unittest.TestCase): - def test_can_get_host(self): - host = Platform.host() - self.assertIsNotNone(host) - self.assertEqual(1, len(Platform.from_string("host"))) - self.assertEqual(host, Platform.from_string("host")) - - def test_can_get_linux_x86_64_without_py_version(self): - got = Platform.from_string("linux_x86_64") - want = Platform(os=OS.linux, arch=Arch.x86_64) - self.assertEqual(want, got[0]) - - def test_can_get_specific_from_string(self): - got = Platform.from_string("cp33_linux_x86_64") - want = Platform(os=OS.linux, arch=Arch.x86_64, minor_version=3) - self.assertEqual(want, got[0]) - - got = Platform.from_string("cp33.0_linux_x86_64") - want = Platform(os=OS.linux, arch=Arch.x86_64, minor_version=3, micro_version=0) - self.assertEqual(want, got[0]) - - def test_can_get_all_for_py_version(self): - cp39 = Platform.all(minor_version=9, micro_version=0) - self.assertEqual(21, len(cp39), f"Got {cp39}") - self.assertEqual(cp39, Platform.from_string("cp39.0_*")) - - def test_can_get_all_for_os(self): - linuxes = Platform.all(OS.linux, minor_version=9) - self.assertEqual(7, len(linuxes)) - self.assertEqual(linuxes, Platform.from_string("cp39_linux_*")) - - def test_can_get_all_for_os_for_host_python(self): - linuxes = Platform.all(OS.linux) - self.assertEqual(7, len(linuxes)) - self.assertEqual(linuxes, Platform.from_string("linux_*")) - - def test_platform_sort(self): - platforms = [ - Platform(os=OS.linux, arch=None), - Platform(os=OS.linux, arch=Arch.x86_64), - Platform(os=OS.osx, arch=None), - Platform(os=OS.osx, arch=Arch.x86_64), - Platform(os=OS.osx, arch=Arch.aarch64), - ] - shuffle(platforms) - platforms.sort() - want = [ - Platform(os=OS.linux, arch=None), - Platform(os=OS.linux, arch=Arch.x86_64), - Platform(os=OS.osx, arch=None), - Platform(os=OS.osx, arch=Arch.x86_64), - Platform(os=OS.osx, arch=Arch.aarch64), - ] - - self.assertEqual(want, platforms) - - def test_wheel_os_alias(self): - self.assertEqual("osx", str(OS.osx)) - self.assertEqual(str(OS.darwin), str(OS.osx)) - - def test_wheel_arch_alias(self): - self.assertEqual("x86_64", str(Arch.x86_64)) - self.assertEqual(str(Arch.amd64), str(Arch.x86_64)) - - def test_wheel_platform_alias(self): - give = Platform( - os=OS.darwin, - arch=Arch.amd64, - ) - alias = Platform( - os=OS.osx, - arch=Arch.x86_64, - ) - - self.assertEqual("osx_x86_64", str(give)) - self.assertEqual(str(alias), str(give)) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/pypi/whl_installer/wheel_installer_test.py b/tests/pypi/whl_installer/wheel_installer_test.py deleted file mode 100644 index 7040b0cfd8..0000000000 --- a/tests/pypi/whl_installer/wheel_installer_test.py +++ /dev/null @@ -1,105 +0,0 @@ -# Copyright 2023 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. - -import json -import os -import shutil -import tempfile -import unittest -from pathlib import Path - -from python.private.pypi.whl_installer import wheel_installer - - -class TestRequirementExtrasParsing(unittest.TestCase): - def test_parses_requirement_for_extra(self) -> None: - cases = [ - ("name[foo]", ("name", frozenset(["foo"]))), - ("name[ Foo123 ]", ("name", frozenset(["Foo123"]))), - (" name1[ foo ] ", ("name1", frozenset(["foo"]))), - ("Name[foo]", ("name", frozenset(["foo"]))), - ("name_foo[bar]", ("name-foo", frozenset(["bar"]))), - ( - "name [fred,bar] @ http://foo.com ; python_version=='2.7'", - ("name", frozenset(["fred", "bar"])), - ), - ( - "name[quux, strange];python_version<'2.7' and platform_version=='2'", - ("name", frozenset(["quux", "strange"])), - ), - ( - "name; (os_name=='a' or os_name=='b') and os_name=='c'", - (None, None), - ), - ( - "name@http://foo.com", - (None, None), - ), - ] - - for case, expected in cases: - with self.subTest(): - self.assertTupleEqual( - wheel_installer._parse_requirement_for_extra(case), expected - ) - - -class TestWhlFilegroup(unittest.TestCase): - def setUp(self) -> None: - self.wheel_name = "example_minimal_package-0.0.1-py3-none-any.whl" - self.wheel_dir = tempfile.mkdtemp() - self.wheel_path = os.path.join(self.wheel_dir, self.wheel_name) - shutil.copy(os.path.join("examples", "wheel", self.wheel_name), self.wheel_dir) - - def tearDown(self): - shutil.rmtree(self.wheel_dir) - - def test_wheel_exists(self) -> None: - wheel_installer._extract_wheel( - Path(self.wheel_path), - installation_dir=Path(self.wheel_dir), - extras={}, - platforms=[], - enable_pipstar=False, - ) - - want_files = [ - "metadata.json", - "site-packages", - self.wheel_name, - ] - self.assertEqual( - sorted(want_files), - sorted( - [ - str(p.relative_to(self.wheel_dir)) - for p in Path(self.wheel_dir).glob("*") - ] - ), - ) - with open("{}/metadata.json".format(self.wheel_dir)) as metadata_file: - metadata_file_content = json.load(metadata_file) - - want = dict( - deps=[], - deps_by_platform={}, - entry_points=[], - name="example-minimal-package", - version="0.0.1", - ) - self.assertEqual(want, metadata_file_content) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/pypi/whl_installer/wheel_test.py b/tests/pypi/whl_installer/wheel_test.py deleted file mode 100644 index 3599fd1868..0000000000 --- a/tests/pypi/whl_installer/wheel_test.py +++ /dev/null @@ -1,345 +0,0 @@ -import unittest -from unittest import mock - -from python.private.pypi.whl_installer import wheel -from python.private.pypi.whl_installer.platform import OS, Arch, Platform - -_HOST_INTERPRETER_FN = ( - "python.private.pypi.whl_installer.wheel.host_interpreter_version" -) - - -class DepsTest(unittest.TestCase): - def test_simple(self): - deps = wheel.Deps("foo", requires_dist=["bar", 'baz; extra=="foo"']) - - got = deps.build() - - self.assertIsInstance(got, wheel.FrozenDeps) - self.assertEqual(["bar"], got.deps) - self.assertEqual({}, got.deps_select) - - def test_can_add_os_specific_deps(self): - for platforms in [ - { - Platform(os=OS.linux, arch=Arch.x86_64), - Platform(os=OS.osx, arch=Arch.x86_64), - Platform(os=OS.osx, arch=Arch.aarch64), - Platform(os=OS.windows, arch=Arch.x86_64), - }, - { - Platform(os=OS.linux, arch=Arch.x86_64, minor_version=8), - Platform(os=OS.osx, arch=Arch.x86_64, minor_version=8), - Platform(os=OS.osx, arch=Arch.aarch64, minor_version=8), - Platform(os=OS.windows, arch=Arch.x86_64, minor_version=8), - }, - { - Platform( - os=OS.linux, arch=Arch.x86_64, minor_version=8, micro_version=1 - ), - Platform(os=OS.osx, arch=Arch.x86_64, minor_version=8, micro_version=1), - Platform( - os=OS.osx, arch=Arch.aarch64, minor_version=8, micro_version=1 - ), - Platform( - os=OS.windows, arch=Arch.x86_64, minor_version=8, micro_version=1 - ), - }, - ]: - with self.subTest(): - deps = wheel.Deps( - "foo", - requires_dist=[ - "bar", - "an_osx_dep; sys_platform=='darwin'", - "posix_dep; os_name=='posix'", - "win_dep; os_name=='nt'", - ], - platforms=platforms, - ) - - got = deps.build() - - self.assertEqual(["bar"], got.deps) - self.assertEqual( - { - "linux_x86_64": ["posix_dep"], - "osx_aarch64": ["an_osx_dep", "posix_dep"], - "osx_x86_64": ["an_osx_dep", "posix_dep"], - "windows_x86_64": ["win_dep"], - }, - got.deps_select, - ) - - def test_non_platform_markers_are_added_to_common_deps(self): - got = wheel.Deps( - "foo", - requires_dist=[ - "bar", - "baz; implementation_name=='cpython'", - "m1_dep; sys_platform=='darwin' and platform_machine=='arm64'", - ], - platforms={ - Platform(os=OS.linux, arch=Arch.x86_64), - Platform(os=OS.osx, arch=Arch.x86_64), - Platform(os=OS.osx, arch=Arch.aarch64), - Platform(os=OS.windows, arch=Arch.x86_64), - }, - ).build() - - self.assertEqual(["bar", "baz"], got.deps) - self.assertEqual( - { - "osx_aarch64": ["m1_dep"], - }, - got.deps_select, - ) - - def test_self_is_ignored(self): - deps = wheel.Deps( - "foo", - requires_dist=[ - "bar", - "req_dep; extra == 'requests'", - "foo[requests]; extra == 'ssl'", - "ssl_lib; extra == 'ssl'", - ], - extras={"ssl"}, - ) - - got = deps.build() - - self.assertEqual(["bar", "req_dep", "ssl_lib"], got.deps) - self.assertEqual({}, got.deps_select) - - def test_self_dependencies_can_come_in_any_order(self): - deps = wheel.Deps( - "foo", - requires_dist=[ - "bar", - "baz; extra == 'feat'", - "foo[feat2]; extra == 'all'", - "foo[feat]; extra == 'feat2'", - "zdep; extra == 'all'", - ], - extras={"all"}, - ) - - got = deps.build() - - self.assertEqual(["bar", "baz", "zdep"], got.deps) - self.assertEqual({}, got.deps_select) - - def test_can_get_deps_based_on_specific_python_version(self): - requires_dist = [ - "bar", - "baz; python_full_version < '3.7.3'", - "posix_dep; os_name=='posix' and python_version >= '3.8'", - ] - - py38_deps = wheel.Deps( - "foo", - requires_dist=requires_dist, - platforms=[ - Platform(os=OS.linux, arch=Arch.x86_64, minor_version=8), - ], - ).build() - py373_deps = wheel.Deps( - "foo", - requires_dist=requires_dist, - platforms=[ - Platform( - os=OS.linux, arch=Arch.x86_64, minor_version=7, micro_version=3 - ), - ], - ).build() - py37_deps = wheel.Deps( - "foo", - requires_dist=requires_dist, - platforms=[ - Platform(os=OS.linux, arch=Arch.x86_64, minor_version=7), - ], - ).build() - - self.assertEqual(["bar", "baz"], py37_deps.deps) - self.assertEqual({}, py37_deps.deps_select) - self.assertEqual(["bar"], py373_deps.deps) - self.assertEqual({}, py37_deps.deps_select) - self.assertEqual(["bar", "posix_dep"], py38_deps.deps) - self.assertEqual({}, py38_deps.deps_select) - - def test_no_version_select_when_single_version(self): - requires_dist = [ - "bar", - "baz; python_version >= '3.8'", - "posix_dep; os_name=='posix'", - "posix_dep_with_version; os_name=='posix' and python_version >= '3.8'", - "arch_dep; platform_machine=='x86_64' and python_version >= '3.8'", - ] - - self.maxDiff = None - - deps = wheel.Deps( - "foo", - requires_dist=requires_dist, - platforms=[ - Platform( - os=os, arch=Arch.x86_64, minor_version=minor, micro_version=micro - ) - for minor, micro in [(8, 4)] - for os in [OS.linux, OS.windows] - ], - ) - got = deps.build() - - self.assertEqual(["arch_dep", "bar", "baz"], got.deps) - self.assertEqual( - { - "linux_x86_64": ["posix_dep", "posix_dep_with_version"], - }, - got.deps_select, - ) - - @mock.patch(_HOST_INTERPRETER_FN) - def test_can_get_version_select(self, mock_host_interpreter_version): - requires_dist = [ - "bar", - "baz; python_version < '3.8'", - "baz_new; python_version >= '3.8'", - "posix_dep; os_name=='posix'", - "posix_dep_with_version; os_name=='posix' and python_version >= '3.8'", - "arch_dep; platform_machine=='x86_64' and python_version < '3.8'", - ] - mock_host_interpreter_version.return_value = (7, 4) - - self.maxDiff = None - - deps = wheel.Deps( - "foo", - requires_dist=requires_dist, - platforms=[ - Platform( - os=os, arch=Arch.x86_64, minor_version=minor, micro_version=micro - ) - for minor, micro in [(7, 4), (8, 8), (9, 8)] - for os in [OS.linux, OS.windows] - ], - ) - got = deps.build() - - self.assertEqual(["bar"], got.deps) - self.assertEqual( - { - "cp37.4_linux_x86_64": ["arch_dep", "baz", "posix_dep"], - "cp37.4_windows_x86_64": ["arch_dep", "baz"], - "cp38.8_linux_x86_64": [ - "baz_new", - "posix_dep", - "posix_dep_with_version", - ], - "cp38.8_windows_x86_64": ["baz_new"], - "cp39.8_linux_x86_64": [ - "baz_new", - "posix_dep", - "posix_dep_with_version", - ], - "cp39.8_windows_x86_64": ["baz_new"], - "linux_x86_64": ["arch_dep", "baz", "posix_dep"], - "windows_x86_64": ["arch_dep", "baz"], - }, - got.deps_select, - ) - - @mock.patch(_HOST_INTERPRETER_FN) - def test_deps_spanning_all_target_py_versions_are_added_to_common( - self, mock_host_version - ): - requires_dist = [ - "bar", - "baz (<2,>=1.11) ; python_version < '3.8'", - "baz (<2,>=1.14) ; python_version >= '3.8'", - ] - mock_host_version.return_value = (8, 4) - - self.maxDiff = None - - deps = wheel.Deps( - "foo", - requires_dist=requires_dist, - platforms=Platform.from_string(["cp37_*", "cp38_*", "cp39_*"]), - ) - got = deps.build() - - self.assertEqual({}, got.deps_select) - self.assertEqual(["bar", "baz"], got.deps) - - @mock.patch(_HOST_INTERPRETER_FN) - def test_deps_are_not_duplicated(self, mock_host_version): - mock_host_version.return_value = (7, 4) - - # See an example in - # https://files.pythonhosted.org/packages/76/9e/db1c2d56c04b97981c06663384f45f28950a73d9acf840c4006d60d0a1ff/opencv_python-4.9.0.80-cp37-abi3-win32.whl.metadata - requires_dist = [ - "bar >=0.1.0 ; python_version < '3.7'", - "bar >=0.2.0 ; python_version >= '3.7'", - "bar >=0.4.0 ; python_version >= '3.6' and platform_system == 'Linux' and platform_machine == 'aarch64'", - "bar >=0.4.0 ; python_version >= '3.9'", - "bar >=0.5.0 ; python_version <= '3.9' and platform_system == 'Darwin' and platform_machine == 'arm64'", - "bar >=0.5.0 ; python_version >= '3.10' and platform_system == 'Darwin'", - "bar >=0.5.0 ; python_version >= '3.10'", - "bar >=0.6.0 ; python_version >= '3.11'", - ] - - deps = wheel.Deps( - "foo", - requires_dist=requires_dist, - platforms=Platform.from_string(["cp37_*", "cp310_*"]), - ) - got = deps.build() - - self.assertEqual(["bar"], got.deps) - self.assertEqual({}, got.deps_select) - - @mock.patch(_HOST_INTERPRETER_FN) - def test_deps_are_not_duplicated_when_encountering_platform_dep_first( - self, mock_host_version - ): - mock_host_version.return_value = (7, 1) - - # Note, that we are sorting the incoming `requires_dist` and we need to ensure that we are not getting any - # issues even if the platform-specific line comes first. - requires_dist = [ - "bar >=0.4.0 ; python_version >= '3.6' and platform_system == 'Linux' and platform_machine == 'aarch64'", - "bar >=0.5.0 ; python_version >= '3.9'", - ] - - self.maxDiff = None - - deps = wheel.Deps( - "foo", - requires_dist=requires_dist, - platforms=Platform.from_string( - [ - "cp37.1_linux_x86_64", - "cp37.1_linux_aarch64", - "cp310_linux_x86_64", - "cp310_linux_aarch64", - ] - ), - ) - got = deps.build() - - self.assertEqual([], got.deps) - self.assertEqual( - { - "cp310_linux_aarch64": ["bar"], - "cp310_linux_x86_64": ["bar"], - "cp37.1_linux_aarch64": ["bar"], - "linux_aarch64": ["bar"], - }, - got.deps_select, - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/pypi/whl_library/BUILD.bazel b/tests/pypi/whl_library/BUILD.bazel new file mode 100644 index 0000000000..cade0d2b8e --- /dev/null +++ b/tests/pypi/whl_library/BUILD.bazel @@ -0,0 +1,11 @@ +load("//python:py_test.bzl", "py_test") +load("//tests/support:support.bzl", "SUPPORTS_BZLMOD") + +py_test( + name = "whl_library_extras_test", + srcs = ["whl_library_extras_test.py"], + target_compatible_with = SUPPORTS_BZLMOD, + deps = [ + "@whl_library_extras_direct_dep//:pkg", + ], +) diff --git a/tests/pypi/whl_library/testdata/BUILD.bazel b/tests/pypi/whl_library/testdata/BUILD.bazel new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/pypi/whl_library/testdata/optional_dep/BUILD.bazel b/tests/pypi/whl_library/testdata/optional_dep/BUILD.bazel new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/pypi/whl_library/testdata/optional_dep/optional-dep-1.0.dist-info/METADATA b/tests/pypi/whl_library/testdata/optional_dep/optional-dep-1.0.dist-info/METADATA new file mode 100644 index 0000000000..6495d1ba36 --- /dev/null +++ b/tests/pypi/whl_library/testdata/optional_dep/optional-dep-1.0.dist-info/METADATA @@ -0,0 +1,2 @@ +Name: optional-dep +Version: 1.0 diff --git a/tests/pypi/whl_library/testdata/optional_dep/optional-dep-1.0.dist-info/RECORD b/tests/pypi/whl_library/testdata/optional_dep/optional-dep-1.0.dist-info/RECORD new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/pypi/whl_library/testdata/optional_dep/optional-dep-1.0.dist-info/WHEEL b/tests/pypi/whl_library/testdata/optional_dep/optional-dep-1.0.dist-info/WHEEL new file mode 100644 index 0000000000..a64521a1cc --- /dev/null +++ b/tests/pypi/whl_library/testdata/optional_dep/optional-dep-1.0.dist-info/WHEEL @@ -0,0 +1 @@ +Wheel-Version: 1.0 diff --git a/tests/pypi/whl_library/testdata/optional_dep/optional_dep.py b/tests/pypi/whl_library/testdata/optional_dep/optional_dep.py new file mode 100644 index 0000000000..4af2718944 --- /dev/null +++ b/tests/pypi/whl_library/testdata/optional_dep/optional_dep.py @@ -0,0 +1 @@ +I_AM_OPTIONAL = True diff --git a/tests/pypi/whl_library/testdata/packages.bzl b/tests/pypi/whl_library/testdata/packages.bzl new file mode 100644 index 0000000000..e4a9b0af4c --- /dev/null +++ b/tests/pypi/whl_library/testdata/packages.bzl @@ -0,0 +1,6 @@ +"""A list of packages that this logical testdata hub repo contains.""" + +packages = [ + "optional_dep", + "pkg", +] diff --git a/tests/pypi/whl_library/testdata/pkg/BUILD.bazel b/tests/pypi/whl_library/testdata/pkg/BUILD.bazel new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/pypi/whl_library/testdata/pkg/pkg-1.0.dist-info/METADATA b/tests/pypi/whl_library/testdata/pkg/pkg-1.0.dist-info/METADATA new file mode 100644 index 0000000000..712b44edbf --- /dev/null +++ b/tests/pypi/whl_library/testdata/pkg/pkg-1.0.dist-info/METADATA @@ -0,0 +1,4 @@ +Name: pkg +Version: 1.0 +Requires-Dist: optional_dep; extra == "optional" +Provides-Extra: optional diff --git a/tests/pypi/whl_library/testdata/pkg/pkg-1.0.dist-info/RECORD b/tests/pypi/whl_library/testdata/pkg/pkg-1.0.dist-info/RECORD new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/pypi/whl_library/testdata/pkg/pkg-1.0.dist-info/WHEEL b/tests/pypi/whl_library/testdata/pkg/pkg-1.0.dist-info/WHEEL new file mode 100644 index 0000000000..a64521a1cc --- /dev/null +++ b/tests/pypi/whl_library/testdata/pkg/pkg-1.0.dist-info/WHEEL @@ -0,0 +1 @@ +Wheel-Version: 1.0 diff --git a/tests/pypi/whl_library/testdata/pkg/pkg.py b/tests/pypi/whl_library/testdata/pkg/pkg.py new file mode 100644 index 0000000000..c5aca099c4 --- /dev/null +++ b/tests/pypi/whl_library/testdata/pkg/pkg.py @@ -0,0 +1,6 @@ +try: + import optional_dep + + WITH_EXTRAS = True +except ImportError: + WITH_EXTRAS = False diff --git a/tests/pypi/whl_library/whl_library_extras_test.py b/tests/pypi/whl_library/whl_library_extras_test.py new file mode 100644 index 0000000000..43cd5aec3f --- /dev/null +++ b/tests/pypi/whl_library/whl_library_extras_test.py @@ -0,0 +1,12 @@ +import unittest + + +class NamespacePackagesTest(unittest.TestCase): + def test_extras_propagated(self): + import pkg + + self.assertEqual(pkg.WITH_EXTRAS, True) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl b/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl index ec7ca63832..5bd1d1f549 100644 --- a/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl +++ b/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl @@ -15,12 +15,12 @@ "" load("@rules_testing//lib:test_suite.bzl", "test_suite") -load("//python/private:glob_excludes.bzl", "glob_excludes") # buildifier: disable=bzl-visibility load( "//python/private/pypi:whl_library_targets.bzl", "whl_library_targets", "whl_library_targets_from_requires", ) # buildifier: disable=bzl-visibility +load("//tests/support/mocks:mocks.bzl", "mocks") _tests = [] @@ -30,6 +30,8 @@ def _test_filegroups(env): def glob(include, *, exclude = [], allow_empty): _ = exclude # @unused env.expect.that_bool(allow_empty).equals(True) + if include == ["rewrite-bin/*"] or include == ["bin/*"]: + return [] return include whl_library_targets( @@ -39,7 +41,9 @@ def _test_filegroups(env): filegroup = lambda **kwargs: calls.append(kwargs), glob = glob, ), - rules = struct(), + rules = struct( + venv_rewrite_shebang = lambda **kwargs: None, + ), ) env.expect.that_collection(calls, expr = "filegroup calls").contains_exactly([ @@ -50,7 +54,7 @@ def _test_filegroups(env): }, { "name": "data", - "srcs": ["data/**"], + "srcs": ["data/**", "bin/**", "include/**"], "visibility": ["//visibility:public"], }, { @@ -68,75 +72,21 @@ def _test_filegroups(env): _tests.append(_test_filegroups) -def _test_platforms(env): - calls = [] - - whl_library_targets( - name = "", - dep_template = None, - dependencies_by_platform = { - "@//python/config_settings:is_python_3.9": ["py39_dep"], - "@platforms//cpu:aarch64": ["arm_dep"], - "@platforms//os:windows": ["win_dep"], - "cp310.11_linux_ppc64le": ["full_version_dep"], - "cp310_linux_ppc64le": ["py310_linux_ppc64le_dep"], - "linux_x86_64": ["linux_intel_dep"], - }, - filegroups = {}, - native = struct( - config_setting = lambda **kwargs: calls.append(kwargs), - ), - rules = struct(), - ) - - env.expect.that_collection(calls).contains_exactly([ - { - "name": "is_python_3.10.11_linux_ppc64le", - "visibility": ["//visibility:private"], - "constraint_values": [ - "@platforms//cpu:ppc64le", - "@platforms//os:linux", - ], - "flag_values": { - Label("//python/config_settings:python_version"): "3.10.11", - }, - }, - { - "name": "is_python_3.10_linux_ppc64le", - "visibility": ["//visibility:private"], - "constraint_values": [ - "@platforms//cpu:ppc64le", - "@platforms//os:linux", - ], - "flag_values": { - Label("//python/config_settings:python_version"): "3.10", - }, - }, - { - "name": "is_linux_x86_64", - "visibility": ["//visibility:private"], - "constraint_values": [ - "@platforms//cpu:x86_64", - "@platforms//os:linux", - ], - }, - ]) # buildifier: @unsorted-dict-items - -_tests.append(_test_platforms) - def _test_copy(env): calls = [] whl_library_targets( name = "", dep_template = None, - dependencies_by_platform = {}, filegroups = {}, copy_files = {"file_src": "file_dest"}, copy_executables = {"exec_src": "exec_dest"}, - native = struct(), + native = struct( + glob = lambda *args, **kwargs: [], + ), rules = struct( copy_file = lambda **kwargs: calls.append(kwargs), + venv_rewrite_shebang = lambda **kwargs: None, ), ) @@ -158,45 +108,18 @@ def _test_copy(env): _tests.append(_test_copy) -def _test_entrypoints(env): - calls = [] - - whl_library_targets( - name = "", - dep_template = None, - dependencies_by_platform = {}, - filegroups = {}, - entry_points = { - "fizz": "buzz.py", - }, - native = struct(), - rules = struct( - py_binary = lambda **kwargs: calls.append(kwargs), - ), - ) - - env.expect.that_collection(calls).contains_exactly([ - { - "name": "rules_python_wheel_entry_point_fizz", - "srcs": ["buzz.py"], - "deps": [":pkg"], - "imports": ["."], - "visibility": ["//visibility:public"], - }, - ]) # buildifier: @unsorted-dict-items - -_tests.append(_test_entrypoints) - def _test_whl_and_library_deps_from_requires(env): filegroup_calls = [] py_library_calls = [] env_marker_setting_calls = [] - mock_glob = _mock_glob() + m_glob = mocks.glob() - mock_glob.results.append(["site-packages/foo/SRCS.py"]) - mock_glob.results.append(["site-packages/foo/DATA.txt"]) - mock_glob.results.append(["site-packages/foo/PYI.pyi"]) + m_glob.results.append([]) # bin + m_glob.results.append([]) # rewrite-bin + m_glob.results.append(["site-packages/foo/SRCS.py"]) # srcs + m_glob.results.append(["site-packages/foo/DATA.txt"]) # data + m_glob.results.append(["site-packages/foo/PYI.pyi"]) # pyi whl_library_targets_from_requires( name = "foo-0-py3-none-any.whl", @@ -216,13 +139,13 @@ def _test_whl_and_library_deps_from_requires(env): native = struct( filegroup = lambda **kwargs: filegroup_calls.append(kwargs), config_setting = lambda **_: None, - glob = mock_glob.glob, - select = _select, + glob = m_glob.glob, ), rules = struct( py_library = lambda **kwargs: py_library_calls.append(kwargs), env_marker_setting = lambda **kwargs: env_marker_setting_calls.append(kwargs), create_inits = lambda *args, **kwargs: ["_create_inits_target"], + venv_rewrite_shebang = lambda **kwargs: None, ), ) @@ -230,7 +153,7 @@ def _test_whl_and_library_deps_from_requires(env): { "name": "whl", "srcs": ["foo-0-py3-none-any.whl"], - "data": ["@pypi//bar:whl"] + _select({ + "data": ["@pypi//bar:whl"] + select({ ":is_include_bar_baz_true": ["@pypi//bar_baz:whl"], "//conditions:default": [], }), @@ -245,41 +168,55 @@ def _test_whl_and_library_deps_from_requires(env): env.expect.that_dict(py_library_call).contains_exactly({ "name": "pkg", - "srcs": ["site-packages/foo/SRCS.py"] + _select({ - Label("//python/config_settings:is_venvs_site_packages"): [], + "srcs": ["site-packages/foo/SRCS.py"] + select({ + Label("//python/config_settings:_is_venvs_site_packages_yes"): [], "//conditions:default": ["_create_inits_target"], }), "pyi_srcs": ["site-packages/foo/PYI.pyi"], - "data": ["site-packages/foo/DATA.txt"], + "data": ["site-packages/foo/DATA.txt", "data"], "imports": ["site-packages"], - "deps": ["@pypi//bar:pkg"] + _select({ + "deps": ["@pypi//bar:pkg"] + select({ ":is_include_bar_baz_true": ["@pypi//bar_baz:pkg"], "//conditions:default": [], }), "tags": ["pypi_name=Foo", "pypi_version=0"], "visibility": ["//visibility:public"], "experimental_venvs_site_packages": Label("//python/config_settings:venvs_site_packages"), + "namespace_package_files": [] + select({ + Label("//python/config_settings:_is_venvs_site_packages_yes"): [], + "//conditions:default": ["_create_inits_target"], + }), }) # buildifier: @unsorted-dict-items - env.expect.that_collection(mock_glob.calls).contains_exactly([ + env.expect.that_collection(m_glob.calls).contains_exactly([ + # bin call + mocks.glob_call( + ["bin/*"], + allow_empty = True, + ), + # rewrite-bin call + mocks.glob_call( + ["rewrite-bin/*"], + allow_empty = True, + ), # srcs call - _glob_call( + mocks.glob_call( ["site-packages/**/*.py"], exclude = [], allow_empty = True, ), # data call - _glob_call( + mocks.glob_call( ["site-packages/**/*"], exclude = [ "**/*.py", "**/*.pyc", "**/*.pyc.*", - "**/*.dist-info/RECORD", - ] + glob_excludes.version_dependent_exclusions(), + ], + allow_empty = True, ), # pyi call - _glob_call(["site-packages/**/*.pyi"], allow_empty = True), + mocks.glob_call(["site-packages/**/*.pyi"], allow_empty = True), ]) env.expect.that_collection(env_marker_setting_calls).contains_exactly([ @@ -292,211 +229,46 @@ def _test_whl_and_library_deps_from_requires(env): _tests.append(_test_whl_and_library_deps_from_requires) -def _test_whl_and_library_deps(env): - filegroup_calls = [] +def _test_sdist_excludes_record(env): py_library_calls = [] - mock_glob = _mock_glob() - mock_glob.results.append(["site-packages/foo/SRCS.py"]) - mock_glob.results.append(["site-packages/foo/DATA.txt"]) - mock_glob.results.append(["site-packages/foo/PYI.pyi"]) + m_glob = mocks.glob() + m_glob.results.append([]) # bin + m_glob.results.append([]) # rewrite-bin + m_glob.results.append([]) # srcs + m_glob.results.append([]) # data + m_glob.results.append([]) # pyi whl_library_targets( name = "foo.whl", dep_template = "@pypi_{name}//:{target}", - dependencies = ["foo", "bar-baz"], - dependencies_by_platform = { - "@//python/config_settings:is_python_3.9": ["py39_dep"], - "@platforms//cpu:aarch64": ["arm_dep"], - "@platforms//os:windows": ["win_dep"], - "cp310_linux_ppc64le": ["py310_linux_ppc64le_dep"], - "cp39_anyos_aarch64": ["py39_arm_dep"], - "cp39_linux_anyarch": ["py39_linux_dep"], - "linux_x86_64": ["linux_intel_dep"], - }, - data_exclude = [], - tags = ["tag1", "tag2"], - # Overrides for testing + sdist_filename = "foo.tar.gz", filegroups = {}, native = struct( - filegroup = lambda **kwargs: filegroup_calls.append(kwargs), + filegroup = lambda **_: None, config_setting = lambda **_: None, - glob = mock_glob.glob, - select = _select, + glob = m_glob.glob, ), rules = struct( py_library = lambda **kwargs: py_library_calls.append(kwargs), - create_inits = lambda **kwargs: ["_create_inits_target"], + create_inits = lambda **kwargs: [], + venv_rewrite_shebang = lambda **kwargs: None, ), ) - env.expect.that_collection(filegroup_calls).contains_exactly([ - { - "name": "whl", - "srcs": ["foo.whl"], - "data": [ - "@pypi_bar_baz//:whl", - "@pypi_foo//:whl", - ] + _select( - { - Label("//python/config_settings:is_python_3.9"): ["@pypi_py39_dep//:whl"], - "@platforms//cpu:aarch64": ["@pypi_arm_dep//:whl"], - "@platforms//os:windows": ["@pypi_win_dep//:whl"], - ":is_python_3.10_linux_ppc64le": ["@pypi_py310_linux_ppc64le_dep//:whl"], - ":is_python_3.9_anyos_aarch64": ["@pypi_py39_arm_dep//:whl"], - ":is_python_3.9_linux_anyarch": ["@pypi_py39_linux_dep//:whl"], - ":is_linux_x86_64": ["@pypi_linux_intel_dep//:whl"], - "//conditions:default": [], - }, - ), - "visibility": ["//visibility:public"], - }, - ]) # buildifier: @unsorted-dict-items - - env.expect.that_collection(py_library_calls).has_size(1) - if len(py_library_calls) != 1: - return - env.expect.that_dict(py_library_calls[0]).contains_exactly({ - "name": "pkg", - "srcs": ["site-packages/foo/SRCS.py"] + _select({ - Label("//python/config_settings:is_venvs_site_packages"): [], - "//conditions:default": ["_create_inits_target"], - }), - "pyi_srcs": ["site-packages/foo/PYI.pyi"], - "data": ["site-packages/foo/DATA.txt"], - "imports": ["site-packages"], - "deps": [ - "@pypi_bar_baz//:pkg", - "@pypi_foo//:pkg", - ] + _select( - { - Label("//python/config_settings:is_python_3.9"): ["@pypi_py39_dep//:pkg"], - "@platforms//cpu:aarch64": ["@pypi_arm_dep//:pkg"], - "@platforms//os:windows": ["@pypi_win_dep//:pkg"], - ":is_python_3.10_linux_ppc64le": ["@pypi_py310_linux_ppc64le_dep//:pkg"], - ":is_python_3.9_anyos_aarch64": ["@pypi_py39_arm_dep//:pkg"], - ":is_python_3.9_linux_anyarch": ["@pypi_py39_linux_dep//:pkg"], - ":is_linux_x86_64": ["@pypi_linux_intel_dep//:pkg"], - "//conditions:default": [], - }, - ), - "tags": ["tag1", "tag2"], - "visibility": ["//visibility:public"], - "experimental_venvs_site_packages": Label("//python/config_settings:venvs_site_packages"), - }) # buildifier: @unsorted-dict-items - -_tests.append(_test_whl_and_library_deps) - -def _test_group(env): - alias_calls = [] - py_library_calls = [] - - mock_glob = _mock_glob() - mock_glob.results.append(["site-packages/foo/srcs.py"]) - mock_glob.results.append(["site-packages/foo/data.txt"]) - mock_glob.results.append(["site-packages/foo/pyi.pyi"]) - - whl_library_targets( - name = "foo.whl", - dep_template = "@pypi_{name}//:{target}", - dependencies = ["foo", "bar-baz", "qux"], - dependencies_by_platform = { - "linux_x86_64": ["box", "box-amd64"], - "windows_x86_64": ["fox"], - "@platforms//os:linux": ["box"], # buildifier: disable=unsorted-dict-items to check that we sort inside the test - }, - tags = [], - entry_points = {}, - data_exclude = [], - group_name = "qux", - group_deps = ["foo", "fox", "qux"], - # Overrides for testing - filegroups = {}, - native = struct( - config_setting = lambda **_: None, - glob = mock_glob.glob, - alias = lambda **kwargs: alias_calls.append(kwargs), - select = _select, - ), - rules = struct( - py_library = lambda **kwargs: py_library_calls.append(kwargs), - create_inits = lambda **kwargs: ["_create_inits_target"], + env.expect.that_collection(m_glob.calls).contains_at_least([ + mocks.glob_call( + ["site-packages/**/*"], + exclude = [ + "**/*.py", + "**/*.pyc", + "**/*.pyc.*", + "**/*.dist-info/RECORD", + ], + allow_empty = True, ), - ) - - env.expect.that_collection(alias_calls).contains_exactly([ - {"name": "pkg", "actual": "@pypi__groups//:qux_pkg", "visibility": ["//visibility:public"]}, - {"name": "whl", "actual": "@pypi__groups//:qux_whl", "visibility": ["//visibility:public"]}, - ]) # buildifier: @unsorted-dict-items - - env.expect.that_collection(py_library_calls).has_size(1) - if len(py_library_calls) != 1: - return - - py_library_call = py_library_calls[0] - env.expect.where(case = "verify py library call").that_dict( - py_library_call, - ).contains_exactly({ - "name": "_pkg", - "srcs": ["site-packages/foo/srcs.py"] + _select({ - Label("//python/config_settings:is_venvs_site_packages"): [], - "//conditions:default": ["_create_inits_target"], - }), - "pyi_srcs": ["site-packages/foo/pyi.pyi"], - "data": ["site-packages/foo/data.txt"], - "imports": ["site-packages"], - "deps": ["@pypi_bar_baz//:pkg"] + _select({ - "@platforms//os:linux": ["@pypi_box//:pkg"], - ":is_linux_x86_64": ["@pypi_box//:pkg", "@pypi_box_amd64//:pkg"], - "//conditions:default": [], - }), - "tags": [], - "visibility": ["@pypi__groups//:__pkg__"], - "experimental_venvs_site_packages": Label("//python/config_settings:venvs_site_packages"), - }) # buildifier: @unsorted-dict-items - - env.expect.that_collection(mock_glob.calls, expr = "glob calls").contains_exactly([ - _glob_call(["site-packages/**/*.py"], exclude = [], allow_empty = True), - _glob_call(["site-packages/**/*"], exclude = [ - "**/*.py", - "**/*.pyc", - "**/*.pyc.*", - "**/*.dist-info/RECORD", - ]), - _glob_call(["site-packages/**/*.pyi"], allow_empty = True), ]) -_tests.append(_test_group) - -def _glob_call(*args, **kwargs): - return struct( - glob = args, - kwargs = kwargs, - ) - -def _mock_glob(): - # buildifier: disable=uninitialized - def glob(*args, **kwargs): - mock.calls.append(_glob_call(*args, **kwargs)) - if not mock.results: - fail("Mock glob missing for invocation: args={} kwargs={}".format( - args, - kwargs, - )) - return mock.results.pop(0) - - mock = struct( - calls = [], - results = [], - glob = glob, - ) - return mock - -def _select(*args, **kwargs): - """We need to have this mock select because we still need to support bazel 6.""" - return [struct( - select = args, - kwargs = kwargs, - )] +_tests.append(_test_sdist_excludes_record) def whl_library_targets_test_suite(name): """create the test suite. diff --git a/tests/pypi/whl_metadata/whl_metadata_tests.bzl b/tests/pypi/whl_metadata/whl_metadata_tests.bzl index 329423a26c..8131b0f452 100644 --- a/tests/pypi/whl_metadata/whl_metadata_tests.bzl +++ b/tests/pypi/whl_metadata/whl_metadata_tests.bzl @@ -5,6 +5,7 @@ load("@rules_testing//lib:truth.bzl", "subjects") load( "//python/private/pypi:whl_metadata.bzl", "find_whl_metadata", + "parse_entry_points", "parse_whl_metadata", ) # buildifier: disable=bzl-visibility @@ -171,6 +172,80 @@ Requires-Dist: this will be ignored _tests.append(_test_parse_metadata_multiline_license) +def _test_parse_entry_points(env): + got = parse_entry_points("""\ +[something] +interesting # with comments + +[console_scripts] +foo = foomod:main +# One which depends on extras: +foobar = importable.foomod:main_bar [bar, baz] + + # With a comment at the end +foobarbaz = foomod:main.attr # comment + +# With extra and comment +foo_extra_comment = foomod:main [extra] # comment + +[something else] +not very much interesting +""") + env.expect.that_dict(got).contains_exactly({ + "foo": { + "attribute": "main", + "extras": "", + "group": "console_scripts", + "module": "foomod", + "name": "foo", + }, + "foo_extra_comment": { + "attribute": "main", + "extras": "extra", + "group": "console_scripts", + "module": "foomod", + "name": "foo_extra_comment", + }, + "foobar": { + "attribute": "main_bar", + "extras": "bar, baz", + "group": "console_scripts", + "module": "importable.foomod", + "name": "foobar", + }, + "foobarbaz": { + "attribute": "main.attr", + "extras": "", + "group": "console_scripts", + "module": "foomod", + "name": "foobarbaz", + }, + }) + +_tests.append(_test_parse_entry_points) + +def _test_parse_entry_points_deduplicate(env): + got = parse_entry_points("""\ +[console_scripts] +FooBar = foomod:main +foobar = othermod:main +fooBAR = another:main + +[gui_scripts] +FOOBAR = guimod:main +""") + env.expect.that_dict(got).contains_exactly({ + "FooBar": { + "attribute": "main", + "extras": "", + "group": "console_scripts", + "module": "foomod", + "name": "FooBar", + }, + }) + +_tests.append(_test_parse_entry_points_deduplicate) + def whl_metadata_test_suite(name): # buildifier: disable=function-docstring test_suite( name = name, diff --git a/tests/pypi/whl_target_platforms/whl_target_platforms_tests.bzl b/tests/pypi/whl_target_platforms/whl_target_platforms_tests.bzl index a976a0cf95..6bec26c10c 100644 --- a/tests/pypi/whl_target_platforms/whl_target_platforms_tests.bzl +++ b/tests/pypi/whl_target_platforms/whl_target_platforms_tests.bzl @@ -34,6 +34,9 @@ def _test_simple(env): "musllinux_1_1_ppc64le": [ struct(os = "linux", cpu = "ppc64le", abi = None, target_platform = "linux_ppc64le", version = (1, 1)), ], + "musllinux_1_2_riscv64": [ + struct(os = "linux", cpu = "riscv64", abi = None, target_platform = "linux_riscv64", version = (1, 2)), + ], "win_amd64": [ struct(os = "windows", cpu = "x86_64", abi = None, target_platform = "windows_x86_64", version = (0, 0)), ], @@ -66,6 +69,9 @@ def _test_with_abi(env): "musllinux_1_1_ppc64le": [ struct(os = "linux", cpu = "ppc64le", abi = "cp311", target_platform = "cp311_linux_ppc64le", version = (1, 1)), ], + "musllinux_1_2_riscv64": [ + struct(os = "linux", cpu = "riscv64", abi = "cp311", target_platform = "cp311_linux_riscv64", version = (1, 2)), + ], "win_amd64": [ struct(os = "windows", cpu = "x86_64", abi = "cp311", target_platform = "cp311_windows_x86_64", version = (0, 0)), ], @@ -103,6 +109,7 @@ def _can_parse_existing_tags(env): "manylinux_11_12_i686": 1, "manylinux_11_12_ppc64": 1, "manylinux_11_12_ppc64le": 1, + "manylinux_11_12_riscv64": 1, "manylinux_11_12_s390x": 1, "manylinux_11_12_x86_64": 1, "manylinux_1_2_aarch64": 1, @@ -111,6 +118,7 @@ def _can_parse_existing_tags(env): "musllinux_11_12_armv7l": 1, "musllinux_11_12_i686": 1, "musllinux_11_12_ppc64le": 1, + "musllinux_11_12_riscv64": 1, "musllinux_11_12_s390x": 1, "musllinux_11_12_x86_64": 1, "win32": 1, diff --git a/tests/python/BUILD.bazel b/tests/python/BUILD.bazel index 2553536b63..887fe969b5 100644 --- a/tests/python/BUILD.bazel +++ b/tests/python/BUILD.bazel @@ -12,6 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -load(":python_tests.bzl", "python_test_suite") +load(":python_tests.bzl", "register_python_tests") -python_test_suite(name = "python_tests") +register_python_tests(name = "python_tests") diff --git a/tests/python/python_tests.bzl b/tests/python/python_tests.bzl index 9081a0e306..cbef5637fd 100644 --- a/tests/python/python_tests.bzl +++ b/tests/python/python_tests.bzl @@ -12,136 +12,31 @@ # See the License for the specific language governing permissions and # limitations under the License. -"" +"""Unit tests for //python/extensions:python.bzl bzlmod extension.""" load("@pythons_hub//:versions.bzl", "MINOR_MAPPING") load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility load("//python/private:python.bzl", "parse_modules") # buildifier: disable=bzl-visibility load("//python/private:repo_utils.bzl", "repo_utils") # buildifier: disable=bzl-visibility +load("//tests/support/mocks:python_ext.bzl", "python_ext") _tests = [] -def _mock_mctx(*modules, environ = {}, mocked_files = {}): - return struct( - path = lambda x: struct(exists = x in mocked_files, _file = x), - read = lambda x, watch = None: mocked_files[x._file if "_file" in dir(x) else x], - getenv = environ.get, - os = struct(environ = environ), - modules = [ - struct( - name = modules[0].name, - tags = modules[0].tags, - is_root = modules[0].is_root, - ), - ] + [ - struct( - name = mod.name, - tags = mod.tags, - is_root = False, - ) - for mod in modules[1:] - ], - ) - -def _mod(*, name, defaults = [], toolchain = [], override = [], single_version_override = [], single_version_platform_override = [], is_root = True): - return struct( - name = name, - tags = struct( - defaults = defaults, - toolchain = toolchain, - override = override, - single_version_override = single_version_override, - single_version_platform_override = single_version_platform_override, - ), +def _rules_python_module(is_root = False): + """A mock of what the real rules_python MODULE.bazel looks like.""" + return python_ext.module( + name = "rules_python", + defaults = [python_ext.defaults(python_version = "3.11")], + toolchain = [python_ext.toolchain(python_version = "3.11")], is_root = is_root, ) -def _defaults(python_version = None, python_version_env = None, python_version_file = None): - return struct( - python_version = python_version, - python_version_env = python_version_env, - python_version_file = python_version_file, - ) - -def _toolchain(python_version, *, is_default = False, **kwargs): - return struct( - is_default = is_default, - python_version = python_version, - **kwargs - ) - -def _override( - auth_patterns = {}, - available_python_versions = [], - base_url = "", - ignore_root_user_error = True, - minor_mapping = {}, - netrc = "", - register_all_versions = False): - return struct( - auth_patterns = auth_patterns, - available_python_versions = available_python_versions, - base_url = base_url, - ignore_root_user_error = ignore_root_user_error, - minor_mapping = minor_mapping, - netrc = netrc, - register_all_versions = register_all_versions, - ) - -def _single_version_override( - python_version = "", - sha256 = {}, - urls = [], - patch_strip = 0, - patches = [], - strip_prefix = "python", - distutils_content = "", - distutils = None): - if not python_version: - fail("missing mandatory args: python_version ({})".format(python_version)) - - return struct( - python_version = python_version, - sha256 = sha256, - urls = urls, - patch_strip = patch_strip, - patches = patches, - strip_prefix = strip_prefix, - distutils_content = distutils_content, - distutils = distutils, - ) - -def _single_version_platform_override( - coverage_tool = None, - patch_strip = 0, - patches = [], - platform = "", - python_version = "", - sha256 = "", - strip_prefix = "python", - urls = []): - if not platform or not python_version: - fail("missing mandatory args: platform ({}) and python_version ({})".format(platform, python_version)) - - return struct( - sha256 = sha256, - urls = urls, - strip_prefix = strip_prefix, - platform = platform, - coverage_tool = coverage_tool, - python_version = python_version, - patch_strip = patch_strip, - patches = patches, - target_compatible_with = [], - target_settings = [], - os_name = "", - arch = "", - ) - -def _test_default(env): +def _test_default_from_rules_python_when_rules_python_is_root(env): + """Verify that rules_python (as root module) default is applied.""" py = parse_modules( - module_ctx = _mock_mctx( - _mod(name = "rules_python", toolchain = [_toolchain("3.11")]), + module_ctx = python_ext.mctx( + _rules_python_module(is_root = True), ), logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) @@ -152,12 +47,10 @@ def _test_default(env): env.expect.that_dict(py.config.minor_mapping).contains_exactly(MINOR_MAPPING) env.expect.that_collection(py.config.kwargs).has_size(0) env.expect.that_collection(py.config.default.keys()).contains_exactly([ - "base_url", - "ignore_root_user_error", + "base_urls", "tool_versions", "platforms", ]) - env.expect.that_bool(py.config.default["ignore_root_user_error"]).equals(True) env.expect.that_str(py.default_python_version).equals("3.11") want_toolchain = struct( @@ -167,12 +60,13 @@ def _test_default(env): ) env.expect.that_collection(py.toolchains).contains_exactly([want_toolchain]) -_tests.append(_test_default) +_tests.append(_test_default_from_rules_python_when_rules_python_is_root) -def _test_default_some_module(env): +def _test_default_from_rules_python_when_rules_python_is_not_root(env): + """Verify that rules_python default applies when rules_python is not the root module.""" py = parse_modules( - module_ctx = _mock_mctx( - _mod(name = "rules_python", toolchain = [_toolchain("3.11")], is_root = False), + module_ctx = python_ext.mctx( + _rules_python_module(), ), logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) @@ -186,12 +80,19 @@ def _test_default_some_module(env): ) env.expect.that_collection(py.toolchains).contains_exactly([want_toolchain]) -_tests.append(_test_default_some_module) +_tests.append(_test_default_from_rules_python_when_rules_python_is_not_root) def _test_default_with_patch_version(env): py = parse_modules( - module_ctx = _mock_mctx( - _mod(name = "rules_python", toolchain = [_toolchain("3.11.2")]), + module_ctx = python_ext.mctx( + modules = [ + python_ext.module( + name = "alpha", + is_root = True, + toolchain = [python_ext.toolchain(python_version = "3.11.2")], + ), + _rules_python_module(is_root = False), + ], ), logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) @@ -203,115 +104,30 @@ def _test_default_with_patch_version(env): python_version = "3.11.2", register_coverage_tool = False, ) - env.expect.that_collection(py.toolchains).contains_exactly([want_toolchain]) + env.expect.that_collection(py.toolchains).contains_at_least([want_toolchain]) _tests.append(_test_default_with_patch_version) -def _test_default_non_rules_python(env): - py = parse_modules( - module_ctx = _mock_mctx( - # NOTE @aignas 2024-09-06: the first item in the module_ctx.modules - # could be a non-root module, which is the case if the root module - # does not make any calls to the extension. - _mod(name = "rules_python", toolchain = [_toolchain("3.11")], is_root = False), - ), - logger = repo_utils.logger(verbosity_level = 0, name = "python"), - ) - - env.expect.that_str(py.default_python_version).equals("3.11") - rules_python_toolchain = struct( - name = "python_3_11", - python_version = "3.11", - register_coverage_tool = False, - ) - env.expect.that_collection(py.toolchains).contains_exactly([rules_python_toolchain]) - -_tests.append(_test_default_non_rules_python) - -def _test_default_non_rules_python_ignore_root_user_error(env): - py = parse_modules( - module_ctx = _mock_mctx( - _mod( - name = "my_module", - toolchain = [_toolchain("3.12", ignore_root_user_error = False)], - ), - _mod(name = "rules_python", toolchain = [_toolchain("3.11")]), - ), - logger = repo_utils.logger(verbosity_level = 0, name = "python"), - ) - - env.expect.that_bool(py.config.default["ignore_root_user_error"]).equals(False) - env.expect.that_str(py.default_python_version).equals("3.12") - - my_module_toolchain = struct( - name = "python_3_12", - python_version = "3.12", - register_coverage_tool = False, - ) - rules_python_toolchain = struct( - name = "python_3_11", - python_version = "3.11", - register_coverage_tool = False, - ) - env.expect.that_collection(py.toolchains).contains_exactly([ - rules_python_toolchain, - my_module_toolchain, - ]).in_order() - -_tests.append(_test_default_non_rules_python_ignore_root_user_error) - -def _test_default_non_rules_python_ignore_root_user_error_non_root_module(env): - py = parse_modules( - module_ctx = _mock_mctx( - _mod(name = "my_module", toolchain = [_toolchain("3.13")]), - _mod(name = "some_module", toolchain = [_toolchain("3.12", ignore_root_user_error = False)]), - _mod(name = "rules_python", toolchain = [_toolchain("3.11")]), - ), - logger = repo_utils.logger(verbosity_level = 0, name = "python"), - ) - - env.expect.that_str(py.default_python_version).equals("3.13") - env.expect.that_bool(py.config.default["ignore_root_user_error"]).equals(True) - - my_module_toolchain = struct( - name = "python_3_13", - python_version = "3.13", - register_coverage_tool = False, - ) - some_module_toolchain = struct( - name = "python_3_12", - python_version = "3.12", - register_coverage_tool = False, - ) - rules_python_toolchain = struct( - name = "python_3_11", - python_version = "3.11", - register_coverage_tool = False, - ) - env.expect.that_collection(py.toolchains).contains_exactly([ - some_module_toolchain, - rules_python_toolchain, - my_module_toolchain, # this was the only toolchain, default to that - ]).in_order() - -_tests.append(_test_default_non_rules_python_ignore_root_user_error_non_root_module) - def _test_toolchain_ordering(env): py = parse_modules( - module_ctx = _mock_mctx( - _mod( + module_ctx = python_ext.mctx( + python_ext.module( name = "my_module", + is_root = True, toolchain = [ - _toolchain("3.10"), - _toolchain("3.10.15"), - _toolchain("3.10.18"), - _toolchain("3.10.13"), - _toolchain("3.11.1"), - _toolchain("3.11.10"), - _toolchain("3.11.13", is_default = True), + python_ext.toolchain(python_version = "3.10"), + python_ext.toolchain(python_version = "3.10.15"), + python_ext.toolchain(python_version = MINOR_MAPPING["3.10"]), + python_ext.toolchain(python_version = "3.10.13"), + python_ext.toolchain(python_version = "3.11.1"), + python_ext.toolchain(python_version = "3.11.10"), + python_ext.toolchain( + python_version = MINOR_MAPPING["3.11"], + is_default = True, + ), ], ), - _mod(name = "rules_python", toolchain = [_toolchain("3.11")]), + _rules_python_module(), ), logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) @@ -320,16 +136,8 @@ def _test_toolchain_ordering(env): for t in py.toolchains ] - env.expect.that_str(py.default_python_version).equals("3.11.13") - env.expect.that_dict(py.config.minor_mapping).contains_exactly({ - "3.10": "3.10.18", - "3.11": "3.11.13", - "3.12": "3.12.11", - "3.13": "3.13.6", - "3.14": "3.14.0rc1", - "3.8": "3.8.20", - "3.9": "3.9.23", - }) + env.expect.that_str(py.default_python_version).equals(MINOR_MAPPING["3.11"]) + env.expect.that_dict(py.config.minor_mapping).contains_exactly(MINOR_MAPPING) env.expect.that_collection(got_versions).contains_exactly([ # First the full-version toolchains that are in minor_mapping # so that they get matched first if only the `python_version` is in MINOR_MAPPING @@ -337,9 +145,9 @@ def _test_toolchain_ordering(env): # The default version is always set in the `python_version` flag, so know, that # the default match will be somewhere in the first bunch. "3.10", - "3.10.18", + MINOR_MAPPING["3.10"], "3.11", - "3.11.13", + MINOR_MAPPING["3.11"], # Next, the rest, where we will match things based on the `python_version` being # the same "3.10.15", @@ -352,12 +160,16 @@ _tests.append(_test_toolchain_ordering) def _test_default_from_defaults(env): py = parse_modules( - module_ctx = _mock_mctx( - _mod( + module_ctx = python_ext.mctx( + python_ext.module( name = "my_root_module", - defaults = [_defaults(python_version = "3.11")], - toolchain = [_toolchain("3.10"), _toolchain("3.11"), _toolchain("3.12")], + defaults = [python_ext.defaults(python_version = "3.11")], is_root = True, + toolchain = [ + python_ext.toolchain(python_version = "3.10"), + python_ext.toolchain(python_version = "3.11"), + python_ext.toolchain(python_version = "3.12"), + ], ), ), logger = repo_utils.logger(verbosity_level = 0, name = "python"), @@ -379,12 +191,21 @@ _tests.append(_test_default_from_defaults) def _test_default_from_defaults_env(env): py = parse_modules( - module_ctx = _mock_mctx( - _mod( + module_ctx = python_ext.mctx( + python_ext.module( name = "my_root_module", - defaults = [_defaults(python_version = "3.11", python_version_env = "PYENV_VERSION")], - toolchain = [_toolchain("3.10"), _toolchain("3.11"), _toolchain("3.12")], + defaults = [ + python_ext.defaults( + python_version = "3.11", + python_version_env = "PYENV_VERSION", + ), + ], is_root = True, + toolchain = [ + python_ext.toolchain(python_version = "3.10"), + python_ext.toolchain(python_version = "3.11"), + python_ext.toolchain(python_version = "3.12"), + ], ), environ = {"PYENV_VERSION": "3.12"}, ), @@ -407,14 +228,22 @@ _tests.append(_test_default_from_defaults_env) def _test_default_from_defaults_file(env): py = parse_modules( - module_ctx = _mock_mctx( - _mod( + module_ctx = python_ext.mctx( + python_ext.module( name = "my_root_module", - defaults = [_defaults(python_version_file = "@@//:.python-version")], - toolchain = [_toolchain("3.10"), _toolchain("3.11"), _toolchain("3.12")], + defaults = [ + python_ext.defaults( + python_version_file = "@@//:.python-version", + ), + ], is_root = True, + toolchain = [ + python_ext.toolchain(python_version = "3.10"), + python_ext.toolchain(python_version = "3.11"), + python_ext.toolchain(python_version = "3.12"), + ], ), - mocked_files = {"@@//:.python-version": "3.12\n"}, + mock_files = {"@@//:.python-version": "3.12\n"}, ), logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) @@ -433,12 +262,86 @@ def _test_default_from_defaults_file(env): _tests.append(_test_default_from_defaults_file) +def _test_default_from_single_toolchain(env): + py = parse_modules( + module_ctx = python_ext.mctx( + python_ext.module( + name = "my_root_module", + is_root = True, + toolchain = [python_ext.toolchain(python_version = "3.12")], + ), + _rules_python_module(), + ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), + ) + env.expect.that_str(py.default_python_version).equals("3.12") + +_tests.append(_test_default_from_single_toolchain) + +def _test_defaults_overrides_single_toolchain(env): + py = parse_modules( + module_ctx = python_ext.mctx( + python_ext.module( + name = "my_root_module", + defaults = [ + # This relies on rules_python registering 3.11 + python_ext.defaults(python_version = "3.11"), + ], + is_root = True, + toolchain = [python_ext.toolchain(python_version = "3.12")], + ), + _rules_python_module(), + ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), + ) + env.expect.that_str(py.default_python_version).equals("3.11") + +_tests.append(_test_defaults_overrides_single_toolchain) + +def _test_defaults_overrides_toolchains_setting_is_default(env): + py = parse_modules( + module_ctx = python_ext.mctx( + python_ext.module( + name = "my_root_module", + defaults = [python_ext.defaults(python_version = "3.13")], + is_root = True, + toolchain = [ + python_ext.toolchain(python_version = "3.13"), + python_ext.toolchain( + python_version = "3.12", + is_default = True, + ), + ], + ), + _rules_python_module(), + ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), + ) + env.expect.that_str(py.default_python_version).equals("3.13") + +_tests.append(_test_defaults_overrides_toolchains_setting_is_default) + def _test_first_occurance_of_the_toolchain_wins(env): py = parse_modules( - module_ctx = _mock_mctx( - _mod(name = "my_module", toolchain = [_toolchain("3.12")]), - _mod(name = "some_module", toolchain = [_toolchain("3.12", configure_coverage_tool = True)]), - _mod(name = "rules_python", toolchain = [_toolchain("3.11")]), + module_ctx = python_ext.mctx( + modules = [ + python_ext.module( + name = "my_module", + is_root = True, + toolchain = [python_ext.toolchain(python_version = "3.12")], + ), + python_ext.module( + name = "some_module", + is_root = False, + toolchain = [ + python_ext.toolchain( + python_version = "3.12", + configure_coverage_tool = True, + ), + ], + ), + _rules_python_module(), + ], environ = { "RULES_PYTHON_BZLMOD_DEBUG": "1", }, @@ -467,8 +370,8 @@ def _test_first_occurance_of_the_toolchain_wins(env): env.expect.that_dict(py.debug_info).contains_exactly({ "toolchains_registered": [ - {"ignore_root_user_error": True, "module": {"is_root": True, "name": "my_module"}, "name": "python_3_12"}, - {"ignore_root_user_error": True, "module": {"is_root": False, "name": "rules_python"}, "name": "python_3_11"}, + {"module": {"is_root": True, "name": "my_module"}, "name": "python_3_12"}, + {"module": {"is_root": False, "name": "rules_python"}, "name": "python_3_11"}, ], }) @@ -476,25 +379,25 @@ _tests.append(_test_first_occurance_of_the_toolchain_wins) def _test_auth_overrides(env): py = parse_modules( - module_ctx = _mock_mctx( - _mod( + module_ctx = python_ext.mctx( + python_ext.module( name = "my_module", - toolchain = [_toolchain("3.12")], + is_root = True, override = [ - _override( - netrc = "/my/netrc", + python_ext.override( auth_patterns = {"foo": "bar"}, + netrc = "/my/netrc", ), ], + toolchain = [python_ext.toolchain(python_version = "3.12")], ), - _mod(name = "rules_python", toolchain = [_toolchain("3.11")]), + _rules_python_module(), ), logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) env.expect.that_dict(py.config.default).contains_at_least({ "auth_patterns": {"foo": "bar"}, - "ignore_root_user_error": True, "netrc": "/my/netrc", }) env.expect.that_str(py.default_python_version).equals("3.12") @@ -516,47 +419,79 @@ def _test_auth_overrides(env): _tests.append(_test_auth_overrides) +def _test_add_target_settings(env): + py = parse_modules( + module_ctx = python_ext.mctx( + python_ext.module( + name = "my_module", + is_root = True, + override = [ + python_ext.override( + add_target_settings = [ + "@@//my:custom_setting", + ], + ), + ], + toolchain = [python_ext.toolchain(python_version = "3.12")], + ), + _rules_python_module(), + ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), + ) + + env.expect.that_collection( + py.config.add_target_settings, + ).contains_exactly(["@@//my:custom_setting"]) + +_tests.append(_test_add_target_settings) + def _test_add_new_version(env): py = parse_modules( - module_ctx = _mock_mctx( - _mod( + module_ctx = python_ext.mctx( + python_ext.module( name = "my_module", - toolchain = [_toolchain("3.13")], + is_root = True, + override = [ + python_ext.override( + available_python_versions = [ + "3.12.4", + "3.13.0", + "3.13.1", + "3.13.99", + ], + base_urls = [], + minor_mapping = { + "3.13": "3.13.99", + }, + ), + ], single_version_override = [ - _single_version_override( + python_ext.single_version_override( + distutils = None, + distutils_content = "", + patch_strip = 0, + patches = [], python_version = "3.13.0", sha256 = { "aarch64-unknown-linux-gnu": "deadbeef", }, - urls = ["example.org"], - patch_strip = 0, - patches = [], strip_prefix = "prefix", - distutils_content = "", - distutils = None, + urls = ["example.org"], ), ], single_version_platform_override = [ - _single_version_platform_override( - sha256 = "deadb00f", - urls = ["something.org", "else.org"], - strip_prefix = "python", - platform = "aarch64-unknown-linux-gnu", + python_ext.single_version_platform_override( coverage_tool = "specific_cov_tool", - python_version = "3.13.99", patch_strip = 2, patches = ["specific-patch.txt"], + platform = "aarch64-unknown-linux-gnu", + python_version = "3.13.99", + sha256 = "deadb00f", + strip_prefix = "python", + urls = ["something.org", "else.org"], ), ], - override = [ - _override( - base_url = "", - available_python_versions = ["3.12.4", "3.13.0", "3.13.1", "3.13.99"], - minor_mapping = { - "3.13": "3.13.99", - }, - ), - ], + toolchain = [python_ext.toolchain(python_version = "3.13")], ), ), logger = repo_utils.logger(verbosity_level = 0, name = "python"), @@ -598,12 +533,24 @@ _tests.append(_test_add_new_version) def _test_register_all_versions(env): py = parse_modules( - module_ctx = _mock_mctx( - _mod( + module_ctx = python_ext.mctx( + python_ext.module( name = "my_module", - toolchain = [_toolchain("3.13")], + is_root = True, + override = [ + python_ext.override( + available_python_versions = [ + "3.12.4", + "3.13.0", + "3.13.1", + "3.13.99", + ], + base_urls = [], + register_all_versions = True, + ), + ], single_version_override = [ - _single_version_override( + python_ext.single_version_override( python_version = "3.13.0", sha256 = { "aarch64-unknown-linux-gnu": "deadbeef", @@ -612,20 +559,14 @@ def _test_register_all_versions(env): ), ], single_version_platform_override = [ - _single_version_platform_override( - sha256 = "deadb00f", - urls = ["something.org"], + python_ext.single_version_platform_override( platform = "aarch64-unknown-linux-gnu", python_version = "3.13.99", + sha256 = "deadb00f", + urls = ["something.org"], ), ], - override = [ - _override( - base_url = "", - available_python_versions = ["3.12.4", "3.13.0", "3.13.1", "3.13.99"], - register_all_versions = True, - ), - ], + toolchain = [python_ext.toolchain(python_version = "3.13")], ), ), logger = repo_utils.logger(verbosity_level = 0, name = "python"), @@ -661,48 +602,127 @@ def _test_register_all_versions(env): _tests.append(_test_register_all_versions) -def _test_add_patches(env): +def _test_ignore_unsupported_versions(env): py = parse_modules( - module_ctx = _mock_mctx( - _mod( + module_ctx = python_ext.mctx( + python_ext.module( name = "my_module", - toolchain = [_toolchain("3.13")], + is_root = True, + override = [ + python_ext.override( + available_python_versions = [ + "3.12.4", + "3.13.0", + "3.13.1", + ], + base_urls = [], + minor_mapping = { + "3.12": "3.12.4", + "3.13": "3.13.1", + }, + ), + ], single_version_override = [ - _single_version_override( + python_ext.single_version_override( python_version = "3.13.0", sha256 = { - "aarch64-apple-darwin": "deadbeef", "aarch64-unknown-linux-gnu": "deadbeef", }, urls = ["example.org"], - patch_strip = 1, - patches = ["common.txt"], - strip_prefix = "prefix", - distutils_content = "", - distutils = None, ), ], single_version_platform_override = [ - _single_version_platform_override( - sha256 = "deadb00f", - urls = ["something.org", "else.org"], - strip_prefix = "python", + python_ext.single_version_platform_override( platform = "aarch64-unknown-linux-gnu", - coverage_tool = "specific_cov_tool", - python_version = "3.13.0", - patch_strip = 2, - patches = ["specific-patch.txt"], + python_version = "3.13.99", + sha256 = "deadb00f", + urls = ["something.org"], ), ], + toolchain = [ + python_ext.toolchain(python_version = "3.11"), + python_ext.toolchain(python_version = "3.12"), + python_ext.toolchain( + python_version = "3.13", + is_default = True, + ), + ], + ), + ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), + ) + + env.expect.that_str(py.default_python_version).equals("3.13") + env.expect.that_collection(py.config.default["tool_versions"].keys()).contains_exactly([ + "3.12.4", + "3.13.0", + "3.13.1", + ]) + env.expect.that_dict(py.config.minor_mapping).contains_exactly({ + # The mapping is calculated automatically + "3.12": "3.12.4", + "3.13": "3.13.1", + }) + env.expect.that_collection(py.toolchains).contains_exactly([ + struct( + name = name, + python_version = version, + register_coverage_tool = False, + ) + for name, version in { + # NOTE: that '3.11' wont be actually registered and present in the + # `tool_versions` above. + "python_3_11": "3.11", + "python_3_12": "3.12", + "python_3_13": "3.13", + }.items() + ]) + +_tests.append(_test_ignore_unsupported_versions) + +def _test_add_patches(env): + py = parse_modules( + module_ctx = python_ext.mctx( + python_ext.module( + name = "my_module", + is_root = True, override = [ - _override( - base_url = "", + python_ext.override( available_python_versions = ["3.13.0"], + base_urls = [], minor_mapping = { "3.13": "3.13.0", }, ), ], + single_version_override = [ + python_ext.single_version_override( + distutils = None, + distutils_content = "", + patch_strip = 1, + patches = ["common.txt"], + python_version = "3.13.0", + sha256 = { + "aarch64-apple-darwin": "deadbeef", + "aarch64-unknown-linux-gnu": "deadbeef", + }, + strip_prefix = "prefix", + urls = ["example.org"], + ), + ], + single_version_platform_override = [ + python_ext.single_version_platform_override( + coverage_tool = "specific_cov_tool", + patch_strip = 2, + patches = ["specific-patch.txt"], + platform = "aarch64-unknown-linux-gnu", + python_version = "3.13.0", + sha256 = "deadb00f", + strip_prefix = "python", + urls = ["something.org", "else.org"], + ), + ], + toolchain = [python_ext.toolchain(python_version = "3.13")], ), ), logger = repo_utils.logger(verbosity_level = 0, name = "python"), @@ -741,14 +761,15 @@ _tests.append(_test_add_patches) def _test_fail_two_overrides(env): errors = [] parse_modules( - module_ctx = _mock_mctx( - _mod( + module_ctx = python_ext.mctx( + python_ext.module( name = "my_module", - toolchain = [_toolchain("3.13")], + is_root = True, override = [ - _override(base_url = "foo"), - _override(base_url = "bar"), + python_ext.override(base_urls = ["foo"]), + python_ext.override(base_urls = ["bar"]), ], + toolchain = [python_ext.toolchain(python_version = "3.13")], ), ), _fail = errors.append, @@ -764,19 +785,26 @@ def _test_single_version_override_errors(env): for test in [ struct( overrides = [ - _single_version_override(python_version = "3.12.4", distutils_content = "foo"), - _single_version_override(python_version = "3.12.4", distutils_content = "foo"), + python_ext.single_version_override( + distutils_content = "foo", + python_version = "3.12.4", + ), + python_ext.single_version_override( + distutils_content = "foo", + python_version = "3.12.4", + ), ], want_error = "Only a single 'python.single_version_override' can be present for '3.12.4'", ), ]: errors = [] parse_modules( - module_ctx = _mock_mctx( - _mod( + module_ctx = python_ext.mctx( + python_ext.module( name = "my_module", - toolchain = [_toolchain("3.13")], + is_root = True, single_version_override = test.overrides, + toolchain = [python_ext.toolchain(python_version = "3.13")], ), ), _fail = errors.append, @@ -790,31 +818,46 @@ def _test_single_version_platform_override_errors(env): for test in [ struct( overrides = [ - _single_version_platform_override(python_version = "3.12.4", platform = "foo", coverage_tool = "foo"), - _single_version_platform_override(python_version = "3.12.4", platform = "foo", coverage_tool = "foo"), + python_ext.single_version_platform_override( + coverage_tool = "foo", + platform = "foo", + python_version = "3.12.4", + ), + python_ext.single_version_platform_override( + coverage_tool = "foo", + platform = "foo", + python_version = "3.12.4", + ), ], want_error = "Only a single 'python.single_version_platform_override' can be present for '(\"3.12.4\", \"foo\")'", ), struct( overrides = [ - _single_version_platform_override(python_version = "3.12", platform = "foo"), + python_ext.single_version_platform_override( + platform = "foo", + python_version = "3.12", + ), ], want_error = "The 'python_version' attribute needs to specify the full version in at least 'X.Y.Z' format, got: '3.12'", ), struct( overrides = [ - _single_version_platform_override(python_version = "foo", platform = "foo"), + python_ext.single_version_platform_override( + platform = "foo", + python_version = "foo", + ), ], want_error = "Failed to parse PEP 440 version identifier 'foo'. Parse error at 'foo'", ), ]: errors = [] parse_modules( - module_ctx = _mock_mctx( - _mod( + module_ctx = python_ext.mctx( + python_ext.module( name = "my_module", - toolchain = [_toolchain("3.13")], + is_root = True, single_version_platform_override = test.overrides, + toolchain = [python_ext.toolchain(python_version = "3.13")], ), ), _fail = lambda *a: errors.append(" ".join(a)), @@ -835,3 +878,17 @@ def python_test_suite(name): name: the name of the test suite """ test_suite(name = name, basic_tests = _tests) + +def register_python_tests(name): + """Registers the python tests if Bzlmod is enabled, otherwise defines an empty test_suite. + + Args: + name: The name of the test target. + """ + if BZLMOD_ENABLED: + python_test_suite(name = name) + else: + native.test_suite( + name = name, + tests = [], + ) diff --git a/tests/python_bzlmod_ext/BUILD.bazel b/tests/python_bzlmod_ext/BUILD.bazel new file mode 100644 index 0000000000..e266c01b9c --- /dev/null +++ b/tests/python_bzlmod_ext/BUILD.bazel @@ -0,0 +1,7 @@ +load(":test_helpers.bzl", "register_python_bzlmod_ext_tests") + +register_python_bzlmod_ext_tests( + name = "python_bzlmod_ext_tests", + parse_runtime_manifest_name = "parse_runtime_manifest_tests", + runtime_manifests_name = "runtime_manifests_tests", +) diff --git a/tests/python_bzlmod_ext/parse_runtime_manifest_tests.bzl b/tests/python_bzlmod_ext/parse_runtime_manifest_tests.bzl new file mode 100644 index 0000000000..4493576147 --- /dev/null +++ b/tests/python_bzlmod_ext/parse_runtime_manifest_tests.bzl @@ -0,0 +1,183 @@ +"""Tests for manifest parsing Starlark functions.""" + +load("@bazel_skylib//lib:structs.bzl", "structs") +load("@rules_testing//lib:analysis_test.bzl", "analysis_test") +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("@rules_testing//lib:util.bzl", rt_util = "util") +load("//python/private:pbs_manifest.bzl", "parse_filename", "parse_runtime_manifest") # buildifier: disable=bzl-visibility + +_tests = [] + +def _test_parse_filename_baseline(name): + """Sets up the baseline filename parsing test. + + Args: + name: The name of the test. + """ + rt_util.helper_target( + native.filegroup, + name = name + "_subject", + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_parse_filename_baseline_impl, + ) + +def _test_parse_filename_baseline_impl(env, target): + _ = target # @unused + + # 1. Baseline + parsed1 = parse_filename("cpython-3.11.15+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz") + env.expect.that_dict(parsed1).contains_exactly({ + "arch": "x86_64", + "archive_flavor": "install_only", + "build_flavor": "", + "build_version": "20260414", + "freethreaded": False, + "libc": "gnu", + "location": "cpython-3.11.15+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz", + "microarch": "", + "os": "linux", + "python_version": "3.11.15", + "vendor": "unknown", + }) + + # 2. Microarch + parsed2 = parse_filename("cpython-3.10.20+20260414-x86_64_v2-unknown-linux-musl-lto-full.tar.zst") + env.expect.that_dict(parsed2).contains_exactly({ + "arch": "x86_64", + "archive_flavor": "full", + "build_flavor": "lto", + "build_version": "20260414", + "freethreaded": False, + "libc": "musl", + "location": "cpython-3.10.20+20260414-x86_64_v2-unknown-linux-musl-lto-full.tar.zst", + "microarch": "v2", + "os": "linux", + "python_version": "3.10.20", + "vendor": "unknown", + }) + + # 3. Freethreaded + parsed3 = parse_filename("cpython-3.13.13+20260414-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst") + env.expect.that_dict(parsed3).contains_exactly({ + "arch": "aarch64", + "archive_flavor": "full", + "build_flavor": "pgo+lto", + "build_version": "20260414", + "freethreaded": True, + "libc": "", + "location": "cpython-3.13.13+20260414-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst", + "microarch": "", + "os": "darwin", + "python_version": "3.13.13", + "vendor": "apple", + }) + + # 4. Invalid + parsed4 = parse_filename("invalid-filename.tar.gz") + env.expect.that_bool(parsed4 == None).equals(True) + + # 5. Full URL (should return the original URL as location) + parsed5 = parse_filename("https://github.com/astral-sh/python-build-standalone/releases/download/20260414/cpython-3.11.15+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz") + env.expect.that_dict(parsed5).contains_exactly({ + "arch": "x86_64", + "archive_flavor": "install_only", + "build_flavor": "", + "build_version": "20260414", + "freethreaded": False, + "libc": "gnu", + "location": "https://github.com/astral-sh/python-build-standalone/releases/download/20260414/cpython-3.11.15+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz", + "microarch": "", + "os": "linux", + "python_version": "3.11.15", + "vendor": "unknown", + }) + +_tests.append(_test_parse_filename_baseline) + +def _test_parse_runtime_manifest(name): + """Sets up the manifest file parsing test. + + Args: + name: The name of the test. + """ + rt_util.helper_target( + native.filegroup, + name = name + "_subject", + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_parse_runtime_manifest_impl, + ) + +def _test_parse_runtime_manifest_impl(env, target): + _ = target # @unused + content = """ +8b14030dd3af9ea7f7c51b4c90feb04afd8a8f45435727e67b875270bd08f3bc cpython-3.11.15+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +a57ffd435652092d16b30e783f9826c55e9c64b0f0a72cbae0a9f39e663137fb cpython-3.11.15+20260414-aarch64-apple-darwin-install_only.tar.gz +ce18fdfd47c66830a40ea9b9e314a14b1636bbfd684501bc5ca1fc6d55a7933f https://example.com/cpython-3.10.20+20260414-x86_64_v2-unknown-linux-musl-lto-full.tar.zst +1111111111111111111111111111111111111111111111111111111111111111 cpython-3.13.13+20260414-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +""" + parsed = parse_runtime_manifest(content) + env.expect.that_collection(parsed).has_size(4) + + env.expect.that_dict(structs.to_dict(parsed[0])).contains_exactly({ + "arch": "x86_64", + "archive_flavor": "install_only", + "build_flavor": "", + "build_version": "20260414", + "freethreaded": False, + "libc": "gnu", + "location": "cpython-3.11.15+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz", + "microarch": "", + "os": "linux", + "python_version": "3.11.15", + "sha256": "8b14030dd3af9ea7f7c51b4c90feb04afd8a8f45435727e67b875270bd08f3bc", + "vendor": "unknown", + }) + + env.expect.that_dict(structs.to_dict(parsed[2])).contains_exactly({ + "arch": "x86_64", + "archive_flavor": "full", + "build_flavor": "lto", + "build_version": "20260414", + "freethreaded": False, + "libc": "musl", + "location": "https://example.com/cpython-3.10.20+20260414-x86_64_v2-unknown-linux-musl-lto-full.tar.zst", + "microarch": "v2", + "os": "linux", + "python_version": "3.10.20", + "sha256": "ce18fdfd47c66830a40ea9b9e314a14b1636bbfd684501bc5ca1fc6d55a7933f", + "vendor": "unknown", + }) + + env.expect.that_dict(structs.to_dict(parsed[3])).contains_exactly({ + "arch": "aarch64", + "archive_flavor": "full", + "build_flavor": "pgo+lto", + "build_version": "20260414", + "freethreaded": True, + "libc": "", + "location": "cpython-3.13.13+20260414-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst", + "microarch": "", + "os": "darwin", + "python_version": "3.13.13", + "sha256": "1111111111111111111111111111111111111111111111111111111111111111", + "vendor": "apple", + }) + +_tests.append(_test_parse_runtime_manifest) + +def parse_runtime_manifest_test_suite(name): + """Defines the test suite for manifest parsing. + + Args: + name: The name of the test suite. + """ + test_suite( + name = name, + tests = _tests, + ) diff --git a/tests/python_bzlmod_ext/runtime_manifests_tests.bzl b/tests/python_bzlmod_ext/runtime_manifests_tests.bzl new file mode 100644 index 0000000000..6de0ff93c2 --- /dev/null +++ b/tests/python_bzlmod_ext/runtime_manifests_tests.bzl @@ -0,0 +1,162 @@ +"""Starlark unit tests for dynamic toolchain registration via manifests.""" + +load("@rules_testing//lib:analysis_test.bzl", "analysis_test") +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("@rules_testing//lib:util.bzl", rt_util = "util") +load("//python/private:python.bzl", "parse_modules") # buildifier: disable=bzl-visibility +load("//python/private:repo_utils.bzl", "repo_utils") # buildifier: disable=bzl-visibility +load("//tests/support/mocks:python_ext.bzl", "python_ext") # buildifier: disable=bzl-visibility + +_tests = [] + +_mock_logger = repo_utils.logger( + name = "mock", + verbosity_level = "ERROR", +) + +def _test_dynamic_manifest_toolchains(name): + rt_util.helper_target( + native.filegroup, + name = name + "_subject", + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_dynamic_manifest_toolchains_impl, + ) + +def _test_dynamic_manifest_toolchains_impl(env, target): + _ = target # @unused + + # Construct Bzlmod mock module locally inside the test execution block. + # We test using virtual patch version "3.11.99" (not present in TOOL_VERSIONS) + # so that the populated config contains ONLY our dynamically parsed manifest keys + # without any pre-populated multi-platform templates, allowing exact dictionary match! + root_module = python_ext.module( + name = "runtime_manifests", + override = [ + python_ext.override( + add_runtime_manifest_urls = [ + "https://github.com/astral-sh/python-build-standalone/releases/download/20260414/SHA256SUMS", + ], + runtime_manifest_sha = "ce18fdfd47c66830a40ea9b9e314a14b1636bbfd684501bc5ca1fc6d55a7933f", + register_all_versions = True, + ), + ], + defaults = [ + python_ext.defaults( + python_version = "3.11.99", + ), + ], + ) + + # Pre-populate mock_files directly to bypass download output struct key mismatch in mock read lookups. + mock_mctx = python_ext.mctx( + modules = [root_module], + mock_files = { + "runtime_manifest": """ +01e607cf764b97d4d5d6f69fd1ff3d8a9a162513dde5c39e98260fce40fe220a cpython-3.11.99+20260414-x86_64-unknown-linux-gnu-pgo+lto-full.tar.zst +8b14030dd3af9ea7f7c51b4c90feb04afd8a8f45435727e67b875270bd08f3bc cpython-3.11.99+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +""", + }, + ) + + res = parse_modules( + module_ctx = mock_mctx, + logger = _mock_logger, + ) + + tool_versions = res.config.default["tool_versions"] + env.expect.that_bool("3.11.99" in tool_versions).equals(True) + + version_info = tool_versions["3.11.99"] + + # Assert on the entire dictionary at once! + env.expect.that_dict(version_info).contains_exactly({ + "sha256": { + "x86_64-unknown-linux-gnu": "8b14030dd3af9ea7f7c51b4c90feb04afd8a8f45435727e67b875270bd08f3bc", + }, + "strip_prefix": { + "x86_64-unknown-linux-gnu": "python", + }, + "url": { + "x86_64-unknown-linux-gnu": [ + "https://github.com/astral-sh/python-build-standalone/releases/download/20260414/cpython-3.11.99+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz", + ], + }, + }) + +_tests.append(_test_dynamic_manifest_toolchains) + +def _test_dynamic_manifest_files(name): + rt_util.helper_target( + native.filegroup, + name = name + "_subject", + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_dynamic_manifest_files_impl, + ) + +def _test_dynamic_manifest_files_impl(env, target): + _ = target # @unused + + root_module = python_ext.module( + name = "runtime_manifests", + override = [ + python_ext.override( + add_runtime_manifest_files = [ + Label("//:SHA256SUMS"), + ], + base_urls = ["https://example.com/dl"], + register_all_versions = True, + ), + ], + defaults = [ + python_ext.defaults( + python_version = "3.12.99", + ), + ], + ) + + mock_mctx = python_ext.mctx( + modules = [root_module], + mock_files = { + str(Label("//:SHA256SUMS")): """ +01e607cf764b97d4d5d6f69fd1ff3d8a9a162513dde5c39e98260fce40fe220a cpython-3.12.99+20260414-x86_64-unknown-linux-gnu-pgo+lto-full.tar.zst +""", + }, + ) + + res = parse_modules( + module_ctx = mock_mctx, + logger = _mock_logger, + ) + + tool_versions = res.config.default["tool_versions"] + env.expect.that_bool("3.12.99" in tool_versions).equals(True) + + version_info = tool_versions["3.12.99"] + + env.expect.that_dict(version_info).contains_exactly({ + "sha256": { + "x86_64-unknown-linux-gnu": "01e607cf764b97d4d5d6f69fd1ff3d8a9a162513dde5c39e98260fce40fe220a", + }, + "strip_prefix": { + "x86_64-unknown-linux-gnu": "python/install", + }, + "url": { + "x86_64-unknown-linux-gnu": [ + "https://example.com/dl/cpython-3.12.99+20260414-x86_64-unknown-linux-gnu-pgo+lto-full.tar.zst", + ], + }, + }) + +_tests.append(_test_dynamic_manifest_files) + +def runtime_manifests_test_suite(name): + test_suite( + name = name, + tests = _tests, + ) diff --git a/tests/python_bzlmod_ext/test_helpers.bzl b/tests/python_bzlmod_ext/test_helpers.bzl new file mode 100644 index 0000000000..78ad57c110 --- /dev/null +++ b/tests/python_bzlmod_ext/test_helpers.bzl @@ -0,0 +1,48 @@ +# Copyright 2026 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. + +"""Helpers to conditionally register tests depending on Bzlmod enablement.""" + +load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility +load(":parse_runtime_manifest_tests.bzl", "parse_runtime_manifest_test_suite") +load(":runtime_manifests_tests.bzl", "runtime_manifests_test_suite") + +def register_python_bzlmod_ext_tests(name, parse_runtime_manifest_name, runtime_manifests_name): + """Registers the Bzlmod extension tests if Bzlmod is enabled, otherwise defines empty test_suites. + + Args: + name: The name of the master test_suite target. + parse_runtime_manifest_name: The name of the parse_runtime_manifest test target. + runtime_manifests_name: The name of the runtime_manifests test target. + """ + if BZLMOD_ENABLED: + parse_runtime_manifest_test_suite(name = parse_runtime_manifest_name) + runtime_manifests_test_suite(name = runtime_manifests_name) + else: + native.test_suite( + name = parse_runtime_manifest_name, + tests = [], + ) + native.test_suite( + name = runtime_manifests_name, + tests = [], + ) + + native.test_suite( + name = name, + tests = [ + parse_runtime_manifest_name, + runtime_manifests_name, + ], + ) diff --git a/tests/repl/repl_test.py b/tests/repl/repl_test.py index 01d0442922..2b3d5c7a4d 100644 --- a/tests/repl/repl_test.py +++ b/tests/repl/repl_test.py @@ -1,6 +1,6 @@ import os import subprocess -import sys +import sys # noqa: F401 import tempfile import unittest from pathlib import Path @@ -21,24 +21,58 @@ foo = 1234 """ +IS_WINDOWS = os.name == "nt" + class ReplTest(unittest.TestCase): def setUp(self): - self.repl = rfiles.Rlocation("rules_python/python/bin/repl") + rpath = "rules_python/python/bin/repl" + if IS_WINDOWS: + rpath += ".exe" + self.repl = rfiles.Rlocation(rpath) assert self.repl + if IS_WINDOWS: + self.repl = os.path.normpath(self.repl) def run_code_in_repl(self, lines: Iterable[str], *, env=None) -> str: """Runs the lines of code in the REPL and returns the text output.""" + input = "\n".join(lines) try: return subprocess.check_output( [self.repl], text=True, stderr=subprocess.STDOUT, - input="\n".join(lines), + input=input, env=env, ).strip() except subprocess.CalledProcessError as error: raise RuntimeError(f"Failed to run the REPL:\n{error.stdout}") from error + except Exception as exc: + if env: + env_str = "\n".join( + f"{key}={value!r}" for key, value in sorted(env.items()) + ) + else: + env_str = "" + if isinstance(exc, subprocess.CalledProcessError): + stdout = exc.stdout + else: + stdout = "" + exc.add_note( + f""" +===== env start ===== +{env_str} +===== env end ===== +===== input start ===== +{input} +===== input end ===== +commmand: {self.repl} +===== stdout start ===== +{stdout} +===== stdout end ===== +""" + ) + raise def test_repl_version(self): """Validates that we can successfully execute arbitrary code on the REPL.""" @@ -55,7 +89,7 @@ def test_repl_version(self): def test_cannot_import_test_module_directly(self): """Validates that we cannot import helper/test_module.py since it's not a direct dep.""" with self.assertRaises(ModuleNotFoundError): - import test_module + import test_module # noqa: F401 @unittest.skipIf( not EXPECT_TEST_MODULE_IMPORTABLE, "test only works without repl_dep set" diff --git a/tests/repo_utils/BUILD.bazel b/tests/repo_utils/BUILD.bazel new file mode 100644 index 0000000000..74e8e37489 --- /dev/null +++ b/tests/repo_utils/BUILD.bazel @@ -0,0 +1,3 @@ +load(":repo_utils_test.bzl", "repo_utils_test_suite") + +repo_utils_test_suite(name = "repo_utils_tests") diff --git a/tests/repo_utils/repo_utils_test.bzl b/tests/repo_utils/repo_utils_test.bzl new file mode 100644 index 0000000000..56d125724d --- /dev/null +++ b/tests/repo_utils/repo_utils_test.bzl @@ -0,0 +1,132 @@ +"""Unit tests for repo_utils.bzl.""" + +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("//python/private:repo_utils.bzl", "repo_utils") # buildifier: disable=bzl-visibility +load("//tests/support/mocks:mocks.bzl", "mocks") + +_tests = [] + +def _make_rctx(*, os_name, mock_extracts = None, mock_files = None): + return mocks.rctx( + attr = { + "name": "unit", + "_rule_name": "test_rule", + }, + mock_files = mock_files, + mock_extracts = mock_extracts, + os_name = os_name, + ) + +def _test_get_platforms_os_name(env): + mock_mrctx = mocks.rctx(os_name = "Mac OS X") + got = repo_utils.get_platforms_os_name(mock_mrctx) + env.expect.that_str(got).equals("osx") + +_tests.append(_test_get_platforms_os_name) + +def _test_relative_to(env): + mock_mrctx_linux = mocks.rctx(os_name = "linux") + mock_mrctx_win = mocks.rctx(os_name = "windows") + + # Case-sensitive matching (Linux) + got = repo_utils.relative_to(mock_mrctx_linux, "foo/bar/baz", "foo/bar") + env.expect.that_str(got).equals("baz") + + # Case-insensitive matching (Windows) + got = repo_utils.relative_to(mock_mrctx_win, "C:/Foo/Bar/Baz", "c:/foo/bar") + env.expect.that_str(got).equals("Baz") + + # Failure case + failures = [] + + def _mock_fail(msg): + failures.append(msg) + + repo_utils.relative_to(mock_mrctx_linux, "foo/bar/baz", "qux", fail = _mock_fail) + env.expect.that_collection(failures).contains_exactly(["foo/bar/baz is not relative to qux"]) + +_tests.append(_test_relative_to) + +def _test_is_relative_to(env): + mock_mrctx_linux = mocks.rctx(os_name = "linux") + mock_mrctx_win = mocks.rctx(os_name = "windows") + + # Case-sensitive matching (Linux) + env.expect.that_bool(repo_utils.is_relative_to(mock_mrctx_linux, "foo/bar/baz", "foo/bar")).equals(True) + env.expect.that_bool(repo_utils.is_relative_to(mock_mrctx_linux, "foo/bar/baz", "qux")).equals(False) + + # Case-insensitive matching (Windows) + env.expect.that_bool(repo_utils.is_relative_to(mock_mrctx_win, "C:/Foo/Bar/Baz", "c:/foo/bar")).equals(True) + env.expect.that_bool(repo_utils.is_relative_to(mock_mrctx_win, "C:/Foo/Bar/Baz", "D:/Foo")).equals(False) + +_tests.append(_test_is_relative_to) + +def _test_extract_calls_chmod_when_enabled(env): + mock_rctx = _make_rctx( + os_name = "linux", + mock_extracts = {"test.whl": {"f1": "c1"}}, + ) + + repo_utils.extract( + mock_rctx, + archive = mock_rctx.path("test.whl"), + output = "out", + supports_whl_extraction = True, + extract_needs_chmod = True, + ) + + env.expect.that_bool(len(mock_rctx.execute_calls) > 0).equals(True) + env.expect.that_str(mock_rctx.execute_calls[0][0]).equals("chmod") + +_tests.append(_test_extract_calls_chmod_when_enabled) + +def _test_extract_skips_chmod_when_disabled(env): + mock_rctx = _make_rctx( + os_name = "linux", + mock_extracts = {"test.whl": {"f1": "c1"}}, + ) + + repo_utils.extract( + mock_rctx, + archive = mock_rctx.path("test.whl"), + output = "out", + supports_whl_extraction = True, + extract_needs_chmod = False, + ) + + env.expect.that_collection(mock_rctx.execute_calls).contains_exactly([]) + +_tests.append(_test_extract_skips_chmod_when_disabled) + +def _test_maybe_fix_permissions_calls_chmod_on_linux(env): + mock_rctx = _make_rctx(os_name = "linux") + + repo_utils.maybe_fix_permissions( + mock_rctx, + whl_path = mock_rctx.path("test.whl"), + ) + + env.expect.that_bool(len(mock_rctx.execute_calls) > 0).equals(True) + env.expect.that_str(mock_rctx.execute_calls[0][0]).equals("chmod") + +_tests.append(_test_maybe_fix_permissions_calls_chmod_on_linux) + +def _test_maybe_fix_permissions_skips_on_windows(env): + mock_rctx = _make_rctx(os_name = "windows") + + repo_utils.maybe_fix_permissions( + mock_rctx, + whl_path = mock_rctx.path("test.whl"), + ) + + env.expect.that_collection(mock_rctx.execute_calls).contains_exactly([]) + +_tests.append(_test_maybe_fix_permissions_skips_on_windows) + +def repo_utils_test_suite(name): + """Create the test suite. + + Args: + name: the name of the test suite + """ + test_suite(name = name, basic_tests = _tests) diff --git a/tests/repos/BUILD.bazel b/tests/repos/BUILD.bazel new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/repos/pkgutil_nspkg1/BUILD.bazel b/tests/repos/pkgutil_nspkg1/BUILD.bazel new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/repos/pkgutil_nspkg1/nspkg/__init__.py b/tests/repos/pkgutil_nspkg1/nspkg/__init__.py new file mode 100644 index 0000000000..c4da2cf0b7 --- /dev/null +++ b/tests/repos/pkgutil_nspkg1/nspkg/__init__.py @@ -0,0 +1,2 @@ +# __init__.py +__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/tests/repos/pkgutil_nspkg1/nspkg/one/a.txt b/tests/repos/pkgutil_nspkg1/nspkg/one/a.txt new file mode 100644 index 0000000000..f4dbe63934 --- /dev/null +++ b/tests/repos/pkgutil_nspkg1/nspkg/one/a.txt @@ -0,0 +1 @@ +dummy content \ No newline at end of file diff --git a/tests/repos/pkgutil_nspkg1/pkgutil_nspkg1-1.0.dist-info/METADATA b/tests/repos/pkgutil_nspkg1/pkgutil_nspkg1-1.0.dist-info/METADATA new file mode 100644 index 0000000000..7ffb60b701 --- /dev/null +++ b/tests/repos/pkgutil_nspkg1/pkgutil_nspkg1-1.0.dist-info/METADATA @@ -0,0 +1,3 @@ +Metadata-Version: 2.1 +Name: pkgutil-nspkg1 +Version: 1.0 diff --git a/tests/repos/pkgutil_nspkg1/pkgutil_nspkg1-1.0.dist-info/RECORD b/tests/repos/pkgutil_nspkg1/pkgutil_nspkg1-1.0.dist-info/RECORD new file mode 100644 index 0000000000..e039fee1ae --- /dev/null +++ b/tests/repos/pkgutil_nspkg1/pkgutil_nspkg1-1.0.dist-info/RECORD @@ -0,0 +1,5 @@ +nspkg/__init__.py,sha256=d10f14d9ce938ae14416c3c1f4b516c4f40dfe0c9bf973a833e4ef40517ba7d0,81 +nspkg/one/a.txt,sha256=bf0ecbdb9b814248d086c9b69cf26182d9d4138f2ad3d0637c4555fc8cbf68e5,13 +pkgutil_nspkg1-1.0.dist-info/METADATA,sha256=49525c3e6f1fc8f46d9c92c996e35f81327f9fe417df2d9d10784fa4e9cfe84b,59 +pkgutil_nspkg1-1.0.dist-info/WHEEL,sha256=d652ec50af6f144788dc1ffef052e8833b6704e98818e6cf78d80a625ad498fb,105 +pkgutil_nspkg1-1.0.dist-info/RECORD,, diff --git a/tests/repos/pkgutil_nspkg1/pkgutil_nspkg1-1.0.dist-info/WHEEL b/tests/repos/pkgutil_nspkg1/pkgutil_nspkg1-1.0.dist-info/WHEEL new file mode 100644 index 0000000000..dad2b72544 --- /dev/null +++ b/tests/repos/pkgutil_nspkg1/pkgutil_nspkg1-1.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: rules_python_whl_from_dir_repo +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/tests/repos/pkgutil_nspkg2/BUILD.bazel b/tests/repos/pkgutil_nspkg2/BUILD.bazel new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/repos/pkgutil_nspkg2/nspkg/__init__.py b/tests/repos/pkgutil_nspkg2/nspkg/__init__.py new file mode 100644 index 0000000000..c4da2cf0b7 --- /dev/null +++ b/tests/repos/pkgutil_nspkg2/nspkg/__init__.py @@ -0,0 +1,2 @@ +# __init__.py +__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/tests/repos/pkgutil_nspkg2/nspkg/two/b.txt b/tests/repos/pkgutil_nspkg2/nspkg/two/b.txt new file mode 100644 index 0000000000..f4dbe63934 --- /dev/null +++ b/tests/repos/pkgutil_nspkg2/nspkg/two/b.txt @@ -0,0 +1 @@ +dummy content \ No newline at end of file diff --git a/tests/repos/pkgutil_nspkg2/pkgutil_nspkg2-1.0.dist-info/METADATA b/tests/repos/pkgutil_nspkg2/pkgutil_nspkg2-1.0.dist-info/METADATA new file mode 100644 index 0000000000..368e64ce19 --- /dev/null +++ b/tests/repos/pkgutil_nspkg2/pkgutil_nspkg2-1.0.dist-info/METADATA @@ -0,0 +1,3 @@ +Metadata-Version: 2.1 +Name: pkgutil-nspkg2 +Version: 1.0 diff --git a/tests/repos/pkgutil_nspkg2/pkgutil_nspkg2-1.0.dist-info/RECORD b/tests/repos/pkgutil_nspkg2/pkgutil_nspkg2-1.0.dist-info/RECORD new file mode 100644 index 0000000000..c93970cb26 --- /dev/null +++ b/tests/repos/pkgutil_nspkg2/pkgutil_nspkg2-1.0.dist-info/RECORD @@ -0,0 +1,5 @@ +nspkg/__init__.py,sha256=d10f14d9ce938ae14416c3c1f4b516c4f40dfe0c9bf973a833e4ef40517ba7d0,81 +nspkg/two/b.txt,sha256=bf0ecbdb9b814248d086c9b69cf26182d9d4138f2ad3d0637c4555fc8cbf68e5,13 +pkgutil_nspkg2-1.0.dist-info/METADATA,sha256=9a72654b480f17c55df07fe27d026c4ea461c493babcc675f26fbcfce40828b6,59 +pkgutil_nspkg2-1.0.dist-info/WHEEL,sha256=d652ec50af6f144788dc1ffef052e8833b6704e98818e6cf78d80a625ad498fb,105 +pkgutil_nspkg2-1.0.dist-info/RECORD,, diff --git a/tests/repos/pkgutil_nspkg2/pkgutil_nspkg2-1.0.dist-info/WHEEL b/tests/repos/pkgutil_nspkg2/pkgutil_nspkg2-1.0.dist-info/WHEEL new file mode 100644 index 0000000000..dad2b72544 --- /dev/null +++ b/tests/repos/pkgutil_nspkg2/pkgutil_nspkg2-1.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: rules_python_whl_from_dir_repo +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/tests/repos/whl_with_data1/BUILD.bazel b/tests/repos/whl_with_data1/BUILD.bazel new file mode 100644 index 0000000000..7ef8ba4cd9 --- /dev/null +++ b/tests/repos/whl_with_data1/BUILD.bazel @@ -0,0 +1 @@ +exports_files(glob(["**"])) diff --git a/tests/repos/whl_with_data1/whl_with_data1-1.0.data/data/bin/data_overlap.sh b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/data/bin/data_overlap.sh new file mode 100644 index 0000000000..b47ce4e9f3 --- /dev/null +++ b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/data/bin/data_overlap.sh @@ -0,0 +1 @@ +echo data_bin diff --git a/tests/repos/whl_with_data1/whl_with_data1-1.0.data/data/include/data_overlap.h b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/data/include/data_overlap.h new file mode 100644 index 0000000000..299c39d0a7 --- /dev/null +++ b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/data/include/data_overlap.h @@ -0,0 +1 @@ +/* data_include */ diff --git a/tests/repos/whl_with_data1/whl_with_data1-1.0.data/data/overlap/both.txt b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/data/overlap/both.txt new file mode 100644 index 0000000000..771c76ed7b --- /dev/null +++ b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/data/overlap/both.txt @@ -0,0 +1 @@ +both1 diff --git a/tests/repos/whl_with_data1/whl_with_data1-1.0.data/data/overlap/data1.txt b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/data/overlap/data1.txt new file mode 100644 index 0000000000..d760283f59 --- /dev/null +++ b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/data/overlap/data1.txt @@ -0,0 +1 @@ +data1 diff --git a/tests/repos/whl_with_data1/whl_with_data1-1.0.data/data/site-packages/data_overlap.py b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/data/site-packages/data_overlap.py new file mode 100644 index 0000000000..d3ee4d8a3f --- /dev/null +++ b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/data/site-packages/data_overlap.py @@ -0,0 +1 @@ +# data_site_packages diff --git a/tests/repos/whl_with_data1/whl_with_data1-1.0.data/data/whl_with_data1/data_data_file.txt b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/data/whl_with_data1/data_data_file.txt new file mode 100644 index 0000000000..39ec676600 --- /dev/null +++ b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/data/whl_with_data1/data_data_file.txt @@ -0,0 +1 @@ +from .data/data diff --git a/tests/repos/whl_with_data1/whl_with_data1-1.0.data/headers/data_overlap.h b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/headers/data_overlap.h new file mode 100644 index 0000000000..ffd49d0cee --- /dev/null +++ b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/headers/data_overlap.h @@ -0,0 +1 @@ +/* headers */ diff --git a/tests/repos/whl_with_data1/whl_with_data1-1.0.data/headers/overlap/both.h b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/headers/overlap/both.h new file mode 100644 index 0000000000..49f33a8c6e --- /dev/null +++ b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/headers/overlap/both.h @@ -0,0 +1 @@ +both diff --git a/tests/repos/whl_with_data1/whl_with_data1-1.0.data/headers/overlap/header1.h b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/headers/overlap/header1.h new file mode 100644 index 0000000000..412e9ed7df --- /dev/null +++ b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/headers/overlap/header1.h @@ -0,0 +1 @@ +header1 diff --git a/tests/repos/whl_with_data1/whl_with_data1-1.0.data/headers/whl_with_data1/header_file.h b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/headers/whl_with_data1/header_file.h new file mode 100644 index 0000000000..59c9bf78c2 --- /dev/null +++ b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/headers/whl_with_data1/header_file.h @@ -0,0 +1 @@ +from .data/headers diff --git a/tests/repos/whl_with_data1/whl_with_data1-1.0.data/platlib/whl_with_data1/platlib_file.txt b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/platlib/whl_with_data1/platlib_file.txt new file mode 100644 index 0000000000..b27295614f --- /dev/null +++ b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/platlib/whl_with_data1/platlib_file.txt @@ -0,0 +1 @@ +from .data/platlib diff --git a/tests/repos/whl_with_data1/whl_with_data1-1.0.data/purelib/data_overlap.py b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/purelib/data_overlap.py new file mode 100644 index 0000000000..f82e46670f --- /dev/null +++ b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/purelib/data_overlap.py @@ -0,0 +1 @@ +# purelib diff --git a/tests/repos/whl_with_data1/whl_with_data1-1.0.data/purelib/whl_with_data1/__init__.py b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/purelib/whl_with_data1/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/repos/whl_with_data1/whl_with_data1-1.0.data/purelib/whl_with_data1/data_file.txt b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/purelib/whl_with_data1/data_file.txt new file mode 100644 index 0000000000..e547fe48ed --- /dev/null +++ b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/purelib/whl_with_data1/data_file.txt @@ -0,0 +1 @@ +from .data diff --git a/tests/repos/whl_with_data1/whl_with_data1-1.0.data/scripts/data_overlap.sh b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/scripts/data_overlap.sh new file mode 100644 index 0000000000..d6eb28dc3d --- /dev/null +++ b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/scripts/data_overlap.sh @@ -0,0 +1 @@ +echo scripts diff --git a/tests/repos/whl_with_data1/whl_with_data1-1.0.data/scripts/overlap/both.sh b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/scripts/overlap/both.sh new file mode 100644 index 0000000000..49f33a8c6e --- /dev/null +++ b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/scripts/overlap/both.sh @@ -0,0 +1 @@ +both diff --git a/tests/repos/whl_with_data1/whl_with_data1-1.0.data/scripts/overlap/script1.sh b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/scripts/overlap/script1.sh new file mode 100644 index 0000000000..4d68a2e3e0 --- /dev/null +++ b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/scripts/overlap/script1.sh @@ -0,0 +1 @@ +script1 diff --git a/tests/repos/whl_with_data1/whl_with_data1-1.0.data/scripts/whl_script.sh b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/scripts/whl_script.sh new file mode 100644 index 0000000000..1a2485251c --- /dev/null +++ b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/scripts/whl_script.sh @@ -0,0 +1 @@ +#!/bin/sh diff --git a/tests/repos/whl_with_data1/whl_with_data1-1.0.data/scripts/whl_with_data1_pythonw b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/scripts/whl_with_data1_pythonw new file mode 100755 index 0000000000..6c7b3434c5 --- /dev/null +++ b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/scripts/whl_with_data1_pythonw @@ -0,0 +1,9 @@ +#!pythonw +import sys + +# On Windows, pythonw doesn't have stdout/stderr streams, +# so output has to be written to a file. +with open(sys.argv[1], "w") as fp: + fp.write("hello from whl_with_data1_pythonw\n") + fp.write(sys.executable) + fp.write("\n") diff --git a/tests/repos/whl_with_data1/whl_with_data1-1.0.data/scripts/whl_with_data1_script b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/scripts/whl_with_data1_script new file mode 100755 index 0000000000..8af40a9a55 --- /dev/null +++ b/tests/repos/whl_with_data1/whl_with_data1-1.0.data/scripts/whl_with_data1_script @@ -0,0 +1,5 @@ +#!python +import sys + +print("hello from whl_with_data1_script") +print(sys.executable) diff --git a/tests/repos/whl_with_data1/whl_with_data1-1.0.dist-info/METADATA b/tests/repos/whl_with_data1/whl_with_data1-1.0.dist-info/METADATA new file mode 100644 index 0000000000..f403970d7a --- /dev/null +++ b/tests/repos/whl_with_data1/whl_with_data1-1.0.dist-info/METADATA @@ -0,0 +1,3 @@ +Metadata-Version: 2.1 +Name: whl-with-data1 +Version: 1.0 diff --git a/tests/repos/whl_with_data1/whl_with_data1-1.0.dist-info/RECORD b/tests/repos/whl_with_data1/whl_with_data1-1.0.dist-info/RECORD new file mode 100644 index 0000000000..10307c76a0 --- /dev/null +++ b/tests/repos/whl_with_data1/whl_with_data1-1.0.dist-info/RECORD @@ -0,0 +1,19 @@ +whl_with_data1-1.0.data/platlib/whl_with_data1/platlib_file.txt,sha256=123,123 +whl_with_data1-1.0.data/scripts/whl_with_data1_script,sha256=123,123 +whl_with_data1-1.0.data/scripts/whl_script.sh,sha256=123,123 +whl_with_data1-1.0.data/headers/whl_with_data1/header_file.h,sha256=123,123 +whl_with_data1-1.0.data/purelib/whl_with_data1/data_file.txt,sha256=123,123 +whl_with_data1-1.0.data/data/whl_with_data1/data_data_file.txt,sha256=123,123 +whl_with_data1-1.0.data/data/whl_with_data1/data_data_file.txt,sha256=123,123 +whl_with_data1-1.0.data/data/overlap/both.txt,sha256=123,123 +whl_with_data1-1.0.data/data/overlap/data1.txt,sha256=123,123 +whl_with_data1-1.0.data/scripts/overlap/both.sh,sha256=123,123 +whl_with_data1-1.0.data/scripts/overlap/script1.sh,sha256=123,123 +whl_with_data1-1.0.data/headers/overlap/both.h,sha256=123,123 +whl_with_data1-1.0.data/headers/overlap/header1.h,sha256=123,123 +whl_with_data1-1.0.data/scripts/data_overlap.sh,sha256=123,123 +whl_with_data1-1.0.data/data/bin/data_overlap.sh,sha256=123,123 +whl_with_data1-1.0.data/headers/data_overlap.h,sha256=123,123 +whl_with_data1-1.0.data/data/include/data_overlap.h,sha256=123,123 +whl_with_data1-1.0.data/purelib/data_overlap.py,sha256=123,123 +whl_with_data1-1.0.data/data/site-packages/data_overlap.py,sha256=123,123 diff --git a/tests/repos/whl_with_data1/whl_with_data1-1.0.dist-info/WHEEL b/tests/repos/whl_with_data1/whl_with_data1-1.0.dist-info/WHEEL new file mode 100644 index 0000000000..a64521a1cc --- /dev/null +++ b/tests/repos/whl_with_data1/whl_with_data1-1.0.dist-info/WHEEL @@ -0,0 +1 @@ +Wheel-Version: 1.0 diff --git a/tests/repos/whl_with_data2/BUILD.bazel b/tests/repos/whl_with_data2/BUILD.bazel new file mode 100644 index 0000000000..af49d1ebbf --- /dev/null +++ b/tests/repos/whl_with_data2/BUILD.bazel @@ -0,0 +1 @@ +exports_files(glob(["*"])) diff --git a/tests/repos/whl_with_data2/whl_with_data2-1.0.data/data/overlap/both.txt b/tests/repos/whl_with_data2/whl_with_data2-1.0.data/data/overlap/both.txt new file mode 100644 index 0000000000..1a8aa8b533 --- /dev/null +++ b/tests/repos/whl_with_data2/whl_with_data2-1.0.data/data/overlap/both.txt @@ -0,0 +1 @@ +both2 diff --git a/tests/repos/whl_with_data2/whl_with_data2-1.0.data/data/overlap/data2.txt b/tests/repos/whl_with_data2/whl_with_data2-1.0.data/data/overlap/data2.txt new file mode 100644 index 0000000000..98d81a2ec6 --- /dev/null +++ b/tests/repos/whl_with_data2/whl_with_data2-1.0.data/data/overlap/data2.txt @@ -0,0 +1 @@ +data2 diff --git a/tests/repos/whl_with_data2/whl_with_data2-1.0.data/data/whl_with_data2/data_data_file.txt b/tests/repos/whl_with_data2/whl_with_data2-1.0.data/data/whl_with_data2/data_data_file.txt new file mode 100644 index 0000000000..39ec676600 --- /dev/null +++ b/tests/repos/whl_with_data2/whl_with_data2-1.0.data/data/whl_with_data2/data_data_file.txt @@ -0,0 +1 @@ +from .data/data diff --git a/tests/repos/whl_with_data2/whl_with_data2-1.0.data/headers/overlap/both.h b/tests/repos/whl_with_data2/whl_with_data2-1.0.data/headers/overlap/both.h new file mode 100644 index 0000000000..49f33a8c6e --- /dev/null +++ b/tests/repos/whl_with_data2/whl_with_data2-1.0.data/headers/overlap/both.h @@ -0,0 +1 @@ +both diff --git a/tests/repos/whl_with_data2/whl_with_data2-1.0.data/headers/overlap/header2.h b/tests/repos/whl_with_data2/whl_with_data2-1.0.data/headers/overlap/header2.h new file mode 100644 index 0000000000..da0a719745 --- /dev/null +++ b/tests/repos/whl_with_data2/whl_with_data2-1.0.data/headers/overlap/header2.h @@ -0,0 +1 @@ +header2 diff --git a/tests/repos/whl_with_data2/whl_with_data2-1.0.data/headers/whl_with_data2/header_file.h b/tests/repos/whl_with_data2/whl_with_data2-1.0.data/headers/whl_with_data2/header_file.h new file mode 100644 index 0000000000..59c9bf78c2 --- /dev/null +++ b/tests/repos/whl_with_data2/whl_with_data2-1.0.data/headers/whl_with_data2/header_file.h @@ -0,0 +1 @@ +from .data/headers diff --git a/tests/repos/whl_with_data2/whl_with_data2-1.0.data/platlib/whl_with_data2/platlib_file.txt b/tests/repos/whl_with_data2/whl_with_data2-1.0.data/platlib/whl_with_data2/platlib_file.txt new file mode 100644 index 0000000000..b27295614f --- /dev/null +++ b/tests/repos/whl_with_data2/whl_with_data2-1.0.data/platlib/whl_with_data2/platlib_file.txt @@ -0,0 +1 @@ +from .data/platlib diff --git a/tests/repos/whl_with_data2/whl_with_data2-1.0.data/purelib/whl_with_data2/__init__.py b/tests/repos/whl_with_data2/whl_with_data2-1.0.data/purelib/whl_with_data2/__init__.py new file mode 100644 index 0000000000..45132c14d7 --- /dev/null +++ b/tests/repos/whl_with_data2/whl_with_data2-1.0.data/purelib/whl_with_data2/__init__.py @@ -0,0 +1,6 @@ +import sys + + +def main(): + print("hello from whl_with_data2_bin") + print(sys.executable) diff --git a/tests/repos/whl_with_data2/whl_with_data2-1.0.data/purelib/whl_with_data2/data_file.txt b/tests/repos/whl_with_data2/whl_with_data2-1.0.data/purelib/whl_with_data2/data_file.txt new file mode 100644 index 0000000000..e547fe48ed --- /dev/null +++ b/tests/repos/whl_with_data2/whl_with_data2-1.0.data/purelib/whl_with_data2/data_file.txt @@ -0,0 +1 @@ +from .data diff --git a/tests/repos/whl_with_data2/whl_with_data2-1.0.data/scripts/overlap/both.sh b/tests/repos/whl_with_data2/whl_with_data2-1.0.data/scripts/overlap/both.sh new file mode 100644 index 0000000000..49f33a8c6e --- /dev/null +++ b/tests/repos/whl_with_data2/whl_with_data2-1.0.data/scripts/overlap/both.sh @@ -0,0 +1 @@ +both diff --git a/tests/repos/whl_with_data2/whl_with_data2-1.0.data/scripts/overlap/script2.sh b/tests/repos/whl_with_data2/whl_with_data2-1.0.data/scripts/overlap/script2.sh new file mode 100644 index 0000000000..026ed8b62a --- /dev/null +++ b/tests/repos/whl_with_data2/whl_with_data2-1.0.data/scripts/overlap/script2.sh @@ -0,0 +1 @@ +script2 diff --git a/tests/repos/whl_with_data2/whl_with_data2-1.0.data/scripts/whl_script.sh b/tests/repos/whl_with_data2/whl_with_data2-1.0.data/scripts/whl_script.sh new file mode 100644 index 0000000000..1a2485251c --- /dev/null +++ b/tests/repos/whl_with_data2/whl_with_data2-1.0.data/scripts/whl_script.sh @@ -0,0 +1 @@ +#!/bin/sh diff --git a/tests/repos/whl_with_data2/whl_with_data2-1.0.dist-info/METADATA b/tests/repos/whl_with_data2/whl_with_data2-1.0.dist-info/METADATA new file mode 100644 index 0000000000..c762d184fb --- /dev/null +++ b/tests/repos/whl_with_data2/whl_with_data2-1.0.dist-info/METADATA @@ -0,0 +1,3 @@ +Metadata-Version: 2.1 +Name: whl-with-data2 +Version: 1.0 diff --git a/tests/repos/whl_with_data2/whl_with_data2-1.0.dist-info/RECORD b/tests/repos/whl_with_data2/whl_with_data2-1.0.dist-info/RECORD new file mode 100644 index 0000000000..55c70740c8 --- /dev/null +++ b/tests/repos/whl_with_data2/whl_with_data2-1.0.dist-info/RECORD @@ -0,0 +1,14 @@ +whl_with_data2-1.0.data/platlib/whl_with_data2/platlib_file.txt,sha256=123,123 +whl_with_data2-1.0.data/scripts/whl_script.sh,sha256=123,123 +whl_with_data2-1.0.data/headers/whl_with_data2/header_file.h,sha256=123,123 +whl_with_data2-1.0.data/purelib/whl_with_data2/data_file.txt,sha256=123,123 +whl_with_data2-1.0.data/data/whl_with_data2/data_data_file.txt,sha256=123,123 +whl_with_data2-1.0.data/data/whl_with_data2/data_data_file.txt,sha256=123,123 +whl_with_data2-1.0.data/data/overlap/both.txt,sha256=123,123 +whl_with_data2-1.0.data/data/overlap/data2.txt,sha256=123,123 +whl_with_data2-1.0.data/scripts/overlap/both.sh,sha256=123,123 +whl_with_data2-1.0.data/scripts/overlap/script2.sh,sha256=123,123 +whl_with_data2-1.0.data/headers/overlap/both.h,sha256=123,123 +whl_with_data2-1.0.data/headers/overlap/header2.h,sha256=123,123 +whl_with_data2-1.0.data/purelib/whl_with_data2/__init__.py,sha256=123,123 +whl_with_data2-1.0.dist-info/entry_points.txt,sha256=123,123 diff --git a/tests/repos/whl_with_data2/whl_with_data2-1.0.dist-info/WHEEL b/tests/repos/whl_with_data2/whl_with_data2-1.0.dist-info/WHEEL new file mode 100644 index 0000000000..a64521a1cc --- /dev/null +++ b/tests/repos/whl_with_data2/whl_with_data2-1.0.dist-info/WHEEL @@ -0,0 +1 @@ +Wheel-Version: 1.0 diff --git a/tests/repos/whl_with_data2/whl_with_data2-1.0.dist-info/entry_points.txt b/tests/repos/whl_with_data2/whl_with_data2-1.0.dist-info/entry_points.txt new file mode 100644 index 0000000000..8389a8a826 --- /dev/null +++ b/tests/repos/whl_with_data2/whl_with_data2-1.0.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +whl_with_data2_bin = whl_with_data2:main diff --git a/tests/runfiles/BUILD.bazel b/tests/runfiles/BUILD.bazel index 5c92026082..505b3c17c5 100644 --- a/tests/runfiles/BUILD.bazel +++ b/tests/runfiles/BUILD.bazel @@ -5,9 +5,40 @@ load("@rules_python//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # bu py_test( name = "runfiles_test", srcs = ["runfiles_test.py"], + data = [ + "//tests/support:current_build_settings", + ], + env = { + "BZLMOD_ENABLED": "1" if BZLMOD_ENABLED else "0", + }, + deps = ["//python/runfiles"], +) + +py_test( + name = "runfiles_min_python_test", + srcs = ["runfiles_test.py"], + data = [ + "//tests/support:current_build_settings", + ], env = { "BZLMOD_ENABLED": "1" if BZLMOD_ENABLED else "0", }, + main = "runfiles_test.py", + python_version = "3.10", + deps = ["//python/runfiles"], +) + +py_test( + name = "pathlib_test", + srcs = ["pathlib_test.py"], + deps = ["//python/runfiles"], +) + +py_test( + name = "pathlib_min_python_test", + srcs = ["pathlib_test.py"], + main = "pathlib_test.py", + python_version = "3.10", deps = ["//python/runfiles"], ) diff --git a/tests/runfiles/pathlib_test.py b/tests/runfiles/pathlib_test.py new file mode 100644 index 0000000000..a959138235 --- /dev/null +++ b/tests/runfiles/pathlib_test.py @@ -0,0 +1,193 @@ +import os +import pathlib +import tempfile +import unittest + +from python.runfiles import runfiles + + +class PathlibTest(unittest.TestCase): + def setUp(self) -> None: + self.tmpdir = tempfile.TemporaryDirectory(dir=os.environ.get("TEST_TMPDIR")) + # Runfiles paths are expected to be posix paths internally when we + # construct the strings for assertions + self.root_path = pathlib.Path(self.tmpdir.name).resolve() + self.root_dir = self.root_path.as_posix() + + # Create dummy files for I/O tests + self.repo_dir = self.root_path / "my_repo" + self.repo_dir.mkdir() + self.test_file = self.repo_dir / "data.txt" + self.test_file.write_text("hello runfiles", encoding="utf-8") + self.sub_dir = self.repo_dir / "subdir" + self.sub_dir.mkdir() + (self.sub_dir / "other.txt").write_text("other content", encoding="utf-8") + + def _create_runfiles(self) -> runfiles.Runfiles: + r = runfiles.Create({"RUNFILES_DIR": self.root_dir}) + assert r is not None + return r + + def tearDown(self) -> None: + self.tmpdir.cleanup() + + def test_path_api(self) -> None: + r = self._create_runfiles() + root = r.root() + + # Test basic joining + p = root / "repo/pkg/file.txt" + self.assertEqual(str(p), f"{self.root_dir}/repo/pkg/file.txt") + + # Test PurePath API + self.assertEqual(p.name, "file.txt") + self.assertEqual(p.suffix, ".txt") + self.assertEqual(p.parent.name, "pkg") + self.assertEqual(p.parts, ("repo", "pkg", "file.txt")) + self.assertEqual(p.stem, "file") + self.assertEqual(p.suffixes, [".txt"]) + + # Test multiple joins + p2 = root / "repo" / "pkg" / "file.txt" + self.assertEqual(p, p2) + + # Test joins with pathlib objects + p3 = root / pathlib.PurePath("repo/pkg/file.txt") + self.assertEqual(p, p3) + + def test_root(self) -> None: + r = self._create_runfiles() + self.assertEqual(str(r.root()), self.root_dir) + + def test_runfiles_root_method(self) -> None: + r = self._create_runfiles() + p = r.root() / "foo/bar" + self.assertEqual(p.runfiles_root(), r.root()) + self.assertEqual(str(p.runfiles_root()), self.root_dir) + + def test_os_path_like(self) -> None: + r = self._create_runfiles() + p = r.root() / "foo" + self.assertEqual(os.fspath(p), f"{self.root_dir}/foo") + + def test_equality_and_hash(self) -> None: + r = self._create_runfiles() + p1 = r.root() / "foo" + p2 = r.root() / "foo" + p3 = r.root() / "bar" + + self.assertEqual(p1, p2) + self.assertNotEqual(p1, p3) + self.assertEqual(hash(p1), hash(p2)) + + def test_join_path(self) -> None: + r = self._create_runfiles() + p = r.root().joinpath("repo", "file") + self.assertEqual(str(p), f"{self.root_dir}/repo/file") + + def test_parents(self) -> None: + r = self._create_runfiles() + p = r.root() / "a/b/c" + parents = list(p.parents) + self.assertEqual(len(parents), 3) + self.assertEqual(str(parents[0]), f"{self.root_dir}/a/b") + self.assertEqual(str(parents[1]), f"{self.root_dir}/a") + self.assertEqual(str(parents[2]), self.root_dir) + + def test_with_methods(self) -> None: + r = self._create_runfiles() + p = r.root() / "foo/bar.txt" + self.assertEqual(str(p.with_name("baz.py")), f"{self.root_dir}/foo/baz.py") + self.assertEqual(str(p.with_suffix(".dat")), f"{self.root_dir}/foo/bar.dat") + + def test_match(self) -> None: + r = self._create_runfiles() + p = r.root() / "foo/bar.txt" + self.assertTrue(p.match("*.txt")) + self.assertTrue(p.match("foo/*.txt")) + self.assertFalse(p.match("bar/*.txt")) + + def test_reading_api(self) -> None: + r = self._create_runfiles() + root = r.root() + p = root / "my_repo/data.txt" + + self.assertTrue(p.exists()) + self.assertTrue(p.is_file()) + self.assertEqual(p.read_text(encoding="utf-8"), "hello runfiles") + self.assertEqual(p.read_bytes(), b"hello runfiles") + + with p.open("r", encoding="utf-8") as f: + self.assertEqual(f.read(), "hello runfiles") + + def test_stat_api(self) -> None: + r = self._create_runfiles() + root = r.root() + p = root / "my_repo/data.txt" + + st = p.stat() + self.assertEqual(st.st_size, len("hello runfiles")) + + def test_iteration_api(self) -> None: + r = self._create_runfiles() + root = r.root() + p = root / "my_repo" + + self.assertTrue(p.is_dir()) + contents = {c.name for c in p.iterdir()} + self.assertEqual(contents, {"data.txt", "subdir"}) + # Ensure they are still runfiles.Path + for c in p.iterdir(): + self.assertIsInstance(c, runfiles.Path) + + def test_glob(self) -> None: + r = self._create_runfiles() + root = r.root() + p = root / "my_repo" + + glob_results = { + pathlib.PurePath(c).relative_to(pathlib.PurePath(p)).as_posix() + for c in p.glob("*.txt") + } + self.assertEqual(glob_results, {"data.txt"}) + + rglob_results = { + pathlib.PurePath(c).relative_to(pathlib.PurePath(p)).as_posix() + for c in p.rglob("*.txt") + } + self.assertEqual(rglob_results, {"data.txt", "subdir/other.txt"}) + + for c in p.rglob("*.txt"): + self.assertIsInstance(c, runfiles.Path) + + def test_resolve_and_absolute(self) -> None: + r = self._create_runfiles() + root = r.root() + p = root / "my_repo/data.txt" + + resolved = p.resolve() + self.assertIsInstance(resolved, runfiles.Path) + self.assertTrue(resolved.exists()) + self.assertEqual(resolved.read_text(encoding="utf-8"), "hello runfiles") + + absoluted = p.absolute() + self.assertIsInstance(absoluted, runfiles.Path) + self.assertTrue(absoluted.exists()) + self.assertEqual(absoluted.read_text(encoding="utf-8"), "hello runfiles") + + def test_runfile_path(self) -> None: + r = self._create_runfiles() + root = r.root() + p = root / "my_repo/data.txt" + self.assertEqual(p.runfile_path, "my_repo/data.txt") + + p2 = root / "foo" / "bar.txt" + self.assertEqual(p2.runfile_path, "foo/bar.txt") + + self.assertEqual(root.runfile_path, "") + self.assertEqual(repr(root), "runfiles.Path('')") + self.assertEqual(repr(p), "runfiles.Path('my_repo/data.txt')") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/runfiles/runfiles_test.py b/tests/runfiles/runfiles_test.py index b8a3d5f7b7..47c964631f 100644 --- a/tests/runfiles/runfiles_test.py +++ b/tests/runfiles/runfiles_test.py @@ -12,7 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json import os +import pathlib import tempfile import unittest from typing import Any, List, Optional @@ -63,6 +65,16 @@ def testRlocationArgumentValidation(self) -> None: lambda: r.Rlocation("\\foo"), ) + def testRlocationWithData(self) -> None: + r = runfiles.Create() + assert r is not None # mypy doesn't understand the unittest api. + settings_path = r.Rlocation( + "rules_python/tests/support/current_build_settings.json" + ) + assert settings_path is not None + settings = json.loads(pathlib.Path(settings_path).read_text()) + self.assertIn("bootstrap_impl", settings) + def testCreatesManifestBasedRunfiles(self) -> None: with _MockFile(contents=["a/b c/d"]) as mf: r = runfiles.Create( @@ -692,7 +704,7 @@ def testCurrentRepository(self) -> None: expected = "" else: expected = "rules_python" - r = runfiles.Create({"RUNFILES_DIR": "whatever"}) + r = runfiles.Create() assert r is not None # mypy doesn't understand the unittest api. self.assertEqual(r.CurrentRepository(), expected) @@ -713,7 +725,7 @@ def __enter__(self) -> Any: tmpdir = os.environ.get("TEST_TMPDIR") self._path = os.path.join(tempfile.mkdtemp(dir=tmpdir), self._name) with open(self._path, "wt", encoding="utf-8", newline="\n") as f: - f.writelines(l + "\n" for l in self._contents) + f.writelines(l + "\n" for l in self._contents) # noqa: E741 return self def __exit__( diff --git a/tests/runtime_env_toolchain/runtime_env_toolchain_tests.bzl b/tests/runtime_env_toolchain/runtime_env_toolchain_tests.bzl index aa4d1c793b..527448ebbc 100644 --- a/tests/runtime_env_toolchain/runtime_env_toolchain_tests.bzl +++ b/tests/runtime_env_toolchain/runtime_env_toolchain_tests.bzl @@ -24,7 +24,6 @@ load( "PY_CC_TOOLCHAIN_TYPE", "TARGET_TOOLCHAIN_TYPE", ) # buildifier: disable=bzl-visibility -load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER") # buildifier: disable=bzl-visibility load("//tests/support:support.bzl", "CC_TOOLCHAIN") _LookupInfo = provider() # buildifier: disable=provider-params @@ -55,25 +54,14 @@ def _test_runtime_env_toolchain_matches(name): name = name + "_subject", ) extra_toolchains = [ + # We have to add a cc toolchain because py_cc toolchain depends on it. + # However, that package also defines a different fake py_cc toolchain we + # don't want to use, so we need to ensure the runtime_env toolchain has + # higher precendence. + CC_TOOLCHAIN, str(Label("//python/runtime_env_toolchains:all")), ] - # We have to add a cc toolchain because py_cc toolchain depends on it. - # However, that package also defines a different fake py_cc toolchain we - # don't want to use, so we need to ensure the runtime_env toolchain has - # higher precendence. - # However, Bazel 6 and Bazel 7 process --extra_toolchains in different - # orders: - # * Bazel 6 goes left to right - # * Bazel 7 goes right to left - # We could just put our preferred toolchain before *and* after - # the undesired toolchain... - # However, Bazel 7 has a bug where *duplicate* entries are ignored, - # and only the *first* entry is respected. - if IS_BAZEL_7_OR_HIGHER: - extra_toolchains.insert(0, CC_TOOLCHAIN) - else: - extra_toolchains.append(CC_TOOLCHAIN) analysis_test( name = name, impl = _test_runtime_env_toolchain_matches_impl, diff --git a/tests/runtime_env_toolchain/toolchain_runs_test.py b/tests/runtime_env_toolchain/toolchain_runs_test.py index c66b0bbd8a..13b5775ff0 100644 --- a/tests/runtime_env_toolchain/toolchain_runs_test.py +++ b/tests/runtime_env_toolchain/toolchain_runs_test.py @@ -14,6 +14,7 @@ def test_ran(self): "rules_python/tests/support/current_build_settings.json" ) settings = json.loads(pathlib.Path(settings_path).read_text()) + if platform.system() == "Windows": self.assertEqual( "/_magic_pyruntime_sentinel_do_not_use", settings["interpreter_path"] diff --git a/tests/support/BUILD.bazel b/tests/support/BUILD.bazel index 303dbafbdf..45f43c89e2 100644 --- a/tests/support/BUILD.bazel +++ b/tests/support/BUILD.bazel @@ -12,12 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -# ==================== -# NOTE: You probably want to use the constants in test_platforms.bzl -# Otherwise, you'll probably have to manually call Label() on these targets -# to force them to resolve in the proper context. -# ==================== - load("@bazel_skylib//rules:common_settings.bzl", "string_flag") load(":sh_py_run_test.bzl", "current_build_settings") @@ -25,69 +19,6 @@ package( default_visibility = ["//:__subpackages__"], ) -platform( - name = "mac", - constraint_values = [ - "@platforms//os:macos", - ], -) - -platform( - name = "linux", - constraint_values = [ - "@platforms//os:linux", - ], -) - -platform( - name = "windows", - constraint_values = [ - "@platforms//os:windows", - ], -) - -# Used when testing downloading of toolchains for a different platform - -platform( - name = "linux_x86_64", - constraint_values = [ - "@platforms//cpu:x86_64", - "@platforms//os:linux", - ], -) - -platform( - name = "linux_aarch64", - constraint_values = [ - "@platforms//cpu:aarch64", - "@platforms//os:linux", - ], -) - -platform( - name = "mac_x86_64", - constraint_values = [ - "@platforms//cpu:x86_64", - "@platforms//os:macos", - ], -) - -platform( - name = "windows_x86_64", - constraint_values = [ - "@platforms//cpu:x86_64", - "@platforms//os:windows", - ], -) - -platform( - name = "win_aarch64", - constraint_values = [ - "@platforms//os:windows", - "@platforms//cpu:aarch64", - ], -) - current_build_settings( name = "current_build_settings", ) diff --git a/tests/support/cc_toolchains/fake_cc_toolchain_config.bzl b/tests/support/cc_toolchains/fake_cc_toolchain_config.bzl index 8240f09e04..b70c79cfd8 100644 --- a/tests/support/cc_toolchains/fake_cc_toolchain_config.bzl +++ b/tests/support/cc_toolchains/fake_cc_toolchain_config.bzl @@ -15,6 +15,7 @@ """Fake for providing CcToolchainConfigInfo.""" load("@rules_cc//cc/common:cc_common.bzl", "cc_common") +load("@rules_cc//cc/toolchains:cc_toolchain_config_info.bzl", "CcToolchainConfigInfo") def _impl(ctx): return cc_common.create_cc_toolchain_config_info( diff --git a/tests/support/copy_file.bzl b/tests/support/copy_file.bzl new file mode 100644 index 0000000000..bd9bb218f3 --- /dev/null +++ b/tests/support/copy_file.bzl @@ -0,0 +1,33 @@ +"""Copies a file to a directory.""" + +def _copy_file_to_dir_impl(ctx): + out_file = ctx.actions.declare_file( + "{}/{}".format(ctx.attr.out_dir, ctx.file.src.basename), + ) + ctx.actions.run_shell( + inputs = [ctx.file.src], + outputs = [out_file], + arguments = [ctx.file.src.path, out_file.path], + # Perform a copy to better match how a file install from + # a repo-phase (e.g. whl extraction) looks. + command = 'cp -f "$1" "$2"', + progress_message = "Copying %{input} to %{output}", + ) + return [DefaultInfo(files = depset([out_file]))] + +copy_file_to_dir = rule( + implementation = _copy_file_to_dir_impl, + doc = """ +This allows copying a file whose name is platform-dependent to a directory. + +While bazel_skylib has a copy_file rule, you must statically specify the +output file name. +""", + attrs = { + "out_dir": attr.string(mandatory = True), + "src": attr.label( + allow_single_file = True, + mandatory = True, + ), + }, +) diff --git a/tests/support/mocks/BUILD.bazel b/tests/support/mocks/BUILD.bazel new file mode 100644 index 0000000000..07bdb2bffc --- /dev/null +++ b/tests/support/mocks/BUILD.bazel @@ -0,0 +1,19 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") +load(":mocks_tests.bzl", "mocks_test_suite") + +package( + default_visibility = ["//:__subpackages__"], +) + +bzl_library( + name = "mocks_bzl", + srcs = ["mocks.bzl"], +) + +bzl_library( + name = "python_ext_bzl", + srcs = ["python_ext.bzl"], + deps = [":mocks_bzl"], +) + +mocks_test_suite(name = "mocks_tests") diff --git a/tests/support/mocks/mocks.bzl b/tests/support/mocks/mocks.bzl new file mode 100644 index 0000000000..6c79b1f4ff --- /dev/null +++ b/tests/support/mocks/mocks.bzl @@ -0,0 +1,582 @@ +"""Mocks for repository_ctx, module_ctx, and File objects.""" + +def _path_new(path, mock_files = None): + """Create a mock path object. + + Args: + path: {type}`string` The path string. + mock_files: {type}`dict[string, string]` A dict of mocked files. + + Returns: + {type}`MockPath` A struct mocking a path object. + """ + mock_files = mock_files or {} + return struct( + exists = path in mock_files, + basename = path.split("/")[-1], + dirname = "/".join(path.split("/")[:-1]), + _path = path, + ) + +def _file_new(short_path, *, path = None, is_source = True, owner = None): + """Create a mock File object. + + Args: + short_path: {type}`string` The short path to the file. + path: {type}`string` The full path to the file. Defaults to a made + up exec-root path or the short path if is_source. + is_source: {type}`bool` Whether the file is a source file. + owner: {type}`Label|string` The owner label of the file. + + Returns: + {type}`MockFile` A struct mocking a File object. + """ + if owner == None: + owner = Label("//:mock") + + owner_str = str(owner) + repo_name = owner_str.split("//")[0] + + is_main_repo = repo_name in ("", "@", "@@") + + actual_short_path = short_path + if not is_main_repo: + repo_name = repo_name.lstrip("@") + if not actual_short_path.startswith("../"): + actual_short_path = "../{}/{}".format(repo_name, short_path) + + if path == None: + rel_path = short_path + if rel_path.startswith("../"): + parts = rel_path.split("/") + rel_path = "/".join(parts[2:]) + + if is_source: + path = "external/{}/{}".format(repo_name, rel_path) + else: + path = "bazel-out/k9-deadbeef/bin/external/{}/{}".format( + repo_name, + rel_path, + ) + elif path == None: + if is_source: + path = short_path + else: + path = "bazel-out/k9-deadbeef/bin/{}".format(short_path) + + return struct( + path = path, + basename = path.split("/")[-1], + dirname = "/".join(path.split("/")[:-1]), + extension = path.split(".")[-1] if "." in path else "", + is_source = is_source, + owner = owner, + short_path = actual_short_path, + ) + +def _tag_new(**kwargs): + """Create a mock tag. + + Args: + **kwargs: {type}`dict` The tag attributes. + + Returns: + {type}`MockTag` A mock tag object. + """ + return struct(**kwargs) + +def _module_new(name, *, is_root = False, **tags): + """Create a mock module object. + + Args: + name: {type}`string` The name of the module. + is_root: {type}`bool` Whether this is the root module. + **tags: {type}`list[MockTag]` Lists of tag objects. + + Returns: + {type}`MockModule` A mock module object. + """ + return struct( + name = name, + tags = struct(**tags), + is_root = is_root, + ) + +def _mctx_read(self, x, watch = None): + _ = watch # @unused + path_str = x._path if hasattr(x, "_path") else str(x) + if path_str in self.mock_files: + return self.mock_files[path_str] + for k, v in self.mock_files.items(): + if k in path_str or k in path_str.replace(":", "/"): + return v + fail("File not found in mock_files: " + path_str) + +def _mctx_path(self, x): + return _path_new(str(x), self.mock_files) + +def _get_download_file_name(url, output = ""): + """Compute the download file name. + + Args: + url: {type}`string` The URL being downloaded. + output: {type}`string` The explicit output path, if any. + + Returns: + {type}`string` The file name. + """ + if output: + return str(output) + return str(url).split("?")[0].split("/")[-1] + +def _mctx_download( + self, + url, + output = "", + sha256 = "", + executable = False, + allow_fail = False, + canonical_id = "", + auth = {}, + headers = {}, + integrity = "", + block = True): + _ = ( + sha256, + executable, + allow_fail, + canonical_id, + auth, + headers, + integrity, + block, + ) # @unused + urls = url if type(url) == "list" else [url] + for u in urls: + content = None + if u in self.mock_downloads: + content = self.mock_downloads[u] + elif "*" in self.mock_downloads: + content = self.mock_downloads["*"] + + if content != None: + if type(content) == "string": + out = _get_download_file_name(u, output) + self.mock_files[out] = content + return struct( + success = True, + wait = lambda: struct(success = True), + ) + else: + return content( + self, + u, + output, + sha256, + executable, + allow_fail, + canonical_id, + auth, + headers, + integrity, + ) + + if not self.mock_downloads: + return struct(success = True, wait = lambda: struct(success = True)) + return struct(success = False, wait = lambda: struct(success = False)) + +def _mrctx_report_progress(self, message): + self.report_progress_calls.append(message) + return None + +def _mctx_add_module(self, **kwargs): + """Add a module to the mock module_ctx. + + Args: + self: The mock module_ctx. + **kwargs: Arguments to pass to _module_new. + + Returns: + {type}`MockModuleCtx` The mock module_ctx. + """ + module = _module_new(**kwargs) + if module.is_root and len(self.modules) > 0: + fail("is_root=True can only be set on the first module in the " + + "modules list.") + self.modules.append(module) + return self + +def _mctx_new( + *args, + modules = None, + environ = None, + mock_files = None, + mock_downloads = None, + os_name = "linux", + arch_name = "x86_64", + facts = None): + """Create a mock module_ctx object. + + Args: + *args: {type}`list[MockModule]` Mock modules passed positionally. + modules: {type}`list[MockModule]` List of mock modules (alternative + to positional args). + environ: {type}`dict[string, string]` Dict of environment variables. + mock_files: {type}`dict[string, string]` Dict mapping path strings + to content. + mock_downloads: {type}`dict[string, string|callable]` Dict mapping + url to string or callable. + os_name: {type}`string` The OS name. + arch_name: {type}`string` The architecture name. + facts: {type}`dict` Optional facts dict. + + Returns: + {type}`MockModuleCtx` A struct mocking a module_ctx object. + """ + modules = list(args) + (modules or []) + + for i, mod in enumerate(modules): + if getattr(mod, "is_root", False) and i != 0: + fail("is_root=True can only be set on the first module in the " + + "modules list.") + + environ = environ or {} + mock_files = mock_files or {} + mock_downloads = mock_downloads or {} + + # buildifier: disable=uninitialized + self = struct( + mock_files = mock_files, + mock_downloads = mock_downloads, + report_progress_calls = [], + getenv = environ.get, + facts = facts, + os = struct( + name = os_name, + arch = arch_name, + ), + modules = list(modules), + path = lambda *a, **k: _mctx_path(self, *a, **k), + read = lambda *a, **k: _mctx_read(self, *a, **k), + download = lambda *a, **k: _mctx_download(self, *a, **k), + report_progress = lambda *a, **k: _mrctx_report_progress(self, *a, **k), + add_module = lambda **k: _mctx_add_module(self, **k), + ) + return self + +def _rctx_read(self, x): + path_str = x._path if hasattr(x, "_path") else str(x) + if path_str not in self.mock_files: + fail("File not found in mock_files: " + path_str) + + val = self.mock_files[path_str] + for _ in range(10): + if type(val) == "dict" and val.get("type") == "symlink": + path_str = val["target"] + if path_str not in self.mock_files: + fail("Symlink target not found in mock_files: " + path_str) + val = self.mock_files[path_str] + else: + break + + if type(val) == "dict" and val.get("type") == "symlink": + fail("Too many symlinks followed") + + return val + +def _rctx_path(self, x): + return _path_new(str(x), self.mock_files) + +def _rctx_file(self, path, content = "", executable = True, legacy_utf8 = True): + _ = executable, legacy_utf8 # @unused + self.mock_files[str(path)] = content + +def _rctx_template(self, path, template, substitutions = {}, executable = True): + _ = executable # @unused + template_str = str(template) + if template_str not in self.mock_files: + fail("Template file not found: " + template_str) + + content = self.mock_files[template_str] + for key, value in substitutions.items(): + content = content.replace(key, value) + + self.mock_files[str(path)] = content + +def _rctx_which(self, program): + prog_str = str(program) + if prog_str in self.mock_which: + res = self.mock_which[prog_str] + if res == None: + return None + return _path_new(res, self.mock_files) + return None + +def _rctx_download( + self, + url, + output = "", + sha256 = "", + executable = False, + allow_fail = False, + canonical_id = "", + auth = {}, + headers = {}, + integrity = ""): + _ = ( + sha256, + executable, + allow_fail, + canonical_id, + auth, + headers, + integrity, + ) # @unused + + urls = url if type(url) == "list" else [url] + + for u in urls: + if u in self.mock_downloads: + res = self.mock_downloads[u] + if type(res) == "string": + out = _get_download_file_name(u, output) + self.mock_files[out] = res + return struct(success = True, sha256 = "mocksha256") + else: + return res( + self, + u, + output, + sha256, + executable, + allow_fail, + canonical_id, + auth, + headers, + integrity, + ) + + if not allow_fail: + fail("Download not mocked for url: " + str(urls)) + return struct(success = False) + +def _rctx_extract( + self, + archive, + output = "", + stripPrefix = "", + rename_files = {}, + *, + watch_archive = "auto"): + _ = ( + stripPrefix, + rename_files, + watch_archive, + ) # @unused + + archive_str = str(archive) + if archive_str in self.mock_extracts: + for f, c in self.mock_extracts[archive_str].items(): + out_path = "{}/{}".format(output, f) if output else str(f) + self.mock_files[out_path] = c + +def _rctx_download_and_extract( + self, + url, + output = "", + sha256 = "", + type = "", + stripPrefix = "", + allow_fail = False, + canonical_id = "", + auth = {}, + headers = {}, + integrity = "", + rename_files = {}): + _ = type # @unused + + res = self.download( + url = url, + output = "", + sha256 = sha256, + allow_fail = allow_fail, + canonical_id = canonical_id, + auth = auth, + headers = headers, + integrity = integrity, + ) + if not res.success: + return res + + urls = url if type(url) == "list" else [url] + for u in urls: + downloaded_file = _get_download_file_name(u) + if downloaded_file in self.mock_extracts: + self.extract( + archive = downloaded_file, + output = output, + stripPrefix = stripPrefix, + rename_files = rename_files, + ) + break + + return res + +def _rctx_execute( + self, + arguments, + timeout = 600, + quiet = True, + working_directory = "", + environment = {}, + custom_reporter = ""): + _ = ( + self, + arguments, + timeout, + quiet, + working_directory, + environment, + custom_reporter, + ) # @unused + self.execute_calls.append(arguments) + return struct(return_code = 0, stdout = "", stderr = "") + +def _rctx_symlink(self, target, link_name): + self.mock_files[str(link_name)] = {"target": str(target), "type": "symlink"} + +def _rctx_new( + attr = None, + environ = None, + mock_files = None, + mock_which = None, + mock_downloads = None, + mock_extracts = None, + os_name = "linux", + arch_name = "x86_64"): + """Create a mock repository_ctx object. + + Args: + attr: {type}`dict` Dict of attributes. + environ: {type}`dict[string, string]` Dict of environment variables. + mock_files: {type}`dict[string, string]` Dict mapping path strings + to content. + mock_which: {type}`dict[string, string]` Dict mapping program + name to path string. + mock_downloads: {type}`dict[string, string|callable]` Dict mapping + url to string or callable. + mock_extracts: {type}`dict[string, dict[string, string]]` Dict mapping + downloaded filename to a dict of extracted filename to content. + os_name: {type}`string` The OS name. + arch_name: {type}`string` The architecture name. + + Returns: + {type}`MockRepositoryCtx` A struct mocking a repository_ctx object. + """ + attr = attr or {} + environ = environ or {} + mock_files = mock_files or {} + mock_which = mock_which or {} + mock_downloads = mock_downloads or {} + mock_extracts = mock_extracts or {} + + # buildifier: disable=uninitialized + self = struct( + mock_files = mock_files, + mock_which = mock_which, + mock_downloads = mock_downloads, + mock_extracts = mock_extracts, + execute_calls = [], + report_progress_calls = [], + attr = struct(**attr), + name = attr.get("name", "mock"), + os = struct( + name = os_name, + arch = arch_name, + ), + os_environ = environ, + path = lambda *a, **k: _rctx_path(self, *a, **k), + read = lambda *a, **k: _rctx_read(self, *a, **k), + file = lambda *a, **k: _rctx_file(self, *a, **k), + getenv = environ.get, + template = lambda *a, **k: _rctx_template(self, *a, **k), + which = lambda *a, **k: _rctx_which(self, *a, **k), + download = lambda *a, **k: _rctx_download(self, *a, **k), + extract = lambda *a, **k: _rctx_extract(self, *a, **k), + delete = lambda x: True, + download_and_extract = lambda *a, **k: _rctx_download_and_extract( + self, + *a, + **k + ), + execute = lambda *a, **k: _rctx_execute(self, *a, **k), + symlink = lambda *a, **k: _rctx_symlink(self, *a, **k), + report_progress = lambda *a, **k: _mrctx_report_progress(self, *a, **k), + ) + + return self + +def _glob_call_new(*args, **kwargs): + """Create a struct representing a glob call. + + Args: + *args: {type}`tuple` Positional arguments to glob. + **kwargs: {type}`dict` Keyword arguments to glob. + + Returns: + {type}`MockGlobCall` A struct with glob and kwargs fields. + """ + return struct( + glob = args, + kwargs = kwargs, + ) + +def _glob_new(): + """Create a mock glob object. + + Returns: + {type}`MockGlob` A struct with calls and results lists, and a + glob function. + """ + calls = [] + results = [] + + def _glob_fn(*args, **kwargs): + calls.append(_glob_call_new(*args, **kwargs)) + if not results: + fail("Mock glob missing for invocation: args={} kwargs={}".format( + args, + kwargs, + )) + return results.pop(0) + + return struct( + calls = calls, + results = results, + glob = _glob_fn, + ) + +def _select_new(value, no_match_error = None): + """A mock select function that returns the value. + + Args: + value: {type}`Any` The value to return. + no_match_error: {type}`string` Ignored. + + Returns: + {type}`MockSelect` The value. + """ + _ = no_match_error # @unused + return value + +mocks = struct( + file = _file_new, + glob = _glob_new, + glob_call = _glob_call_new, + mctx = _mctx_new, + module = _module_new, + path = _path_new, + rctx = _rctx_new, + select = _select_new, + tag = _tag_new, +) diff --git a/tests/support/mocks/mocks_tests.bzl b/tests/support/mocks/mocks_tests.bzl new file mode 100644 index 0000000000..1dc495b342 --- /dev/null +++ b/tests/support/mocks/mocks_tests.bzl @@ -0,0 +1,139 @@ +"""Tests for mocks.bzl""" + +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("//tests/support/mocks:mocks.bzl", "mocks") + +_tests = [] + +def _test_path(env): + p1 = mocks.path("a/b/c", mock_files = {"a/b/c": "data"}) + env.expect.that_bool(p1.exists).equals(True) + env.expect.that_str(p1.basename).equals("c") + env.expect.that_str(p1.dirname).equals("a/b") + env.expect.that_str(p1._path).equals("a/b/c") + + p2 = mocks.path("d/e/f", mock_files = {}) + env.expect.that_bool(p2.exists).equals(False) + +_tests.append(_test_path) + +def _test_file(env): + # Default main repo + f1 = mocks.file("a/b.txt", is_source = True) + env.expect.that_str(f1.path).equals("a/b.txt") + env.expect.that_str(f1.short_path).equals("a/b.txt") + env.expect.that_str(f1.basename).equals("b.txt") + env.expect.that_str(f1.dirname).equals("a") + env.expect.that_str(f1.extension).equals("txt") + env.expect.that_bool(f1.is_source).equals(True) + env.expect.that_str(str(f1.owner)).equals(str(Label("//:mock"))) + + # External repo + f2 = mocks.file("a/b.txt", is_source = True, owner = "@foo//:mock") + env.expect.that_str(f2.path).equals("external/foo/a/b.txt") + env.expect.that_str(f2.short_path).equals("../foo/a/b.txt") + + # External repo generated file + f3 = mocks.file("a/b.txt", is_source = False, owner = "@foo//:mock") + env.expect.that_str(f3.path).equals( + "bazel-out/k9-deadbeef/bin/external/foo/a/b.txt", + ) + env.expect.that_str(f3.short_path).equals("../foo/a/b.txt") + +_tests.append(_test_file) + +def _test_mctx(env): + mctx = mocks.mctx( + environ = {"FOO": "bar"}, + mock_files = {"file.txt": "content"}, + mock_downloads = {"http://example.com": "downloaded"}, + os_name = "windows", + arch_name = "x86_64", + ) + env.expect.that_str(mctx.getenv("FOO")).equals("bar") + env.expect.that_str(mctx.read(mocks.path("file.txt"))).equals("content") + env.expect.that_str(mctx.os.name).equals("windows") + env.expect.that_str(mctx.os.arch).equals("x86_64") + + # Test download + res = mctx.download("http://example.com", "out.txt") + env.expect.that_bool(res.success).equals(True) + env.expect.that_str(mctx.read(mocks.path("out.txt"))).equals("downloaded") + + # Test report progress + mctx.report_progress("doing something") + env.expect.that_collection(mctx.report_progress_calls).contains_exactly([ + "doing something", + ]) + +_tests.append(_test_mctx) + +def _test_rctx(env): + rctx = mocks.rctx( + environ = {"FOO": "bar"}, + mock_files = {"file.txt": "content"}, + mock_which = {"mycmd": "path/to/mycmd"}, + ) + env.expect.that_str(rctx.os_environ["FOO"]).equals("bar") + env.expect.that_str(rctx.read(mocks.path("file.txt"))).equals("content") + + # Test which + w = rctx.which("mycmd") + env.expect.that_str(w._path).equals("path/to/mycmd") + env.expect.that_bool(rctx.which("not_found") == None).equals(True) + + # Test file writing + rctx.file("new.txt", "new content") + env.expect.that_str(rctx.read(mocks.path("new.txt"))).equals("new content") + + # Test template + rctx.file("template.txt", "Hello {name}") + rctx.template("rendered.txt", "template.txt", {"{name}": "World"}) + env.expect.that_str(rctx.read(mocks.path("rendered.txt"))).equals( + "Hello World", + ) + + # Test symlink reading + rctx.symlink("rendered.txt", "link.txt") + env.expect.that_str(rctx.read(mocks.path("link.txt"))).equals("Hello World") + +_tests.append(_test_rctx) + +def _test_glob(env): + g = mocks.glob() + g.results.append(["a.txt", "b.txt"]) + res = g.glob(["*.txt"], exclude = ["c.txt"]) + env.expect.that_collection(res).contains_exactly(["a.txt", "b.txt"]) + env.expect.that_collection(g.calls).has_size(1) + env.expect.that_collection(g.calls[0].glob[0]).contains_exactly(["*.txt"]) + env.expect.that_collection(g.calls[0].kwargs["exclude"]).contains_exactly([ + "c.txt", + ]) + +_tests.append(_test_glob) + +def _test_module_and_tags(env): + mod = mocks.module( + "my_mod", + is_root = True, + my_tag = [mocks.tag(attr1 = "val1")], + ) + env.expect.that_str(mod.name).equals("my_mod") + env.expect.that_bool(mod.is_root).equals(True) + env.expect.that_str(mod.tags.my_tag[0].attr1).equals("val1") + +_tests.append(_test_module_and_tags) + +def _test_select(env): + res = mocks.select({"//conditions:default": "val"}) + env.expect.that_dict(res).contains_exactly({"//conditions:default": "val"}) + +_tests.append(_test_select) + +def mocks_test_suite(name): + """Create the test suite. + + Args: + name: the name of the test suite + """ + test_suite(name = name, basic_tests = _tests) diff --git a/tests/support/mocks/python_ext.bzl b/tests/support/mocks/python_ext.bzl new file mode 100644 index 0000000000..dc3ac41f8d --- /dev/null +++ b/tests/support/mocks/python_ext.bzl @@ -0,0 +1,140 @@ +"""Helper for defining a mock module for the python bzlmod extension.""" + +load(":mocks.bzl", "mocks") + +def _module(name = "rules_python", is_root = True, **tags): + """Creates a mock Bzlmod module struct with defaulted tag lists. + + Args: + name: The module name. + is_root: Whether this is the root module. + **tags: Lists of tag objects. + + Returns: + A mock module struct. + """ + defaulted_tags = { + "defaults": [], + "override": [], + "single_version_override": [], + "single_version_platform_override": [], + "toolchain": [], + } + defaulted_tags.update(tags) + return mocks.module(name = name, is_root = is_root, **defaulted_tags) + +def _override(**kwargs): + """Creates a mock python.override tag with default values.""" + attrs = { + "add_runtime_manifest_files": [], + "add_runtime_manifest_urls": [], + "add_target_settings": [], + "available_python_versions": [], + "base_urls": ["https://github.com/astral-sh/python-build-standalone/releases/download"], + "ignore_root_user_error": True, + "minor_mapping": {}, + "register_all_versions": False, + "runtime_manifest_sha": "", + } + attrs.update(kwargs) + return mocks.tag(**attrs) + +def _defaults(**kwargs): + """Creates a mock python.defaults tag with default values.""" + attrs = { + "python_version": "", + "python_version_env": "", + } + attrs.update(kwargs) + return mocks.tag(**attrs) + +def _single_version_override(**kwargs): + """Creates a mock python.single_version_override tag with default values.""" + attrs = { + "distutils": None, + "distutils_content": "", + "patch_strip": 0, + "patches": [], + "python_version": "", + "sha256": {}, + "strip_prefix": "python", + "urls": [], + } + attrs.update(kwargs) + return mocks.tag(**attrs) + +def _single_version_platform_override(**kwargs): + """Creates a mock python.single_version_platform_override tag with default values.""" + attrs = { + "arch": "", + "coverage_tool": None, + "os_name": "", + "patch_strip": 0, + "patches": [], + "platform": "", + "python_version": "", + "sha256": "", + "strip_prefix": "python", + "target_compatible_with": [], + "target_settings": [], + "urls": [], + } + attrs.update(kwargs) + return mocks.tag(**attrs) + +def _toolchain(**kwargs): + """Creates a mock python.toolchain tag with default values.""" + attrs = { + "configure_coverage_tool": False, + "ignore_root_user_error": True, + "is_default": False, + "python_version": "", + } + attrs.update(kwargs) + return mocks.tag(**attrs) + +_DEFAULT_RUNTIMES_MANIFEST = """ +87275619c2706affa4d1090d2ca3dad354b6d69f8b85dbfafe38785870751b9a 20251031/cpython-3.9.25+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz +6112d46355857680b81849764a6cf9f38cc4cd0d1cf29d432bc12fe5aeedf9d0 20260414/cpython-3.10.20+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +1111111111111111111111111111111111111111111111111111111111111111 20241016/cpython-3.10.15+20241016-x86_64-unknown-linux-gnu-install_only.tar.gz +2222222222222222222222222222222222222222222222222222222222222222 20240224/cpython-3.10.13+20240224-x86_64-unknown-linux-gnu-install_only.tar.gz +0000000000000000000000000000000000000000000000000000000000000000 20260414/cpython-3.11.15+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +3333333333333333333333333333333333333333333333333333333333333333 20241016/cpython-3.11.10+20241016-x86_64-unknown-linux-gnu-install_only.tar.gz +4444444444444444444444444444444444444444444444444444444444444444 20230116/cpython-3.11.2+20230116-x86_64-unknown-linux-gnu-install_only.tar.gz +5555555555555555555555555555555555555555555555555555555555555555 20230116/cpython-3.11.1+20230116-x86_64-unknown-linux-gnu-install_only.tar.gz +6666666666666666666666666666666666666666666666666666666666666666 20260414/cpython-3.12.13+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +7777777777777777777777777777777777777777777777777777777777777777 20240726/cpython-3.12.4+20240726-x86_64-unknown-linux-gnu-install_only.tar.gz +8888888888888888888888888888888888888888888888888888888888888888 20260414/cpython-3.13.13+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +9999999999999999999999999999999999999999999999999999999999999999 20241016/cpython-3.13.0+20241016-x86_64-unknown-linux-gnu-install_only.tar.gz +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 20260414/cpython-3.14.4+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 20251031/cpython-3.14.0+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz +cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc 20260414/cpython-3.15.0a8+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd 20241205/cpython-3.13.1+20241205-x86_64-unknown-linux-gnu-install_only.tar.gz +""" + +def _mctx(*args, **kwargs): + """Creates a mock module_ctx pre-populated with the default runtimes manifest. + + Args: + *args: Positional arguments to pass to mocks.mctx. + **kwargs: Keyword arguments to pass to mocks.mctx. + + Returns: + A mock module_ctx struct. + """ + mock_files = { + "python/private/runtimes_manifest.txt": _DEFAULT_RUNTIMES_MANIFEST, + } + mock_files.update(kwargs.pop("mock_files", {})) + kwargs["mock_files"] = mock_files + return mocks.mctx(*args, **kwargs) + +python_ext = struct( + defaults = _defaults, + mctx = _mctx, + module = _module, + override = _override, + single_version_override = _single_version_override, + single_version_platform_override = _single_version_platform_override, + toolchain = _toolchain, +) diff --git a/tests/support/platforms/BUILD.bazel b/tests/support/platforms/BUILD.bazel new file mode 100644 index 0000000000..eeb7ccb597 --- /dev/null +++ b/tests/support/platforms/BUILD.bazel @@ -0,0 +1,85 @@ +package( + default_visibility = ["//:__subpackages__"], +) + +# ==================== +# NOTE: You probably want to use the constants in test_platforms.bzl +# Otherwise, you'll probably have to manually call Label() on these targets +# to force them to resolve in the proper context. +# ==================== +platform( + name = "mac", + constraint_values = [ + "@platforms//os:macos", + ], +) + +platform( + name = "linux", + constraint_values = [ + "@platforms//os:linux", + ], +) + +platform( + name = "windows", + constraint_values = [ + "@platforms//os:windows", + ], +) + +platform( + name = "linux_x86_64", + constraint_values = [ + "@platforms//cpu:x86_64", + "@platforms//os:linux", + ], +) + +platform( + name = "linux_aarch64", + constraint_values = [ + "@platforms//cpu:aarch64", + "@platforms//os:linux", + ], +) + +platform( + name = "mac_x86_64", + constraint_values = [ + "@platforms//cpu:x86_64", + "@platforms//os:macos", + ], +) + +platform( + name = "mac_aarch64", + constraint_values = [ + "@platforms//cpu:aarch64", + "@platforms//os:macos", + ], +) + +platform( + name = "windows_x86_64", + constraint_values = [ + "@platforms//cpu:x86_64", + "@platforms//os:windows", + ], +) + +platform( + name = "windows_aarch64", + constraint_values = [ + "@platforms//os:windows", + "@platforms//cpu:aarch64", + ], +) + +platform( + name = "exotic_unix", + constraint_values = [ + "@platforms//os:linux", + "@platforms//cpu:s390x", + ], +) diff --git a/tests/support/platforms/platforms.bzl b/tests/support/platforms/platforms.bzl new file mode 100644 index 0000000000..92a1d61844 --- /dev/null +++ b/tests/support/platforms/platforms.bzl @@ -0,0 +1,17 @@ +"""Constants and utilities for platforms used for testing.""" + +platform_targets = struct( + LINUX = Label("//tests/support/platforms:linux"), + LINUX_AARCH64 = Label("//tests/support/platforms:linux_aarch64"), + LINUX_X86_64 = Label("//tests/support/platforms:linux_x86_64"), + MAC = Label("//tests/support/platforms:mac"), + MAC_X86_64 = Label("//tests/support/platforms:mac_x86_64"), + MAC_AARCH64 = Label("//tests/support/platforms:mac_aarch64"), + WINDOWS = Label("//tests/support/platforms:windows"), + WINDOWS_AARCH64 = Label("//tests/support/platforms:windows_aarch64"), + WINDOWS_X86_64 = Label("//tests/support/platforms:windows_x86_64"), + + # Unspecified Unix platform that is unlikely to be the host platform in CI, + # but still provides a Python toolchain. + EXOTIC_UNIX = Label("//tests/support/platforms:exotic_unix"), +) diff --git a/tests/support/py_reconfig.bzl b/tests/support/py_reconfig.bzl index d52cc5dd95..9bbfdb1104 100644 --- a/tests/support/py_reconfig.bzl +++ b/tests/support/py_reconfig.bzl @@ -17,6 +17,7 @@ This facilitates verify running binaries with different configuration settings without the overhead of a bazel-in-bazel integration test. """ +load("@rules_python_internal//:rules_python_config.bzl", "config") load("//python/private:attr_builders.bzl", "attrb") # buildifier: disable=bzl-visibility load("//python/private:common_labels.bzl", "labels") # buildifier: disable=bzl-visibility load("//python/private:py_binary_macro.bzl", "py_binary_macro") # buildifier: disable=bzl-visibility @@ -30,7 +31,10 @@ def _perform_transition_impl(input_settings, attr, base_impl): settings.update(base_impl(input_settings, attr)) settings[labels.VISIBLE_FOR_TESTING] = True - settings["//command_line_option:build_python_zip"] = attr.build_python_zip + + if _BUILTIN_BUILD_PYTHON_ZIP: + settings["//command_line_option:build_python_zip"] = str(attr.build_python_zip) + settings[labels.BUILD_PYTHON_ZIP] = attr.build_python_zip if attr.bootstrap_impl: settings[labels.BOOTSTRAP_IMPL] = attr.bootstrap_impl if attr.extra_toolchains: @@ -43,12 +47,16 @@ def _perform_transition_impl(input_settings, attr, base_impl): settings[labels.VENVS_USE_DECLARE_SYMLINK] = attr.venvs_use_declare_symlink if attr.venvs_site_packages: settings[labels.VENVS_SITE_PACKAGES] = attr.venvs_site_packages - for key, value in attr.config_settings.items(): - settings[str(key)] = value return settings +_BUILTIN_BUILD_PYTHON_ZIP = [] if config.bazel_10_or_later else [ + "//command_line_option:build_python_zip", +] + _RECONFIG_INPUTS = [ + "//command_line_option:build_runfile_links", "//command_line_option:extra_toolchains", + "//command_line_option:stamp", CUSTOM_RUNTIME, labels.BOOTSTRAP_IMPL, labels.PYTHON_SRC, @@ -57,14 +65,14 @@ _RECONFIG_INPUTS = [ labels.VENVS_USE_DECLARE_SYMLINK, ] _RECONFIG_OUTPUTS = _RECONFIG_INPUTS + [ - "//command_line_option:build_python_zip", + labels.BUILD_PYTHON_ZIP, labels.VISIBLE_FOR_TESTING, -] +] + _BUILTIN_BUILD_PYTHON_ZIP _RECONFIG_INHERITED_OUTPUTS = [v for v in _RECONFIG_OUTPUTS if v in _RECONFIG_INPUTS] _RECONFIG_ATTRS = { "bootstrap_impl": attrb.String(), - "build_python_zip": attrb.String(default = "auto"), + "build_python_zip": attrb.Bool(default = config.build_python_zip_default), "config_settings": attrb.LabelKeyedStringDict(), "extra_toolchains": attrb.StringList( doc = """ diff --git a/tests/support/py_runtime_info_subject.bzl b/tests/support/py_runtime_info_subject.bzl index 541d4d9e18..0c6c672743 100644 --- a/tests/support/py_runtime_info_subject.bzl +++ b/tests/support/py_runtime_info_subject.bzl @@ -37,6 +37,9 @@ def py_runtime_info_subject(info, *, meta): coverage_tool = lambda *a, **k: _py_runtime_info_subject_coverage_tool(self, *a, **k), files = lambda *a, **k: _py_runtime_info_subject_files(self, *a, **k), interpreter = lambda *a, **k: _py_runtime_info_subject_interpreter(self, *a, **k), + interpreter_files_to_run = lambda *a, **k: ( + _py_runtime_info_subject_interpreter_files_to_run(self, *a, **k) + ), interpreter_path = lambda *a, **k: _py_runtime_info_subject_interpreter_path(self, *a, **k), interpreter_version_info = lambda *a, **k: _py_runtime_info_subject_interpreter_version_info(self, *a, **k), python_version = lambda *a, **k: _py_runtime_info_subject_python_version(self, *a, **k), @@ -84,6 +87,15 @@ def _py_runtime_info_subject_interpreter(self): meta = self.meta.derive("interpreter()"), ) +def _py_runtime_info_subject_interpreter_files_to_run(self): + return subjects.struct( + self.actual.interpreter_files_to_run, + attrs = dict( + executable = subjects.file, + ), + meta = self.meta.derive("interpreter_files_to_run()"), + ) + def _py_runtime_info_subject_interpreter_path(self): return subjects.str( self.actual.interpreter_path, diff --git a/tests/support/support.bzl b/tests/support/support.bzl index 37d3488316..64f77d76bc 100644 --- a/tests/support/support.bzl +++ b/tests/support/support.bzl @@ -19,15 +19,8 @@ # rules_testing or as config_setting values, which don't support Label in some # places. +load("@rules_python_internal//:rules_python_config.bzl", "config") load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility -load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER") # buildifier: disable=bzl-visibility - -MAC = Label("//tests/support:mac") -MAC_X86_64 = Label("//tests/support:mac_x86_64") -LINUX = Label("//tests/support:linux") -LINUX_X86_64 = Label("//tests/support:linux_x86_64") -WINDOWS = Label("//tests/support:windows") -WINDOWS_X86_64 = Label("//tests/support:windows_x86_64") PY_TOOLCHAINS = str(Label("//tests/support/py_toolchains:all")) CC_TOOLCHAIN = str(Label("//tests/support/cc_toolchains:all")) @@ -40,14 +33,13 @@ CUSTOM_RUNTIME = str(Label("//tests/support:custom_runtime")) SUPPORTS_BOOTSTRAP_SCRIPT = select({ "@platforms//os:windows": ["@platforms//:incompatible"], "//conditions:default": [], -}) if IS_BAZEL_7_OR_HIGHER else ["@platforms//:incompatible"] +}) -SUPPORTS_BZLMOD_UNIXY = select({ - "@platforms//os:windows": ["@platforms//:incompatible"], - "//conditions:default": [], -}) if BZLMOD_ENABLED else ["@platforms//:incompatible"] +SUPPORTS_BZLMOD = [] if BZLMOD_ENABLED else ["@platforms//:incompatible"] NOT_WINDOWS = select({ "@platforms//os:windows": ["@platforms//:incompatible"], "//conditions:default": [], }) + +BAZEL_8_OR_LATER = [] if config.bazel_8_or_later else ["@platforms//:incompatible"] diff --git a/tests/support/whl_from_dir/whl_from_dir_repo.bzl b/tests/support/whl_from_dir/whl_from_dir_repo.bzl index 176525636c..c827c8a0a0 100644 --- a/tests/support/whl_from_dir/whl_from_dir_repo.bzl +++ b/tests/support/whl_from_dir/whl_from_dir_repo.bzl @@ -8,23 +8,44 @@ load("//python/private:repo_utils.bzl", "repo_utils") # buildifier: disable=bzl def _whl_from_dir_repo(rctx): root = rctx.path(rctx.attr.root).dirname - repo_utils.watch_tree(rctx, root) + rctx.watch_tree(root) output = rctx.path(rctx.attr.output) - repo_utils.execute_checked( - rctx, - # cd to root so zip recursively takes everything there. - working_directory = str(root), - op = "WhlFromDir", - arguments = [ - "zip", - "-0", # Skip compressing - "-X", # Don't store file time or metadata - str(output), - "-r", - ".", - ], - ) + if repo_utils.get_platforms_os_name(rctx) == "windows": + powershell_exe = rctx.which("powershell.exe") or rctx.which("powershell") + if not powershell_exe: + fail("powershell not found on PATH") + + zip_script = rctx.path(rctx.attr._zip_script) + + repo_utils.execute_checked( + rctx, + op = "WhlFromDir", + arguments = [ + powershell_exe, + "-NoProfile", + "-File", + str(zip_script), + str(output), + str(root), + ], + # zip.ps1 handles relativizing paths. + ) + else: + repo_utils.execute_checked( + rctx, + # cd to root so zip recursively takes everything there. + working_directory = str(root), + op = "WhlFromDir", + arguments = [ + "zip", + "-0", # Skip compressing + "-X", # Don't store file time or metadata + str(output), + "-r", + ".", + ], + ) rctx.file("BUILD.bazel", 'exports_files(glob(["*"]))') whl_from_dir_repo = repository_rule( @@ -46,5 +67,9 @@ A file whose directory will be put into the output wheel. All files are included verbatim. """, ), + "_zip_script": attr.label( + default = "//tests/support/whl_from_dir:zip.ps1", + allow_single_file = True, + ), }, ) diff --git a/tests/support/whl_from_dir/zip.ps1 b/tests/support/whl_from_dir/zip.ps1 new file mode 100644 index 0000000000..1e8c199cde --- /dev/null +++ b/tests/support/whl_from_dir/zip.ps1 @@ -0,0 +1,45 @@ +param ( + [Parameter(Position=0, Mandatory=$true)] + [string]$Output, + + [Parameter(Position=1, Mandatory=$true)] + [string]$Root +) + +Add-Type -AssemblyName System.IO.Compression + +$fixedTime = [datetime]"1980-01-01T00:00:00" +$RootFull = (Resolve-Path $Root).Path + +$stream = [System.IO.File]::Open($Output, [System.IO.FileMode]::Create) +try { + $archive = [System.IO.Compression.ZipArchive]::new($stream, [System.IO.Compression.ZipArchiveMode]::Create) + try { + $files = Get-ChildItem -Path $RootFull -Recurse -File + foreach ($file in $files) { + # Relativize path and normalize separators + $relPath = $file.FullName.Substring($RootFull.Length).TrimStart('\', '/') + $relPath = $relPath -replace '\\', '/' + + $entry = $archive.CreateEntry($relPath, [System.IO.Compression.CompressionLevel]::NoCompression) + $entry.LastWriteTime = $fixedTime + + $entryStream = $entry.Open() + try { + $fileStream = [System.IO.File]::OpenRead($file.FullName) + try { + $fileStream.CopyTo($entryStream) + } finally { + $fileStream.Dispose() + } + } finally { + $entryStream.Dispose() + } + } + } finally { + $archive.Dispose() + } +} finally { + $stream.Dispose() +} + diff --git a/tests/toolchains/BUILD.bazel b/tests/toolchains/BUILD.bazel index f32ab6f056..b336a06269 100644 --- a/tests/toolchains/BUILD.bazel +++ b/tests/toolchains/BUILD.bazel @@ -13,6 +13,7 @@ # limitations under the License. load("@bazel_skylib//rules:build_test.bzl", "build_test") +load("@bazel_skylib//rules:diff_test.bzl", "diff_test") load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility load("//tests/support:py_reconfig.bzl", "py_reconfig_test") load(":defs.bzl", "define_toolchain_tests") @@ -38,3 +39,18 @@ build_test( "@python_3_11//:python_headers", ], ) + +# Verify that runtimes_manifest_workspace.bzl exactly matches runtimes_manifest.txt +genrule( + name = "gen_expected_runtimes_manifest_workspace", + srcs = ["//python/private:runtimes_manifest.txt"], + outs = ["expected_runtimes_manifest_workspace.bzl"], + cmd = "$(execpath //python/private:sync_runtimes_manifest_workspace) $(location //python/private:runtimes_manifest.txt) $@", + tools = ["//python/private:sync_runtimes_manifest_workspace"], +) + +diff_test( + name = "runtimes_manifest_workspace_sync_test", + file1 = "//python/private:runtimes_manifest_workspace.bzl", + file2 = ":expected_runtimes_manifest_workspace.bzl", +) diff --git a/tests/toolchains/custom_platform_toolchain_test.py b/tests/toolchains/custom_platform_toolchain_test.py index d6c083a6a2..2769d4bcf9 100644 --- a/tests/toolchains/custom_platform_toolchain_test.py +++ b/tests/toolchains/custom_platform_toolchain_test.py @@ -3,12 +3,12 @@ class VerifyCustomPlatformToolchainTest(unittest.TestCase): - def test_custom_platform_interpreter_used(self): - # We expect the repo name, and thus path, to have the - # platform name in it. - self.assertIn("linux-x86-install-only-stripped", sys._base_executable) - print(sys._base_executable) + # For lack of a better option, check the version. Identifying the + self.assertEqual( + "3.13.1", + f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}", + ) if __name__ == "__main__": diff --git a/tests/toolchains/defs.bzl b/tests/toolchains/defs.bzl index 25863d18c4..fb4b3beb94 100644 --- a/tests/toolchains/defs.bzl +++ b/tests/toolchains/defs.bzl @@ -57,4 +57,5 @@ def define_toolchain_tests(name): deps = ["//python/runfiles"], data = ["//tests/support:current_build_settings"], target_compatible_with = select(target_compatible_with), + size = "large", ) diff --git a/tests/toolchains/multi_platform_resolution/BUILD.bazel b/tests/toolchains/multi_platform_resolution/BUILD.bazel new file mode 100644 index 0000000000..35f18e98ed --- /dev/null +++ b/tests/toolchains/multi_platform_resolution/BUILD.bazel @@ -0,0 +1,3 @@ +load(":resolution_tests.bzl", "resolution_test_suite") + +resolution_test_suite(name = "resolution_tests") diff --git a/tests/toolchains/multi_platform_resolution/resolution_tests.bzl b/tests/toolchains/multi_platform_resolution/resolution_tests.bzl new file mode 100644 index 0000000000..a48d815789 --- /dev/null +++ b/tests/toolchains/multi_platform_resolution/resolution_tests.bzl @@ -0,0 +1,177 @@ +"""Tests to verify toolchain resolution of different config variants. + +NOTE: This test relies on the project toolchain configuration. This is +intentional because it wants to verify that, using the toolchains as +rules_python configures them, the different implementations can be used +by setting the appropriate flags. +""" + +load("@bazel_skylib//lib:structs.bzl", "structs") +load("@rules_testing//lib:analysis_test.bzl", "analysis_test") +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("//python:versions.bzl", "TOOL_VERSIONS") +load("//python/private:common_labels.bzl", "labels") # buildifier: disable=bzl-visibility +load("//python/private:toolchain_types.bzl", "TARGET_TOOLCHAIN_TYPE") # buildifier: disable=bzl-visibility +load("//python/private:version.bzl", "version") # buildifier: disable=bzl-visibility +load("//tests/support/platforms:platforms.bzl", "platform_targets") + +_PLATFORM_TARGET_MAP = { + "linux": { + "aarch64": platform_targets.LINUX_AARCH64, + "x86_64": platform_targets.LINUX_X86_64, + }, + "osx": { + "aarch64": platform_targets.MAC_AARCH64, + "x86_64": platform_targets.MAC_X86_64, + }, + "windows": { + "aarch64": platform_targets.WINDOWS_AARCH64, + "x86_64": platform_targets.WINDOWS_X86_64, + }, +} +_PLATFORM_TRIPLES = { + ("linux", "glibc"): "unknown_linux_gnu", + ("linux", "musl"): "unknown_linux_musl", + "osx": "apple_darwin", + "windows": "pc_windows_msvc", +} + +_ResolvedToolchainsInfo = provider( + doc = "Tell what toolchain was found", + fields = { + "target": "ToolchainInfo for //python:toolchain_type", + }, +) + +def _current_toolchain_impl(ctx): + # todo: also return current settings for various config flags + # to help identify state + return [_ResolvedToolchainsInfo( + target = ctx.toolchains[TARGET_TOOLCHAIN_TYPE], + )] + +_current_toolchain = rule( + implementation = _current_toolchain_impl, + toolchains = [ + TARGET_TOOLCHAIN_TYPE, + ], +) + +def _platform(os, arch, libc, *, ft): + if os == "linux": + platform_triple = _PLATFORM_TRIPLES[os, libc] + else: + platform_triple = _PLATFORM_TRIPLES[os] + return struct( + arch = arch, + freethreaded = ft, + libc = libc, + os = os, + platform_triple = platform_triple, + platform_target = _PLATFORM_TARGET_MAP[os][arch], + ) + +# There's many exceptions to the full `os x arch x libc x threading` matrix, +# so just list the specific combinations that are supported. +# We also omit some more esoteric archs to reduce the matrix size (and +# thus how many runtimes get downloaded). +# The important quality is to have at least 2 for every dimension +_PLATFORMS = [ + _platform("linux", "aarch64", "glibc", ft = "no"), + _platform("linux", "aarch64", "glibc", ft = "yes"), + _platform("linux", "x86_64", "glibc", ft = "no"), + _platform("linux", "x86_64", "glibc", ft = "yes"), + _platform("linux", "x86_64", "musl", ft = "no"), + _platform("osx", "aarch64", None, ft = "no"), + _platform("osx", "aarch64", None, ft = "yes"), + _platform("osx", "x86_64", None, ft = "no"), + _platform("osx", "x86_64", None, ft = "yes"), + _platform("windows", "aarch64", None, ft = "no"), + _platform("windows", "aarch64", None, ft = "yes"), + _platform("windows", "x86_64", None, ft = "no"), + _platform("windows", "x86_64", None, ft = "yes"), +] + +def _compute_runtimes(): + runtimes = [] + + # Limit to the two most recent versions. This helps ensure that multiple + # versions are matching correctly. Limit to two because the disk/download + # isn't worth the marginal coverage improvment. + selected_versions = sorted( + TOOL_VERSIONS.keys(), + key = lambda v: version.parse(v).key(), + )[-2:] + + for python_version in selected_versions: + for platform in _PLATFORMS: + runtimes.append(struct( + name = "{python_version}_{arch}_{triple}{threading}".format( + python_version = python_version.replace(".", "_"), + arch = platform.arch, + triple = platform.platform_triple, + threading = "_freethreaded" if platform.freethreaded == "yes" else "", + ), + python_version = python_version, + **structs.to_dict(platform) + )) + + return sorted(runtimes, key = lambda v: v.name) + +def _test_toolchains_impl(env, target): + target_tc = target[_ResolvedToolchainsInfo].target + toolchain_str = str(target_tc.toolchain_label).replace("-", "_") + env.expect.that_str(toolchain_str).contains(env.ctx.attr.expected_toolchain_name) + +def _test_toolchains(name): + _current_toolchain( + name = name + "_current_toolchain", + ) + test_names = [] + for runtime in _compute_runtimes(): + test_name = "test_{}".format(runtime.name) + test_names.append(test_name) + config_settings = { + "//command_line_option:platforms": [runtime.platform_target], + labels.VISIBLE_FOR_TESTING: True, + labels.PY_FREETHREADED: runtime.freethreaded, + labels.PYTHON_VERSION: runtime.python_version, + } + if runtime.libc: + config_settings[labels.PY_LINUX_LIBC] = runtime.libc + + analysis_test( + name = test_name, + target = name + "_current_toolchain", + impl = _test_toolchains_impl, + config_settings = config_settings, + attrs = {"expected_toolchain_name": attr.string()}, + attr_values = { + "expected_toolchain_name": runtime.name, + # A lot of tests are generated, so set tags to make selecting + # subsets easier + "tags": [ + "python-version={}".format(runtime.python_version), + "libc={}".format(runtime.libc), + "freethreaded={}".format(runtime.freethreaded), + "os={}".format(runtime.os), + "arch={}".format(runtime.arch), + ], + }, + ) + + # We have to return a target for `name`. + native.test_suite( + name = name, + tests = test_names, + ) + +_tests = [ + _test_toolchains, +] + +def resolution_test_suite(name): + test_suite( + name = name, + tests = _tests, + ) diff --git a/tests/toolchains/transitions/transitions_tests.bzl b/tests/toolchains/transitions/transitions_tests.bzl index 0cd79b373b..81ce1e68cc 100644 --- a/tests/toolchains/transitions/transitions_tests.bzl +++ b/tests/toolchains/transitions/transitions_tests.bzl @@ -14,11 +14,10 @@ "" -load("@pythons_hub//:versions.bzl", "DEFAULT_PYTHON_VERSION", "MINOR_MAPPING") +load("@pythons_hub//:versions.bzl", "DEFAULT_PYTHON_VERSION", "MINOR_MAPPING", "PYTHON_VERSIONS") load("@rules_testing//lib:analysis_test.bzl", "analysis_test") load("@rules_testing//lib:test_suite.bzl", "test_suite") load("@rules_testing//lib:util.bzl", rt_util = "util") -load("//python:versions.bzl", "TOOL_VERSIONS") load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility load("//python/private:common_labels.bzl", "labels") # buildifier: disable=bzl-visibility load("//python/private:full_version.bzl", "full_version") # buildifier: disable=bzl-visibility @@ -142,7 +141,7 @@ def _test_full_version(name): name = name, tests = { v.replace(".", "_"): (v, v) - for v in TOOL_VERSIONS + for v in PYTHON_VERSIONS }, ) diff --git a/tests/tools/private/release/BUILD.bazel b/tests/tools/private/release/BUILD.bazel index 9f3bc0542a..633cfa328f 100644 --- a/tests/tools/private/release/BUILD.bazel +++ b/tests/tools/private/release/BUILD.bazel @@ -1,10 +1,132 @@ -load("@rules_python//python:defs.bzl", "py_test") +load("@rules_python//python:defs.bzl", "py_library", "py_test") + +py_library( + name = "release_test_helper", + srcs = ["release_test_helper.py"], + deps = [ + "//tools/private/release:release_lib", + ], +) + +py_test( + name = "add_backports_test", + srcs = ["add_backports_test.py"], + deps = [ + ":release_test_helper", + "//tools/private/release:release_lib", + ], +) + +py_test( + name = "changelog_news_test", + srcs = ["changelog_news_test.py"], + deps = [ + ":release_test_helper", + "//tools/private/release:release_lib", + ], +) + +py_test( + name = "complete_sync_changelog_test", + srcs = ["complete_sync_changelog_test.py"], + deps = [ + ":release_test_helper", + "//tools/private/release:release_lib", + ], +) + +py_test( + name = "create_rc_test", + srcs = ["create_rc_test.py"], + deps = [ + ":release_test_helper", + "//tools/private/release:release_lib", + ], +) + +py_test( + name = "create_release_branch_test", + srcs = ["create_release_branch_test.py"], + deps = [ + ":release_test_helper", + "//tools/private/release:release_lib", + ], +) + +py_test( + name = "gh_test", + srcs = ["gh_test.py"], + deps = [ + "//tools/private/release:release_lib", + ], +) + +py_test( + name = "git_test", + srcs = ["git_test.py"], + deps = [ + "//tools/private/release:release_lib", + ], +) + +py_test( + name = "on_pr_merged_test", + srcs = ["on_pr_merged_test.py"], + deps = [ + ":release_test_helper", + "//tools/private/release:release_lib", + ], +) + +py_test( + name = "prepare_test", + srcs = ["prepare_test.py"], + deps = [ + ":release_test_helper", + "//tools/private/release:release_lib", + ], +) + +py_test( + name = "process_backports_test", + srcs = ["process_backports_test.py"], + deps = [ + ":release_test_helper", + "//tools/private/release:release_lib", + ], +) + +py_test( + name = "promote_rc_test", + srcs = ["promote_rc_test.py"], + deps = [ + ":release_test_helper", + "//tools/private/release:release_lib", + ], +) + +py_test( + name = "release_issue_test", + srcs = ["release_issue_test.py"], + deps = [ + "//tools/private/release:release_lib", + ], +) py_test( name = "release_test", srcs = ["release_test.py"], deps = [ - "//tools/private/release", + "//tools/private/release:release_lib", + ], +) + +py_test( + name = "utils_test", + srcs = ["utils_test.py"], + deps = [ + ":release_test_helper", + "//tools/private/release:release_lib", "@dev_pip//packaging", ], ) diff --git a/tests/tools/private/release/add_backports_test.py b/tests/tools/private/release/add_backports_test.py new file mode 100644 index 0000000000..75729e87fe --- /dev/null +++ b/tests/tools/private/release/add_backports_test.py @@ -0,0 +1,107 @@ +import argparse +import unittest +from unittest.mock import patch + +from tests.tools.private.release.release_test_helper import _mock_git_and_gh +from tools.private.release.add_backports import AddBackports + + +class CmdAddBackportsTest(unittest.TestCase): + def setUp(self): + _mock_git_and_gh(self) + self.addCleanup(patch.stopall) + self.mock_gh.resolve_pr_number.side_effect = lambda x: int( + x.lstrip("#").split("/")[-1] + ) + + def test_add_backports_explicit_issue(self): + args = argparse.Namespace(issue=123, prs=["124", "125"]) + self.mock_gh.get_issue_body.return_value = """ +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch +- [ ] Tag Final + +## Backports +""" + result = AddBackports(args, self.mock_gh).run() + + self.assertEqual(result, 0) + self.mock_gh.get_issue_body.assert_called_once_with(123) + self.mock_gh.update_issue_body.assert_called_once() + call_args = self.mock_gh.update_issue_body.call_args[0] + self.assertEqual(call_args[0], 123) + self.assertIn("- [ ] #124", call_args[1]) + self.assertIn("- [ ] #125", call_args[1]) + # Should also auto-add Tag RC0 + self.assertIn("- [ ] Tag RC0", call_args[1]) + self.assertIn("- [ ] Sync Changelog #124", call_args[1]) + self.assertIn("- [ ] Sync Changelog #125", call_args[1]) + + def test_add_backports_auto_discover_success(self): + args = argparse.Namespace(issue=None, prs=["124"]) + self.mock_gh.get_open_tracking_issues.return_value = [ + {"number": 456, "title": "Release 2.1.0", "url": "http://..."} + ] + self.mock_gh.get_issue_body.return_value = """ +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch +- [ ] Tag Final + +## Backports +""" + result = AddBackports(args, self.mock_gh).run() + + self.assertEqual(result, 0) + self.mock_gh.get_open_tracking_issues.assert_called_once() + self.mock_gh.get_issue_body.assert_called_once_with(456) + self.mock_gh.update_issue_body.assert_called_once_with(456, unittest.mock.ANY) + + def test_add_backports_auto_discover_no_issues(self): + args = argparse.Namespace(issue=None, prs=["124"]) + self.mock_gh.get_open_tracking_issues.return_value = [] + + result = AddBackports(args, self.mock_gh).run() + + self.assertEqual(result, 1) + self.mock_gh.get_open_tracking_issues.assert_called_once() + self.mock_gh.get_issue_body.assert_not_called() + + def test_add_backports_auto_discover_multiple_issues(self): + args = argparse.Namespace(issue=None, prs=["124"]) + self.mock_gh.get_open_tracking_issues.return_value = [ + {"number": 456, "title": "Release 2.1.0", "url": "http://..."}, + {"number": 789, "title": "Release 2.2.0", "url": "http://..."}, + ] + + result = AddBackports(args, self.mock_gh).run() + + self.assertEqual(result, 1) + self.mock_gh.get_open_tracking_issues.assert_called_once() + self.mock_gh.get_issue_body.assert_not_called() + + def test_add_backports_no_auto_add_rc_if_pending(self): + args = argparse.Namespace(issue=123, prs=["124"]) + self.mock_gh.get_issue_body.return_value = """ +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch +- [ ] Tag RC0 +- [ ] Tag Final + +## Backports +""" + result = AddBackports(args, self.mock_gh).run() + + self.assertEqual(result, 0) + self.mock_gh.update_issue_body.assert_called_once() + call_args = self.mock_gh.update_issue_body.call_args[0] + self.assertNotIn("Tag RC1", call_args[1]) + # Tag RC0 should still be there + self.assertIn("- [ ] Tag RC0", call_args[1]) + self.assertIn("- [ ] Sync Changelog #124", call_args[1]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/tools/private/release/changelog_news_test.py b/tests/tools/private/release/changelog_news_test.py new file mode 100644 index 0000000000..83ab2cc959 --- /dev/null +++ b/tests/tools/private/release/changelog_news_test.py @@ -0,0 +1,524 @@ +import pathlib +import unittest +from unittest.mock import patch + +from tests.tools.private.release.release_test_helper import TempDirTestCase +from tools.private.release import changelog_news + + +class ChangelogNewsTest(TempDirTestCase): + def test_update_changelog_with_news(self): + # Arrange + changelog = """# Changelog + +{#unreleased} +## Unreleased + +[unreleased]: https://github.com/bazel-contrib/rules_python/releases/tag/unreleased + +Unreleased changes are tracked as individual files in the [news/](./news) +directory, or view the [latest generated +changelog](https://rules-python.readthedocs.io/en/latest/changelog.html). + +{#v2-0-2} +## [2.0.2] - 2026-05-14 + +[2.0.2]: https://github.com/bazel-contrib/rules_python/releases/tag/2.0.2 + +{#v2-0-2-added} +### Added +* (toolchains) Some older change. +""" + changelog_path = self.tmpdir / "CHANGELOG.md" + changelog_path.write_text(changelog) + + news_dir = self.tmpdir / "news" + news_dir.mkdir() + + # Create news files + (news_dir / "123.fixed.md").write_text("Fixed a bug in the compiler") + # Test that it handles prefixing "* " if not present + (news_dir / "456.added.md").write_text("* Added a new feature for Python 3.13") + # Empty file should be ignored + (news_dir / "789.changed.md").write_text("") + # Invalid name should be ignored + (news_dir / "invalid_name.md").write_text("Should be ignored") + + # Act + changelog_news.update_changelog( + "3.0.0", + "2026-06-16", + changelog_path=changelog_path, + news_dir=news_dir, + ) + + # Assert + # 1. News files matching the pattern should be deleted (even empty ones) + self.assertFalse((news_dir / "123.fixed.md").exists()) + self.assertFalse((news_dir / "456.added.md").exists()) + self.assertFalse((news_dir / "789.changed.md").exists()) + # Invalid name does not match pattern -> NOT deleted + self.assertTrue((news_dir / "invalid_name.md").exists()) + + new_content = changelog_path.read_text() + + # 2. A fresh active Unreleased section should be present + self.assertIn("{#unreleased}", new_content) + self.assertIn("## Unreleased", new_content) + self.assertIn( + "Unreleased changes are tracked as individual files in the [news/](./news)\n" + "directory, or view the [latest generated\n" + "changelog](https://rules-python.readthedocs.io/en/latest/changelog.html).", + new_content, + ) + + # 3. The new release section should be present + self.assertIn("{#v3-0-0}", new_content) + self.assertIn("## [3.0.0] - 2026-06-16", new_content) + self.assertIn( + "[3.0.0]: https://github.com/bazel-contrib/rules_python/releases/tag/3.0.0", + new_content, + ) + + # 4. Correct categories and content + self.assertIn( + "{#v3-0-0-fixed}\n### Fixed\n* Fixed a bug in the compiler", + new_content, + ) + self.assertIn( + "{#v3-0-0-added}\n### Added\n* Added a new feature for Python 3.13", + new_content, + ) + + # 5. Omitted categories should NOT be present in the new release + self.assertNotIn("{#v3-0-0-removed}", new_content) + self.assertNotIn("{#v3-0-0-changed}", new_content) + + # 6. Old release should still be there + self.assertIn("{#v2-0-2}", new_content) + self.assertIn("## [2.0.2] - 2026-05-14", new_content) + + def test_update_changelog_sorting(self): + # Arrange + changelog = """# Changelog + +{#unreleased} +## Unreleased + +[unreleased]: https://github.com/bazel-contrib/rules_python/releases/tag/unreleased + +Unreleased changes are tracked as individual files in the [news/](./news) +directory, or view the [latest generated +changelog](https://rules-python.readthedocs.io/en/latest/changelog.html). + +{#v2-0-2} +## [2.0.2] - 2026-05-14 + +[2.0.2]: https://github.com/bazel-contrib/rules_python/releases/tag/2.0.2 + +{#v2-0-2-added} +### Added +* (toolchains) Some older change. +""" + changelog_path = self.tmpdir / "CHANGELOG.md" + changelog_path.write_text(changelog) + + news_dir = self.tmpdir / "news" + news_dir.mkdir() + + # Create news files with different sub-categories and some without + (news_dir / "1.fixed.md").write_text("* (zebra) Zebra fix") + (news_dir / "2.fixed.md").write_text("* (apple) Apple fix") + (news_dir / "3.fixed.md").write_text("No subcategory B") + (news_dir / "4.fixed.md").write_text("* (apple) Another apple fix") + (news_dir / "5.fixed.md").write_text("No subcategory A") + + # Act + changelog_news.update_changelog( + "3.0.0", + "2026-06-16", + changelog_path=changelog_path, + news_dir=news_dir, + ) + + # Assert + new_content = changelog_path.read_text() + + # Expected order in Fixed section: + # 1. No subcategory A + # 2. No subcategory B + # 3. (apple) Another apple fix + # 4. (apple) Apple fix + # 5. (zebra) Zebra fix + + expected_fixed_section = ( + "### Fixed\n" + "* No subcategory A\n" + "* No subcategory B\n" + "* (apple) Another apple fix\n" + "* (apple) Apple fix\n" + "* (zebra) Zebra fix\n" + ) + + self.assertIn(expected_fixed_section, new_content) + + def test_update_changelog_read_failure(self): + # Arrange + original_read_text = pathlib.Path.read_text + + with patch("pathlib.Path.read_text", autospec=True) as mock_read_text: + + def side_effect(path_self, *args, **kwargs): + if "bad_file.fixed.md" in str(path_self): + raise IOError("Simulated read error") + return original_read_text(path_self, *args, **kwargs) + + mock_read_text.side_effect = side_effect + + changelog = """# Changelog + +{#unreleased} +## Unreleased + +[unreleased]: https://github.com/bazel-contrib/rules_python/releases/tag/unreleased + +Unreleased changes are tracked as individual files in the [news/](./news) +directory, or view the [latest generated +changelog](https://rules-python.readthedocs.io/en/latest/changelog.html). + +{#v2-0-2} +## [2.0.2] - 2026-05-14 + +[2.0.2]: https://github.com/bazel-contrib/rules_python/releases/tag/2.0.2 + +{#v2-0-2-added} +### Added +* (toolchains) Some older change. +""" + changelog_path = self.tmpdir / "CHANGELOG.md" + changelog_path.write_text(changelog) + + news_dir = self.tmpdir / "news" + news_dir.mkdir() + + # Create the bad file (must exist so it is found by iterdir) + bad_file = news_dir / "bad_file.fixed.md" + bad_file.write_text("some content that won't be read") + + # Create a good file too + good_file = news_dir / "good_file.fixed.md" + good_file.write_text("* (sub) Good fix") + + # Act & Assert + # It should raise IOError + with self.assertRaises(IOError): + changelog_news.update_changelog( + "3.0.0", + "2026-06-16", + changelog_path=changelog_path, + news_dir=news_dir, + ) + + # Both files should still exist (no deletion on failure!) + self.assertTrue(bad_file.exists()) + self.assertTrue(good_file.exists()) + + # Changelog should not be modified + new_content = changelog_path.read_text() + self.assertEqual(changelog, new_content) + + def test_update_changelog_merge_existing(self): + # Arrange + changelog = """# Changelog + +{#unreleased} +## Unreleased + +[unreleased]: https://github.com/bazel-contrib/rules_python/releases/tag/unreleased + +Unreleased changes are tracked as individual files in the [news/](./news) +directory, or view the [latest generated +changelog](https://rules-python.readthedocs.io/en/latest/changelog.html). + +{#v2-0-3} +## [2.0.3] - 2026-06-15 + +[2.0.3]: https://github.com/bazel-contrib/rules_python/releases/tag/2.0.3 + +{#v2-0-3-fixed} +### Fixed +* (pypi) Old fix + multi-line detail + * nested bullet item +* (pypi) Z old fix +""" + changelog_path = self.tmpdir / "CHANGELOG.md" + changelog_path.write_text(changelog) + + news_dir = self.tmpdir / "news" + news_dir.mkdir() + + # Create news files to merge + # 1. New fix in same category (should merge and sort) + (news_dir / "1.fixed.md").write_text("(pypi) New fix") + # 2. New entry in new category (should create category) + (news_dir / "2.added.md").write_text("(toolchains) New feature") + + # Act + changelog_news.update_changelog( + "2.0.3", + "2026-06-15", + changelog_path=changelog_path, + news_dir=news_dir, + ) + + # Assert + # News files should be deleted + self.assertFalse((news_dir / "1.fixed.md").exists()) + self.assertFalse((news_dir / "2.added.md").exists()) + + new_content = changelog_path.read_text() + + # Expected merged and sorted Fixed section: + # 1. (pypi) New fix (New < Old) + # 2. (pypi) Old fix (with its multi-line detail!) + # 3. (pypi) Z old fix + expected_fixed_section = ( + "### Fixed\n" + "* (pypi) New fix\n" + "* (pypi) Old fix\n" + " multi-line detail\n" + " * nested bullet item\n" + "* (pypi) Z old fix\n" + ) + self.assertIn(expected_fixed_section, new_content) + + # Expected created Added section: + expected_added_section = "### Added\n* (toolchains) New feature\n" + self.assertIn(expected_added_section, new_content) + + # Active Unreleased section should NOT be touched (should still be empty/pointing to news) + self.assertIn("Unreleased changes are tracked as individual files", new_content) + + def test_update_changelog_does_not_leak(self): + # Arrange + changelog = """# Changelog + +{#unreleased} +## Unreleased + +[unreleased]: https://github.com/bazel-contrib/rules_python/releases/tag/unreleased + +Unreleased changes are tracked as individual files in the [news/](./news) +directory, or view the [latest generated +changelog](https://rules-python.readthedocs.io/en/latest/changelog.html). + +{#v2-0-2} +## [2.0.2] - 2026-05-14 + +[2.0.2]: https://github.com/bazel-contrib/rules_python/releases/tag/2.0.2 + +This release body mentions the word unreleased and {#unreleased} anchor to test leaks. +""" + changelog_path = self.tmpdir / "CHANGELOG.md" + changelog_path.write_text(changelog) + + news_dir = self.tmpdir / "news" + news_dir.mkdir() + (news_dir / "1.fixed.md").write_text("Some fix") + + # Act + changelog_news.update_changelog( + "3.0.0", + "2026-06-16", + changelog_path=changelog_path, + news_dir=news_dir, + ) + + # Assert + new_content = changelog_path.read_text() + + # The 2.0.2 body should NOT be modified + self.assertIn( + "This release body mentions the word unreleased and {#unreleased} anchor to test leaks.", + new_content, + ) + + def test_update_changelog_empty_news(self): + # Arrange + changelog = """# Changelog + +{#unreleased} +## Unreleased + +[unreleased]: https://github.com/bazel-contrib/rules_python/releases/tag/unreleased + +Unreleased changes are tracked as individual files in the [news/](./news) +directory, or view the [latest generated +changelog](https://rules-python.readthedocs.io/en/latest/changelog.html). + +{#v2-0-2} +## [2.0.2] - 2026-05-14 + +[2.0.2]: https://github.com/bazel-contrib/rules_python/releases/tag/2.0.2 + +{#v2-0-2-added} +### Added +* (toolchains) Some older change. +""" + changelog_path = self.tmpdir / "CHANGELOG.md" + changelog_path.write_text(changelog) + + news_dir = self.tmpdir / "news" + news_dir.mkdir() + + # Act + changelog_news.update_changelog( + "3.0.0", + "2026-06-16", + changelog_path=changelog_path, + news_dir=news_dir, + ) + + # Assert + new_content = changelog_path.read_text() + + # The new release section should be present and contain "No notable changes." + self.assertIn("{#v3-0-0}", new_content) + self.assertIn("## [3.0.0] - 2026-06-16", new_content) + self.assertIn( + "[3.0.0]: https://github.com/bazel-contrib/rules_python/releases/tag/3.0.0", + new_content, + ) + self.assertIn("No notable changes.", new_content) + + # Verify that we didn't accidentally create any categories + self.assertNotIn("{#v3-0-0-fixed}", new_content) + self.assertNotIn("{#v3-0-0-added}", new_content) + + def test_update_changelog_selective_news_files(self): + # Arrange + changelog = """# Changelog + +{#unreleased} +## Unreleased + +[unreleased]: https://github.com/bazel-contrib/rules_python/releases/tag/unreleased + +{#v2-0-2} +## [2.0.2] - 2026-05-14 + +[2.0.2]: https://github.com/bazel-contrib/rules_python/releases/tag/2.0.2 +""" + changelog_path = self.tmpdir / "CHANGELOG.md" + changelog_path.write_text(changelog) + + news_dir = self.tmpdir / "news" + news_dir.mkdir() + + # Create news files + (news_dir / "123.fixed.md").write_text("Fix A") + (news_dir / "456.fixed.md").write_text("Fix B") + + # Act: Only process 123.fixed.md + changelog_news.update_changelog( + "2.0.3", + "2026-06-16", + changelog_path=changelog_path, + news_dir=news_dir, + news_files=[news_dir / "123.fixed.md"], + ) + + # Assert + # 1. Only 123.fixed.md should be deleted + self.assertFalse((news_dir / "123.fixed.md").exists()) + self.assertTrue((news_dir / "456.fixed.md").exists()) + + new_content = changelog_path.read_text() + + # 2. Only Fix A should be in the changelog + self.assertIn("Fix A", new_content) + self.assertNotIn("Fix B", new_content) + + def test_update_changelog_insertion_point(self): + # Arrange + changelog = """# Changelog + +{#unreleased} +## Unreleased + +[unreleased]: https://github.com/bazel-contrib/rules_python/releases/tag/unreleased + +{#v2-2-0} +## [2.2.0] - 2026-06-30 + +[2.2.0]: https://github.com/bazel-contrib/rules_python/releases/tag/2.2.0 + +{#v2-0-0} +## [2.0.0] - 2026-04-09 + +[2.0.0]: https://github.com/bazel-contrib/rules_python/releases/tag/2.0.0 +""" + changelog_path = self.tmpdir / "CHANGELOG.md" + changelog_path.write_text(changelog) + + news_dir = self.tmpdir / "news" + news_dir.mkdir() + (news_dir / "123.fixed.md").write_text("Fix in 2.1.0") + + # Act: Insert 2.1.0 + changelog_news.update_changelog( + "2.1.0", + "2026-06-17", + changelog_path=changelog_path, + news_dir=news_dir, + ) + + # Assert + new_content = changelog_path.read_text() + + # Verify 2.1.0 is inserted BEFORE 2.0.0 but AFTER 2.2.0 + idx_2_2_0 = new_content.index("{#v2-2-0}") + idx_2_1_0 = new_content.index("{#v2-1-0}") + idx_2_0_0 = new_content.index("{#v2-0-0}") + + self.assertTrue(idx_2_2_0 < idx_2_1_0 < idx_2_0_0) + self.assertIn("Fix in 2.1.0", new_content) + + def test_update_changelog_insertion_point_too_small(self): + # Arrange + changelog = """# Changelog + +{#unreleased} +## Unreleased + +[unreleased]: https://github.com/bazel-contrib/rules_python/releases/tag/unreleased + +{#v2-0-0} +## [2.0.0] - 2026-04-09 + +[2.0.0]: https://github.com/bazel-contrib/rules_python/releases/tag/2.0.0 +""" + changelog_path = self.tmpdir / "CHANGELOG.md" + changelog_path.write_text(changelog) + + news_dir = self.tmpdir / "news" + news_dir.mkdir() + (news_dir / "123.fixed.md").write_text("Fix in 1.0.0") + + # Act & Assert + with self.assertRaises(ValueError) as ctx: + changelog_news.update_changelog( + "1.0.0", + "2026-01-01", + changelog_path=changelog_path, + news_dir=news_dir, + ) + self.assertIn( + "Could not find a version in CHANGELOG.md smaller than 1.0.0", + str(ctx.exception), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/tools/private/release/complete_sync_changelog_test.py b/tests/tools/private/release/complete_sync_changelog_test.py new file mode 100644 index 0000000000..66c40c01d3 --- /dev/null +++ b/tests/tools/private/release/complete_sync_changelog_test.py @@ -0,0 +1,120 @@ +import argparse +import unittest +from unittest.mock import patch + +from tests.tools.private.release.release_test_helper import _mock_git_and_gh +from tools.private.release.complete_sync_changelog import CompleteSyncChangelog + + +class CompleteSyncChangelogTest(unittest.TestCase): + def setUp(self): + _mock_git_and_gh(self) + self.addCleanup(patch.stopall) + + # Dynamic mock for issue body + self.issue_body = "" + + def mock_get_body(issue_num): + return self.issue_body + + def mock_update_body(issue_num, body): + self.issue_body = body + + self.mock_gh.get_issue_body.side_effect = mock_get_body + self.mock_gh.update_issue_body.side_effect = mock_update_body + + def test_complete_sync_changelog_success(self): + args = argparse.Namespace(pr=999) + self.mock_gh.get_pr_info.return_value = { + "state": "MERGED", + "body": "Updates CHANGELOG.md\n\nRelease-Tracking-Issue: #123", + "mergeCommit": {"oid": "abcdef1234567890"}, + } + self.issue_body = """ +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch +- [ ] Sync Changelog #124 | status=pending pr=#999 +- [ ] Sync Changelog #125 | status=pending pr=#999 +- [ ] Sync Changelog #126 | status=pending pr=#888 +- [ ] Tag Final + +## Backports +""" + result = CompleteSyncChangelog(args, self.mock_gh).run() + + self.assertEqual(result, 0) + self.mock_gh.get_pr_info.assert_called_once_with(999) + self.mock_gh.get_issue_body.assert_called_once_with(123) + self.mock_gh.update_issue_body.assert_called_once() + + # Check that only tasks pointing to #999 were marked checked=True and status=done + self.assertIn( + "- [x] Sync Changelog #124 | status=done pr=#999 commit= abcdef12", + self.issue_body, + ) + self.assertIn( + "- [x] Sync Changelog #125 | status=done pr=#999 commit= abcdef12", + self.issue_body, + ) + # Task pointing to #888 should remain unchanged + self.assertIn( + "- [ ] Sync Changelog #126 | status=pending pr=#888", + self.issue_body, + ) + + def test_complete_sync_changelog_not_merged(self): + args = argparse.Namespace(pr=999) + self.mock_gh.get_pr_info.return_value = { + "state": "OPEN", + "body": "Updates CHANGELOG.md\n\nRelease-Tracking-Issue: #123", + } + + result = CompleteSyncChangelog(args, self.mock_gh).run() + + self.assertEqual(result, 1) + self.mock_gh.get_pr_info.assert_called_once_with(999) + self.mock_gh.update_issue_body.assert_not_called() + + def test_complete_sync_changelog_missing_tracking_issue_link(self): + args = argparse.Namespace(pr=999) + self.mock_gh.get_pr_info.return_value = { + "state": "MERGED", + "body": "Updates CHANGELOG.md without tracking issue link", + "mergeCommit": {"oid": "abcdef1234567890"}, + } + + result = CompleteSyncChangelog(args, self.mock_gh).run() + + self.assertEqual(result, 1) + self.mock_gh.get_pr_info.assert_called_once_with(999) + self.mock_gh.update_issue_body.assert_not_called() + + def test_complete_sync_changelog_no_matching_tasks(self): + args = argparse.Namespace(pr=999) + self.mock_gh.get_pr_info.return_value = { + "state": "MERGED", + "body": "Updates CHANGELOG.md\n\nRelease-Tracking-Issue: #123", + "mergeCommit": {"oid": "abcdef1234567890"}, + } + # Checklist has no tasks pointing to #999 + self.issue_body = """ +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch +- [ ] Sync Changelog #124 | status=pending pr=#888 +- [ ] Tag Final + +## Backports +""" + result = CompleteSyncChangelog(args, self.mock_gh).run() + + # Should log warning but return 0 (success/noop) + self.assertEqual(result, 0) + self.mock_gh.get_pr_info.assert_called_once_with(999) + self.mock_gh.get_issue_body.assert_called_once_with(123) + self.mock_gh.update_issue_body.assert_not_called() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/tools/private/release/create_rc_test.py b/tests/tools/private/release/create_rc_test.py new file mode 100644 index 0000000000..8e4ed101e4 --- /dev/null +++ b/tests/tools/private/release/create_rc_test.py @@ -0,0 +1,406 @@ +import argparse +import os +import pathlib +import tempfile +import unittest +from unittest.mock import MagicMock, call, patch + +from tests.tools.private.release.release_test_helper import _mock_git_and_gh +from tools.private.release.create_rc import CreateRc + + +class CmdCreateRcTest(unittest.TestCase): + def setUp(self): + _mock_git_and_gh(self) + + def test_create_rc_success_first_rc(self): + # Arrange + args = MagicMock(issue=123, remote="my-remote") + self.mock_gh.get_issue_title.return_value = "Release 2.0.0" + self.mock_gh.get_issue_body.return_value = """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12 +- [ ] Tag RC0 | status=pending +""" + self.mock_git.get_remote_tags.return_value = [] + self.mock_git.get_commit_sha.return_value = "1234567890" + + # Act + with tempfile.TemporaryDirectory() as tmpdir: + github_output_file = pathlib.Path(tmpdir) / "github_output" + with patch.dict(os.environ, {"GITHUB_OUTPUT": str(github_output_file)}): + result = CreateRc(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 0) + self.assertTrue(github_output_file.exists()) + self.assertEqual(github_output_file.read_text(), "tag_name=2.0.0-rc0\n") + self.mock_git.fetch.assert_has_calls( + [call("my-remote"), call("my-remote", tags=True, force=True)] + ) + self.mock_git.checkout.assert_not_called() + self.mock_git.tag.assert_called_once_with("2.0.0-rc0", "my-remote/release/2.0") + self.mock_git.push.assert_called_once_with("my-remote", "2.0.0-rc0") + self.mock_git.get_commit_sha.assert_called_once_with("my-remote/release/2.0") + + self.mock_gh.update_issue_body.assert_called_once() + call_args = self.mock_gh.update_issue_body.call_args[0] + self.assertEqual(call_args[0], 123) + self.assertIn("tag=2.0.0-rc0", call_args[1]) + self.assertIn("commit= 12345678", call_args[1]) + + self.mock_gh.post_issue_comment.assert_called_once() + comment_call_args = self.mock_gh.post_issue_comment.call_args[0] + self.assertEqual(comment_call_args[0], 123) + self.assertIn( + "**New Release Candidate Tagged!** šŸšŸŒæ", + comment_call_args[1], + ) + self.assertIn( + "tagged on branch [`release/2.0`](https://github.com/bazel-contrib/rules_python/tree/release/2.0)", + comment_call_args[1], + ) + self.assertIn( + "- [Github Release 2.0.0-rc0](https://github.com/bazel-contrib/rules_python/releases/tag/2.0.0-rc0)", + comment_call_args[1], + ) + self.assertIn( + "- [BCR Entry 2.0.0-rc0](https://registry.bazel.build/modules/rules_python/2.0.0-rc0)", + comment_call_args[1], + ) + self.assertIn( + "- [BCR PRs](https://github.com/bazelbuild/bazel-central-registry/pulls?q=is%3Apr+rules_python+2.0.0-rc0)", + comment_call_args[1], + ) + self.assertIn( + "- [Release workflow status](https://github.com/bazel-contrib/rules_python/actions/workflows/release_create_rc.yaml)", + comment_call_args[1], + ) + + def test_create_rc_success_with_run_id(self): + # Arrange + args = MagicMock(issue=123, remote="my-remote") + self.mock_gh.get_issue_title.return_value = "Release 2.0.0" + self.mock_gh.get_issue_body.return_value = """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12 +- [ ] Tag RC0 | status=pending +""" + self.mock_git.get_remote_tags.return_value = [] + self.mock_git.get_commit_sha.return_value = "1234567890" + + # Act + with patch.dict(os.environ, {"GITHUB_RUN_ID": "987654321"}): + result = CreateRc(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 0) + self.mock_gh.post_issue_comment.assert_called_once() + comment_call_args = self.mock_gh.post_issue_comment.call_args[0] + self.assertIn( + "- [Release workflow status](https://github.com/bazel-contrib/rules_python/actions/runs/987654321)", + comment_call_args[1], + ) + self.assertIn( + "tagged on branch [`release/2.0`](https://github.com/bazel-contrib/rules_python/tree/release/2.0)", + comment_call_args[1], + ) + + def test_create_rc_success_next_rc(self): + # Arrange + args = MagicMock(issue=123, remote="my-remote") + self.mock_gh.get_issue_title.return_value = "Release 2.0.0" + self.mock_gh.get_issue_body.return_value = """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12 +- [x] Tag RC0 | status=done tag=2.0.0-rc0 commit=abcdef12 +- [ ] Tag RC1 | status=pending +""" + self.mock_git.get_remote_tags.return_value = ["2.0.0-rc0"] + self.mock_git.get_commit_sha.return_value = "1234567890" + + # Act + result = CreateRc(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 0) + self.mock_git.fetch.assert_has_calls( + [call("my-remote"), call("my-remote", tags=True, force=True)] + ) + self.mock_git.checkout.assert_not_called() + self.mock_git.tag.assert_called_once_with("2.0.0-rc1", "my-remote/release/2.0") + self.mock_git.push.assert_called_once_with("my-remote", "2.0.0-rc1") + self.mock_git.get_commit_sha.assert_called_once_with("my-remote/release/2.0") + + self.mock_gh.update_issue_body.assert_called_once() + call_args = self.mock_gh.update_issue_body.call_args[0] + self.assertEqual(call_args[0], 123) + self.assertIn("tag=2.0.0-rc1", call_args[1]) + + self.mock_gh.post_issue_comment.assert_called_once() + comment_call_args = self.mock_gh.post_issue_comment.call_args[0] + self.assertEqual(comment_call_args[0], 123) + self.assertIn( + "**New Release Candidate Tagged!** šŸšŸŒæ", + comment_call_args[1], + ) + self.assertIn( + "tagged on branch [`release/2.0`](https://github.com/bazel-contrib/rules_python/tree/release/2.0)", + comment_call_args[1], + ) + self.assertIn( + "- [Github Release 2.0.0-rc1](https://github.com/bazel-contrib/rules_python/releases/tag/2.0.0-rc1)", + comment_call_args[1], + ) + self.assertIn( + "- [BCR Entry 2.0.0-rc1](https://registry.bazel.build/modules/rules_python/2.0.0-rc1)", + comment_call_args[1], + ) + self.assertIn( + "- [BCR PRs](https://github.com/bazelbuild/bazel-central-registry/pulls?q=is%3Apr+rules_python+2.0.0-rc1)", + comment_call_args[1], + ) + self.assertIn( + "- [Release workflow status](https://github.com/bazel-contrib/rules_python/actions/workflows/release_create_rc.yaml)", + comment_call_args[1], + ) + + def test_create_rc_gating_on_backports(self): + # Arrange + args = MagicMock(issue=123, remote="my-remote") + self.mock_gh.get_issue_title.return_value = "Release 2.0.0" + self.mock_gh.get_issue_body.return_value = """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12 +- [ ] Tag RC0 | status=pending + +## Backports +- [ ] #124 | status=pending +""" + # Act + result = CreateRc(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 1) + self.mock_git.tag.assert_not_called() + self.mock_git.push.assert_not_called() + + def test_create_rc_not_blocked_by_ignored_backports(self): + # Arrange + args = MagicMock(issue=123, remote="my-remote") + self.mock_gh.get_issue_title.return_value = "Release 2.0.0" + self.mock_gh.get_issue_body.return_value = """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12 +- [ ] Tag RC0 | status=pending + +## Backports +- [ ] #124 | status=ignore +""" + self.mock_git.get_remote_tags.return_value = [] + self.mock_git.get_commit_sha.return_value = "1234567890" + + # Act + result = CreateRc(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 0) + self.mock_git.tag.assert_called_once_with("2.0.0-rc0", "my-remote/release/2.0") + self.mock_git.push.assert_called_once_with("my-remote", "2.0.0-rc0") + + def test_create_rc_with_finished_backports(self): + # Arrange + args = MagicMock(issue=123, remote="my-remote") + self.mock_gh.get_issue_title.return_value = "Release 2.0.0" + self.mock_gh.get_issue_body.return_value = """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12 +- [ ] Tag RC0 | status=pending + +## Backports +- [x] #124 | status=done rc=rc0 commit=abcdef12 +""" + self.mock_git.get_remote_tags.return_value = [] + self.mock_git.get_commit_sha.return_value = "1234567890" + + # Act + result = CreateRc(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 0) + self.mock_git.tag.assert_called_once_with("2.0.0-rc0", "my-remote/release/2.0") + self.mock_git.push.assert_called_once_with("my-remote", "2.0.0-rc0") + + def test_create_rc_auto_add_task(self): + # Arrange + args = argparse.Namespace(issue=123, remote="my-remote") + self.mock_gh.get_issue_title.return_value = "Release 2.0.0" + self.mock_gh.get_issue_body.return_value = """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12 +- [x] Tag RC0 | status=done tag=2.0.0-rc0 commit=abcdef12 +- [ ] Tag Final +""" + self.mock_git.get_remote_tags.return_value = ["2.0.0-rc0"] + self.mock_git.get_commit_sha.return_value = "1234567890" + + # Act + result = CreateRc(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 0) + self.mock_git.tag.assert_called_once_with("2.0.0-rc1", "my-remote/release/2.0") + self.mock_git.push.assert_called_once_with("my-remote", "2.0.0-rc1") + + self.assertEqual(self.mock_gh.update_issue_body.call_count, 2) + call1_args = self.mock_gh.update_issue_body.call_args_list[0][0] + call2_args = self.mock_gh.update_issue_body.call_args_list[1][0] + + self.assertEqual(call1_args[0], 123) + self.assertIn("- [ ] Tag RC1", call1_args[1]) + self.assertIn( + "- [x] Tag RC0 | status=done tag=2.0.0-rc0 commit=abcdef12\n- [ ]" + " Tag RC1\n- [ ] Tag Final", + call1_args[1].strip(), + ) + + self.assertEqual(call2_args[0], 123) + self.assertIn( + "- [x] Tag RC1 | status=done tag=2.0.0-rc1 commit= 12345678", + call2_args[1], + ) + + @patch("tools.private.release.create_rc.ProcessBackports") + def test_create_rc_calls_process_backports(self, mock_pb_class): + # Arrange + mock_pb = mock_pb_class.return_value + mock_pb.run.return_value = 0 + + args = MagicMock(issue=123, remote="my-remote") + self.mock_gh.get_issue_title.return_value = "Release 2.0.0" + self.mock_gh.get_issue_body.return_value = """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12 +- [ ] Tag RC0 | status=pending +""" + self.mock_git.get_remote_tags.return_value = [] + self.mock_git.get_commit_sha.return_value = "1234567890" + + # Act + result = CreateRc(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 0) + mock_pb_class.assert_called_once() + called_args = mock_pb_class.call_args[0][0] + self.assertEqual(called_args.issue, 123) + self.assertEqual(called_args.remote, "my-remote") + self.assertFalse(called_args.dry_run) + self.assertIsNone(called_args.add) + self.assertIsNone(called_args.triggering_comment) + mock_pb.run.assert_called_once() + + @patch("tools.private.release.create_rc.ProcessBackports") + def test_create_rc_aborts_on_process_backports_failure(self, mock_pb_class): + # Arrange + mock_pb = mock_pb_class.return_value + mock_pb.run.return_value = 1 + + args = MagicMock(issue=123, remote="my-remote") + + # Act + result = CreateRc(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 1) + mock_pb_class.assert_called_once() + mock_pb.run.assert_called_once() + self.mock_gh.get_issue_body.assert_not_called() + self.mock_git.tag.assert_not_called() + + @patch("tools.private.release.create_rc.ProcessBackports") + def test_create_rc_failure_reacts_to_comment(self, mock_pb_class): + # Arrange + mock_pb = mock_pb_class.return_value + mock_pb.run.return_value = 1 # Simulate failure + + args = MagicMock(issue=123, remote="my-remote", triggering_comment=456) + + # Act + result = CreateRc(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 1) + self.mock_gh.add_comment_reaction.assert_called_once_with(456, "-1") + + @patch("tools.private.release.create_rc.ProcessBackports") + def test_create_rc_failure_no_comment_no_reaction(self, mock_pb_class): + # Arrange + mock_pb = mock_pb_class.return_value + mock_pb.run.return_value = 1 # Simulate failure + + args = MagicMock(issue=123, remote="my-remote", triggering_comment=None) + + # Act + result = CreateRc(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 1) + self.mock_gh.add_comment_reaction.assert_not_called() + + @patch("tools.private.release.create_rc.ProcessBackports") + def test_create_rc_success_with_comment_no_reaction(self, mock_pb_class): + # Arrange + mock_pb = mock_pb_class.return_value + mock_pb.run.return_value = 0 + + args = MagicMock(issue=123, remote="my-remote", triggering_comment=456) + self.mock_gh.get_issue_title.return_value = "Release 2.0.0" + self.mock_gh.get_issue_body.return_value = """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12 +- [ ] Tag RC0 | status=pending +""" + self.mock_git.get_remote_tags.return_value = [] + self.mock_git.get_commit_sha.return_value = "1234567890" + + # Act + result = CreateRc(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 0) + self.mock_gh.add_comment_reaction.assert_not_called() + + @patch("tools.private.release.create_rc.ProcessBackports") + def test_create_rc_precondition_failure_reacts_to_comment(self, mock_pb_class): + # Arrange + mock_pb = mock_pb_class.return_value + mock_pb.run.return_value = 0 # Backports succeed + + args = MagicMock(issue=123, remote="my-remote", triggering_comment=456) + self.mock_gh.get_issue_body.return_value = """ +## Checklist +- [ ] Prepare Release | status=pending +- [ ] Create Release branch | status=pending +- [ ] Tag RC0 | status=pending +""" + + # Act + result = CreateRc(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 1) + self.mock_gh.add_comment_reaction.assert_called_once_with(456, "-1") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/tools/private/release/create_release_branch_test.py b/tests/tools/private/release/create_release_branch_test.py new file mode 100644 index 0000000000..07e3652c85 --- /dev/null +++ b/tests/tools/private/release/create_release_branch_test.py @@ -0,0 +1,149 @@ +import unittest +from unittest.mock import MagicMock + +from tests.tools.private.release.release_test_helper import _mock_git_and_gh +from tools.private.release.create_release_branch import CreateReleaseBranch + + +class CmdCreateReleaseBranchTest(unittest.TestCase): + def setUp(self): + _mock_git_and_gh(self) + + def test_create_release_branch_success(self): + # Arrange + args = MagicMock(issue=123, remote="my-remote") + self.mock_gh.get_issue_title.return_value = "Release 2.0.0" + self.mock_gh.get_issue_body.return_value = """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [ ] Create Release branch | status=pending +""" + self.mock_git.branch_exists.return_value = False + self.mock_git.remote_branch_exists.return_value = False + + # Act + result = CreateReleaseBranch(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 0) + self.mock_git.fetch.assert_called_once_with("my-remote") + self.mock_git.checkout.assert_not_called() + self.mock_git.push.assert_called_once_with( + "my-remote", "abcdef12:refs/heads/release/2.0" + ) + + self.mock_gh.update_issue_body.assert_called_once() + call_args = self.mock_gh.update_issue_body.call_args[0] + self.assertEqual(call_args[0], 123) + self.assertIn( + "branch_url=https://github.com/bazel-contrib/rules_python/tree/release/2.0", + call_args[1], + ) + self.assertIn("commit= abcdef12", call_args[1]) + + def test_create_release_branch_prepare_not_done(self): + # Arrange + args = MagicMock(issue=123, remote="my-remote") + self.mock_gh.get_issue_title.return_value = "Release 2.0.0" + self.mock_gh.get_issue_body.return_value = """ +## Checklist +- [ ] Prepare Release | status=pending +- [ ] Create Release branch | status=pending +""" + # Act + result = CreateReleaseBranch(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 1) + self.mock_git.fetch.assert_not_called() + self.mock_git.push.assert_not_called() + self.mock_gh.update_issue_body.assert_not_called() + + def test_create_release_branch_already_checked(self): + # Arrange + args = MagicMock(issue=123, remote="my-remote") + self.mock_gh.get_issue_title.return_value = "Release 2.0.0" + self.mock_gh.get_issue_body.return_value = """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12 +""" + # Act + result = CreateReleaseBranch(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 0) + self.mock_git.fetch.assert_not_called() + self.mock_git.push.assert_not_called() + self.mock_gh.update_issue_body.assert_not_called() + + def test_create_release_branch_already_exists_same_commit(self): + # Arrange + args = MagicMock(issue=123, remote="my-remote") + self.mock_gh.get_issue_title.return_value = "Release 2.0.0" + self.mock_gh.get_issue_body.return_value = """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [ ] Create Release branch | status=pending +""" + self.mock_git.remote_branch_exists.return_value = True + self.mock_git.get_commit_sha.return_value = "abcdef12" + + # Act + result = CreateReleaseBranch(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 0) + self.mock_git.fetch.assert_called_once_with("my-remote") + self.mock_git.push.assert_not_called() + self.mock_gh.update_issue_body.assert_called_once() # Should still update checklist + + def test_create_release_branch_already_exists_fast_forward(self): + # Arrange + args = MagicMock(issue=123, remote="my-remote") + self.mock_gh.get_issue_title.return_value = "Release 2.0.0" + self.mock_gh.get_issue_body.return_value = """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [ ] Create Release branch | status=pending +""" + self.mock_git.remote_branch_exists.return_value = True + self.mock_git.get_commit_sha.return_value = "oldcommit" + self.mock_git.is_ancestor.return_value = True + + # Act + result = CreateReleaseBranch(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 0) + self.mock_git.fetch.assert_called_once_with("my-remote") + self.mock_git.push.assert_called_once_with( + "my-remote", "abcdef12:refs/heads/release/2.0" + ) + self.mock_gh.update_issue_body.assert_called_once() + + def test_create_release_branch_already_exists_non_ff(self): + # Arrange + args = MagicMock(issue=123, remote="my-remote") + self.mock_gh.get_issue_title.return_value = "Release 2.0.0" + self.mock_gh.get_issue_body.return_value = """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [ ] Create Release branch | status=pending +""" + self.mock_git.remote_branch_exists.return_value = True + self.mock_git.get_commit_sha.return_value = "othercommit" + self.mock_git.is_ancestor.return_value = False + + # Act + result = CreateReleaseBranch(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 1) + self.mock_git.fetch.assert_called_once_with("my-remote") + self.mock_git.push.assert_not_called() + self.mock_gh.update_issue_body.assert_not_called() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/tools/private/release/gh_test.py b/tests/tools/private/release/gh_test.py new file mode 100644 index 0000000000..4010d2dca5 --- /dev/null +++ b/tests/tools/private/release/gh_test.py @@ -0,0 +1,61 @@ +import unittest +from unittest.mock import patch + +from tools.private.release.gh import GitHub + + +class GitHubTest(unittest.TestCase): + def setUp(self): + self.gh = GitHub("my-owner/my-repo") + + @patch("tools.private.release.gh.run_cmd") + def test_resolve_pr_number_digit(self, mock_run_cmd): + # 124 and #125 should resolve immediately without running command + self.assertEqual(self.gh.resolve_pr_number("124"), 124) + self.assertEqual(self.gh.resolve_pr_number("#125"), 125) + mock_run_cmd.assert_not_called() + + @patch("tools.private.release.gh.run_cmd") + def test_resolve_pr_number_url_simple(self, mock_run_cmd): + url = "https://github.com/my-owner/my-repo/pull/126" + # Should resolve via regex without calling gh + result = self.gh.resolve_pr_number(url) + self.assertEqual(result, 126) + mock_run_cmd.assert_not_called() + + @patch("tools.private.release.gh.run_cmd") + def test_resolve_pr_number_url_with_subpath(self, mock_run_cmd): + url = "https://github.com/my-owner/my-repo/pull/126/files" + # Should resolve via regex without calling gh + result = self.gh.resolve_pr_number(url) + self.assertEqual(result, 126) + mock_run_cmd.assert_not_called() + + @patch("tools.private.release.gh.run_cmd") + def test_resolve_pr_number_url_with_query(self, mock_run_cmd): + url = "https://github.com/my-owner/my-repo/pull/126/files?w=1" + # Should resolve via regex without calling gh + result = self.gh.resolve_pr_number(url) + self.assertEqual(result, 126) + mock_run_cmd.assert_not_called() + + @patch("tools.private.release.gh.run_cmd") + def test_resolve_pr_number_url_other_repo(self, mock_run_cmd): + # URL for a different repo should fail immediately without calling gh + url = "https://github.com/other-owner/other-repo/pull/126" + with self.assertRaises(ValueError) as ctx: + self.gh.resolve_pr_number(url) + self.assertIn("URL is not for the configured repository", str(ctx.exception)) + mock_run_cmd.assert_not_called() + + @patch("tools.private.release.gh.run_cmd") + def test_resolve_pr_number_invalid_ref(self, mock_run_cmd): + # Invalid reference (not number, not URL) should fail + with self.assertRaises(ValueError) as ctx: + self.gh.resolve_pr_number("invalid-ref") + self.assertIn("Could not resolve PR reference", str(ctx.exception)) + mock_run_cmd.assert_not_called() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/tools/private/release/git_test.py b/tests/tools/private/release/git_test.py new file mode 100644 index 0000000000..34643fc016 --- /dev/null +++ b/tests/tools/private/release/git_test.py @@ -0,0 +1,189 @@ +import unittest +from unittest.mock import patch + +from tools.private.release.git import Git + + +class GitCheckoutTest(unittest.TestCase): + def setUp(self): + self.git = Git(".") + self.patcher = patch.object(self.git, "_run_git") + self.mock_run_git = self.patcher.start() + self.addCleanup(self.patcher.stop) + + def test_checkout_simple(self): + self.git.checkout("my-branch") + self.mock_run_git.assert_called_once_with( + "checkout", "my-branch", capture_output=False + ) + + @patch("tools.private.release.git.Git.branch_exists") + def test_checkout_track_remote_new_branch(self, mock_branch_exists): + mock_branch_exists.return_value = False + + self.git.checkout("my-branch", track_remote="origin") + + mock_branch_exists.assert_called_once_with("my-branch") + self.mock_run_git.assert_called_once_with( + "checkout", "--track", "origin/my-branch", capture_output=False + ) + + @patch("tools.private.release.git.Git.reset_hard") + @patch("tools.private.release.git.Git.branch_exists") + def test_checkout_track_remote_existing_branch( + self, mock_branch_exists, mock_reset_hard + ): + mock_branch_exists.return_value = True + + self.git.checkout("my-branch", track_remote="origin") + + mock_branch_exists.assert_called_once_with("my-branch") + self.mock_run_git.assert_called_once_with( + "checkout", "my-branch", capture_output=False + ) + mock_reset_hard.assert_called_once_with(reset_to="origin/my-branch") + + +class GitFetchTest(unittest.TestCase): + def setUp(self): + self.git = Git(".") + self.patcher = patch.object(self.git, "_run_git") + self.mock_run_git = self.patcher.start() + self.addCleanup(self.patcher.stop) + + def test_fetch_default(self): + self.git.fetch() + self.mock_run_git.assert_called_once_with( + "fetch", "origin", capture_output=False + ) + + def test_fetch_custom_remote(self): + self.git.fetch("upstream") + self.mock_run_git.assert_called_once_with( + "fetch", "upstream", capture_output=False + ) + + def test_fetch_with_refspec(self): + self.git.fetch("origin", refspec="my-branch") + self.mock_run_git.assert_called_once_with( + "fetch", "origin", "my-branch", capture_output=False + ) + + def test_fetch_with_tags_and_force(self): + self.git.fetch("origin", tags=True, force=True) + self.mock_run_git.assert_called_once_with( + "fetch", "origin", "--tags", "--force", capture_output=False + ) + + def test_fetch_all_options(self): + self.git.fetch("origin", refspec="my-branch", tags=True, force=True) + self.mock_run_git.assert_called_once_with( + "fetch", "origin", "my-branch", "--tags", "--force", capture_output=False + ) + + +class GitGetModifiedFilesTest(unittest.TestCase): + def setUp(self): + self.git = Git(".") + self.patcher = patch.object(self.git, "_run_git") + self.mock_run_git = self.patcher.start() + self.addCleanup(self.patcher.stop) + + def test_get_modified_files(self): + self.mock_run_git.return_value = "file1.txt\nfile2.py\n\n" + files = self.git.get_modified_files("HEAD") + self.mock_run_git.assert_called_once_with( + "show", "--name-only", "--format=", "HEAD" + ) + self.assertEqual(files, ["file1.txt", "file2.py"]) + + def test_get_modified_files_empty(self): + self.mock_run_git.return_value = "" + files = self.git.get_modified_files("HEAD") + self.assertEqual(files, []) + + +class GitDiffTest(unittest.TestCase): + def setUp(self): + self.git = Git(".") + self.patcher = patch.object(self.git, "_run_git") + self.mock_run_git = self.patcher.start() + self.addCleanup(self.patcher.stop) + + def test_diff_has_changes(self): + self.mock_run_git.return_value = "some diff output" + output = self.git.diff() + self.mock_run_git.assert_called_once_with("diff") + self.assertEqual(output, "some diff output") + + def test_diff_empty(self): + self.mock_run_git.return_value = "" + output = self.git.diff() + self.mock_run_git.assert_called_once_with("diff") + self.assertEqual(output, "") + + +class GitApplyTest(unittest.TestCase): + def setUp(self): + self.git = Git(".") + self.patcher = patch.object(self.git, "_run_git") + self.mock_run_git = self.patcher.start() + self.addCleanup(self.patcher.stop) + + def test_apply(self): + self.git.apply("patch.patch") + self.mock_run_git.assert_called_once_with( + "apply", "patch.patch", capture_output=False + ) + + +class GitApplyCheckTest(unittest.TestCase): + def setUp(self): + self.git = Git(".") + self.patcher = patch.object(self.git, "_run_git") + self.mock_run_git = self.patcher.start() + self.addCleanup(self.patcher.stop) + + def test_apply_check_clean(self): + self.mock_run_git.return_value = "" + result = self.git.apply_check("patch.patch") + self.mock_run_git.assert_called_once_with( + "apply", "--check", "patch.patch", capture_output=False + ) + self.assertTrue(result) + + def test_apply_check_conflict(self): + import subprocess + + self.mock_run_git.side_effect = subprocess.CalledProcessError( + 1, ["git", "apply", "--check", "patch.patch"] + ) + result = self.git.apply_check("patch.patch") + self.mock_run_git.assert_called_once_with( + "apply", "--check", "patch.patch", capture_output=False + ) + self.assertFalse(result) + + +class GitResetHardTest(unittest.TestCase): + def setUp(self): + self.git = Git(".") + self.patcher = patch.object(self.git, "_run_git") + self.mock_run_git = self.patcher.start() + self.addCleanup(self.patcher.stop) + + def test_reset_hard_default(self): + self.git.reset_hard() + self.mock_run_git.assert_called_once_with( + "reset", "--hard", "HEAD", capture_output=False + ) + + def test_reset_hard_custom(self): + self.git.reset_hard(reset_to="my-commit") + self.mock_run_git.assert_called_once_with( + "reset", "--hard", "my-commit", capture_output=False + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/tools/private/release/on_pr_merged_test.py b/tests/tools/private/release/on_pr_merged_test.py new file mode 100644 index 0000000000..a5644c5705 --- /dev/null +++ b/tests/tools/private/release/on_pr_merged_test.py @@ -0,0 +1,112 @@ +import argparse +import unittest +from unittest.mock import MagicMock, patch + +from tests.tools.private.release.release_test_helper import _mock_git_and_gh +from tools.private.release.on_pr_merged import OnPrMerged + + +class CmdOnPrMergedTest(unittest.TestCase): + def setUp(self): + _mock_git_and_gh(self) + self.addCleanup(patch.stopall) + + # Mock ProcessBackports + self.mock_process_patcher = patch( + "tools.private.release.on_pr_merged.ProcessBackports" + ) + self.mock_process_class = self.mock_process_patcher.start() + self.mock_process_instance = MagicMock() + self.mock_process_class.return_value = self.mock_process_instance + + def test_on_pr_merged_no_comment(self): + args = argparse.Namespace(pr=124, remote="origin", dry_run=True) + self.mock_gh.get_pr_comments.return_value = [ + {"body": "Some comment"}, + {"body": "Another comment /backport_wrong"}, + ] + + result = OnPrMerged(args, self.mock_git, self.mock_gh).run() + + self.assertEqual(result, 1) + self.mock_gh.get_pr_comments.assert_called_once_with(124) + self.mock_gh.get_open_tracking_issues.assert_not_called() + self.mock_process_class.assert_not_called() + + def test_on_pr_merged_has_comment_no_active_release(self): + args = argparse.Namespace(pr=124, remote="origin", dry_run=True) + self.mock_gh.get_pr_comments.return_value = [ + {"body": "/backport"}, + ] + self.mock_gh.get_open_tracking_issues.return_value = [] + + result = OnPrMerged(args, self.mock_git, self.mock_gh).run() + + self.assertEqual(result, 1) + self.mock_gh.get_pr_comments.assert_called_once_with(124) + self.mock_gh.get_open_tracking_issues.assert_called_once() + self.mock_gh.get_issue_body.assert_not_called() + self.mock_process_class.assert_not_called() + + def test_on_pr_merged_has_comment_not_in_backports(self): + args = argparse.Namespace(pr=124, remote="origin", dry_run=True) + self.mock_gh.get_pr_comments.return_value = [ + {"body": " /backport "}, + ] + self.mock_gh.get_open_tracking_issues.return_value = [ + {"number": 456, "title": "Release 2.1.0", "url": "http://..."} + ] + self.mock_gh.get_issue_body.return_value = """ +## Checklist +- [ ] Prepare Release + +## Backports +- [ ] #125 | status=pending +""" + result = OnPrMerged(args, self.mock_git, self.mock_gh).run() + + self.assertEqual(result, 1) + self.mock_gh.get_pr_comments.assert_called_once_with(124) + self.mock_gh.get_open_tracking_issues.assert_called_once() + self.mock_gh.get_issue_body.assert_called_once_with(456) + self.mock_process_class.assert_not_called() + + def test_on_pr_merged_success(self): + args = argparse.Namespace(pr=124, remote="origin", dry_run=True) + self.mock_gh.get_pr_comments.return_value = [ + {"body": "/backport"}, + ] + self.mock_gh.get_open_tracking_issues.return_value = [ + {"number": 456, "title": "Release 2.1.0", "url": "http://..."} + ] + self.mock_gh.get_issue_body.return_value = """ +## Checklist +- [ ] Prepare Release + +## Backports +- [ ] #124 | status=pending +""" + self.mock_process_instance.run.return_value = 0 + + result = OnPrMerged(args, self.mock_git, self.mock_gh).run() + + self.assertEqual(result, 0) + self.mock_gh.get_pr_comments.assert_called_once_with(124) + self.mock_gh.get_open_tracking_issues.assert_called_once() + self.mock_gh.get_issue_body.assert_called_once_with(456) + + # Verify ProcessBackports was instantiated with correct args + self.mock_process_class.assert_called_once() + called_args = self.mock_process_class.call_args[0][0] + self.assertEqual(called_args.issue, 456) + self.assertEqual(called_args.remote, "origin") + self.assertEqual(called_args.dry_run, True) + self.assertIsNone(called_args.add) + self.assertIsNone(called_args.triggering_comment) + + # Verify ProcessBackports.run() was called + self.mock_process_instance.run.assert_called_once() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/tools/private/release/prepare_test.py b/tests/tools/private/release/prepare_test.py new file mode 100644 index 0000000000..45b92a636f --- /dev/null +++ b/tests/tools/private/release/prepare_test.py @@ -0,0 +1,250 @@ +import unittest +from unittest.mock import MagicMock, patch + +from tests.tools.private.release.release_test_helper import ( + TempDirTestCase, + _mock_git_and_gh, +) +from tools.private.release.gh import ( + MultipleTrackingIssuesError, + NoTrackingIssueError, +) +from tools.private.release.prepare import Prepare + + +class CmdPrepareTest(TempDirTestCase): + def setUp(self): + super().setUp() + _mock_git_and_gh(self) + + @patch("tools.private.release.prepare.changelog_news") + @patch("tools.private.release.prepare.replace_version_next") + def test_prepare_success_existing_issue(self, mock_replace, mock_changelog): + # Arrange + args = MagicMock(version="2.0.0", issue=None, dry_run=False) + self.mock_git.status.side_effect = ["", "M foo"] + self.mock_git.branch_exists.return_value = False + self.mock_gh.get_release_tracking_issue.side_effect = None + self.mock_gh.get_release_tracking_issue.return_value = 123 + self.mock_gh.create_pr.return_value = "https://github.com/foo/bar/pull/456" + self.mock_gh.get_issue_body.return_value = "- [ ] Prepare Release" + + # Act + result = Prepare(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 0) + self.mock_gh.get_release_tracking_issue.assert_called_once_with("2.0.0") + self.mock_gh.create_tracking_issue.assert_not_called() + self.mock_gh.create_pr.assert_called_once_with( + title="Prepare release v2.0.0", + body="Work towards #123", + base="main", + ) + self.mock_git.add_modified_and_deleted.assert_called_once() + + @patch("tools.private.release.prepare.changelog_news") + @patch("tools.private.release.prepare.replace_version_next") + def test_prepare_success_create_issue(self, mock_replace, mock_changelog): + # Arrange + template_dir = self.tmpdir / ".github" / "ISSUE_TEMPLATE" + template_dir.mkdir(parents=True, exist_ok=True) + template_file = template_dir / "release_tracking_template.md" + template_file.write_text("dummy template content") + + args = MagicMock(version="2.0.0", issue=None, dry_run=False) + self.mock_git.status.side_effect = ["", "M foo"] + self.mock_git.branch_exists.return_value = False + self.mock_gh.get_release_tracking_issue.side_effect = NoTrackingIssueError( + "Not found" + ) + self.mock_gh.create_tracking_issue.return_value = 123 + self.mock_gh.create_pr.return_value = "https://github.com/foo/bar/pull/456" + self.mock_gh.get_issue_body.return_value = "- [ ] Prepare Release" + + # Act + result = Prepare(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 0) + self.mock_gh.get_release_tracking_issue.assert_called_once_with("2.0.0") + self.mock_gh.create_tracking_issue.assert_called_once_with( + "2.0.0", "dummy template content" + ) + self.mock_gh.create_pr.assert_called_once_with( + title="Prepare release v2.0.0", + body="Work towards #123", + base="main", + ) + self.mock_git.add_modified_and_deleted.assert_called_once() + + @patch("tools.private.release.prepare.changelog_news") + @patch("tools.private.release.prepare.replace_version_next") + def test_prepare_ambiguous_issue(self, mock_replace, mock_changelog): + # Arrange + args = MagicMock(version="2.0.0", issue=None, dry_run=False) + self.mock_git.status.side_effect = ["", "M foo"] + self.mock_git.branch_exists.return_value = False + self.mock_gh.get_release_tracking_issue.side_effect = ( + MultipleTrackingIssuesError("Multiple open tracking issues") + ) + + # Act + result = Prepare(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 1) + self.mock_gh.get_release_tracking_issue.assert_called_once_with("2.0.0") + self.mock_gh.create_tracking_issue.assert_not_called() + self.mock_gh.create_pr.assert_not_called() + self.mock_git.add_modified_and_deleted.assert_not_called() + + @patch("tools.private.release.prepare.changelog_news") + @patch("tools.private.release.prepare.replace_version_next") + def test_prepare_dry_run(self, mock_replace, mock_changelog): + # Arrange + args = MagicMock(version="2.0.0", issue=None, dry_run=True) + self.mock_git.status.side_effect = [""] + self.mock_gh.get_release_tracking_issue.side_effect = None + self.mock_gh.get_release_tracking_issue.return_value = 123 + + # Act + result = Prepare(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 0) + self.mock_git.checkout.assert_not_called() + self.mock_git.commit.assert_not_called() + self.mock_git.push.assert_not_called() + self.mock_gh.create_pr.assert_not_called() + self.mock_gh.update_issue_body.assert_not_called() + self.mock_git.fetch.assert_called_once() + self.mock_gh.get_release_tracking_issue.assert_called_once_with("2.0.0") + self.mock_git.add_modified_and_deleted.assert_not_called() + + @patch("tools.private.release.prepare.changelog_news") + @patch("tools.private.release.prepare.replace_version_next") + def test_prepare_use_associated_pr_from_tracking_issue( + self, mock_replace, mock_changelog + ): + # Arrange + args = MagicMock(version="2.0.0", issue=None, dry_run=False) + self.mock_git.status.side_effect = ["", ""] + self.mock_git.branch_exists.return_value = True + self.mock_gh.get_release_tracking_issue.side_effect = None + self.mock_gh.get_release_tracking_issue.return_value = 123 + self.mock_gh.get_open_pr.return_value = None + # PR #456 is already associated in the tracking issue + self.mock_gh.get_issue_body.return_value = ( + "- [ ] Prepare Release | status=pending pr=#456" + ) + + # Act + result = Prepare(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 0) + self.mock_git.checkout.assert_called_once_with("prepare-2.0.0") + self.mock_git.commit.assert_not_called() + self.mock_git.push.assert_called_once_with( + "origin", "prepare-2.0.0", set_upstream=True, force=True + ) + self.mock_gh.get_open_pr.assert_called_once_with("prepare-2.0.0") + self.mock_gh.create_pr.assert_not_called() # Should NOT create a new PR + self.mock_gh.update_issue_body.assert_called_once() + call_args = self.mock_gh.update_issue_body.call_args[0] + self.assertIn("pr=#456", call_args[1]) + + @patch("tools.private.release.prepare.changelog_news") + @patch("tools.private.release.prepare.replace_version_next") + def test_prepare_create_pr_when_none_associated(self, mock_replace, mock_changelog): + # Arrange + args = MagicMock(version="2.0.0", issue=None, dry_run=False) + self.mock_git.status.side_effect = ["", ""] + self.mock_git.branch_exists.return_value = True + self.mock_gh.get_release_tracking_issue.side_effect = None + self.mock_gh.get_release_tracking_issue.return_value = 123 + self.mock_gh.get_open_pr.return_value = None + # No PR associated in the tracking issue + self.mock_gh.get_issue_body.return_value = "- [ ] Prepare Release" + self.mock_gh.create_pr.return_value = "https://github.com/foo/bar/pull/789" + + # Act + result = Prepare(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 0) + self.mock_git.checkout.assert_called_once_with("prepare-2.0.0") + self.mock_git.commit.assert_not_called() + self.mock_git.push.assert_called_once_with( + "origin", "prepare-2.0.0", set_upstream=True, force=True + ) + self.mock_gh.get_open_pr.assert_called_once_with("prepare-2.0.0") + self.mock_gh.create_pr.assert_called_once_with( + title="Prepare release v2.0.0", + body="Work towards #123", + base="main", + ) + self.mock_gh.update_issue_body.assert_called_once() + call_args = self.mock_gh.update_issue_body.call_args[0] + self.assertIn("pr=#789", call_args[1]) + + @patch("tools.private.release.prepare.changelog_news") + @patch("tools.private.release.prepare.replace_version_next") + def test_prepare_reuse_existing_pr(self, mock_replace, mock_changelog): + # Arrange + args = MagicMock(version="2.0.0", issue=None, dry_run=False) + self.mock_git.status.side_effect = ["", ""] + self.mock_git.branch_exists.return_value = True + self.mock_gh.get_release_tracking_issue.side_effect = None + self.mock_gh.get_release_tracking_issue.return_value = 123 + self.mock_gh.get_open_pr.return_value = { + "number": 456, + "url": "https://github.com/foo/bar/pull/456", + } + self.mock_gh.get_issue_body.return_value = "- [ ] Prepare Release" + + # Act + result = Prepare(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 0) + self.mock_git.checkout.assert_called_once_with("prepare-2.0.0") + self.mock_git.commit.assert_not_called() + self.mock_git.push.assert_called_once_with( + "origin", "prepare-2.0.0", set_upstream=True, force=True + ) + self.mock_gh.get_open_pr.assert_called_once_with("prepare-2.0.0") + self.mock_gh.create_pr.assert_not_called() + self.mock_gh.update_issue_body.assert_called_once() + call_args = self.mock_gh.update_issue_body.call_args[0] + self.assertIn("pr=#456", call_args[1]) + + @patch("tools.private.release.prepare.changelog_news") + @patch("tools.private.release.prepare.replace_version_next") + def test_prepare_dry_run_no_issue(self, mock_replace, mock_changelog): + # Arrange + template_dir = self.tmpdir / ".github" / "ISSUE_TEMPLATE" + template_dir.mkdir(parents=True, exist_ok=True) + template_file = template_dir / "release_tracking_template.md" + template_file.write_text("dummy template content") + + args = MagicMock(version="2.0.0", issue=None, dry_run=True) + self.mock_git.status.side_effect = [""] + self.mock_gh.get_release_tracking_issue.side_effect = NoTrackingIssueError( + "Not found" + ) + + # Act + result = Prepare(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 0) + self.mock_git.checkout.assert_not_called() + self.mock_gh.create_tracking_issue.assert_not_called() + self.mock_gh.create_pr.assert_not_called() + self.mock_git.add_modified_and_deleted.assert_not_called() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/tools/private/release/process_backports_test.py b/tests/tools/private/release/process_backports_test.py new file mode 100644 index 0000000000..d5df43ae46 --- /dev/null +++ b/tests/tools/private/release/process_backports_test.py @@ -0,0 +1,751 @@ +import argparse +import datetime +import unittest +from unittest.mock import call, patch + +from tests.tools.private.release.release_test_helper import _mock_git_and_gh +from tools.private.release.process_backports import ProcessBackports + + +class CmdProcessBackportsTest(unittest.TestCase): + def setUp(self): + _mock_git_and_gh(self) + self.mock_changelog_news = patch( + "tools.private.release.process_backports.changelog_news" + ).start() + self.mock_replace_version_next = patch( + "tools.private.release.process_backports.replace_version_next" + ).start() + self.addCleanup(patch.stopall) + self.mock_gh.resolve_pr_number.side_effect = lambda x: int( + x.lstrip("#").split("/")[-1] + ) + self.mock_git.diff.return_value = "" + self.mock_git.apply_check.return_value = True + + # Dynamic mock for issue body + self.issue_body = "" + + def mock_get_body(issue_num): + return self.issue_body + + def mock_update_body(issue_num, body): + self.issue_body = body + + self.mock_gh.get_issue_body.side_effect = mock_get_body + self.mock_gh.update_issue_body.side_effect = mock_update_body + + def test_process_backports_no_pending(self): + args = argparse.Namespace( + issue=123, remote="origin", dry_run=False, add=None, triggering_comment=None + ) + self.issue_body = "No backports here" + + result = ProcessBackports(args, self.mock_git, self.mock_gh).run() + + self.assertEqual(result, 0) + self.mock_gh.get_issue_body.assert_called_once_with(123) + self.mock_git.fetch.assert_not_called() + + @patch("tools.private.release.process_backports.datetime") + def test_process_backports_success(self, mock_datetime): + mock_datetime.date.today.return_value = datetime.date(2026, 7, 1) + args = argparse.Namespace( + issue=123, remote="origin", dry_run=False, add=None, triggering_comment=None + ) + self.mock_gh.get_issue_title.return_value = "Release 2.0.0" + self.issue_body = """ +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch +- [ ] Sync Changelog #124 +- [ ] Tag Final + +## Backports +- [ ] #124 | status=pending +""" + self.mock_git.get_remote_tags.return_value = [] + + def mock_resolve(items): + for item in items: + if item.pr_ref == "#124": + item.commit = "abcdef12" + item.status = "done" + return items + + self.mock_gh.get_merge_commits_for_prs.side_effect = mock_resolve + + self.mock_git.sort_commits_chronologically.return_value = ["abcdef12"] + self.mock_git.get_commit_sha.side_effect = ["12345678", "12345678", "main_sha"] + self.mock_git.get_commit_message.return_value = 'Cherry-pick "fix bug"' + self.mock_git.get_modified_files.return_value = ["news/124.fixed.md"] + self.mock_git.diff.return_value = "version diff for 124" + self.mock_git.apply_check.return_value = True + self.mock_gh.create_pr.return_value = "https://github.com/foo/bar/pull/999" + + result = ProcessBackports(args, self.mock_git, self.mock_gh).run() + + self.assertEqual(result, 0) + self.mock_git.fetch.assert_has_calls( + [ + call("origin", tags=True, force=True), + call("origin"), + call("origin", refspec="main"), + ] + ) + self.mock_git.checkout.assert_has_calls( + [ + call("release/2.0", track_remote="origin"), + call("main", track_remote="origin"), + call("prepare-2.0.0-backports-6affdae", create_branch=True), + call("release/2.0"), + ] + ) + self.mock_git.cherry_pick.assert_called_once_with("abcdef12") + self.mock_git.diff.assert_called_once() + self.mock_git.apply_check.assert_called_once_with(unittest.mock.ANY) + self.mock_git.apply.assert_called_once_with(unittest.mock.ANY) + self.mock_changelog_news.update_changelog.assert_has_calls( + [ + call("2.0.0", "2026-07-01"), + call( + "2.0.0", + "2026-07-01", + news_files=["news/124.fixed.md"], + delete_news=True, + ), + ] + ) + self.assertEqual(self.mock_git.add_modified_and_deleted.call_count, 2) + self.mock_replace_version_next.assert_called_once_with("2.0.0") + self.mock_git.commit.assert_has_calls( + [ + call('Cherry-pick "fix bug"\n\nWork towards #123', amend=True), + call("chore(release): sync changelog for v2.0.0 backports"), + ] + ) + self.mock_git.push.assert_has_calls( + [ + call("origin", "release/2.0"), + call( + "origin", + "prepare-2.0.0-backports-6affdae", + set_upstream=True, + force=True, + ), + ] + ) + + self.mock_gh.create_pr.assert_called_once_with( + title="chore(release): sync changelog for v2.0.0 backports", + body="Updates CHANGELOG.md and removes news files for backports:\n- #124\n\nWork towards #123\nRelease-Tracking-Issue: #123", + base="main", + labels=["type: sync-changelog"], + ) + self.mock_gh.enable_auto_merge.assert_called_once_with(999) + + self.assertEqual(self.mock_gh.update_issue_body.call_count, 2) + call_args_list = self.mock_gh.update_issue_body.call_args_list + self.assertEqual(call_args_list[0][0][0], 123) + self.assertIn( + "- [x] #124 | status=done rc=rc0 commit= 12345678", call_args_list[0][0][1] + ) + self.assertEqual(call_args_list[1][0][0], 123) + self.assertIn( + "- [ ] Sync Changelog #124 | status=pending pr=#999", + call_args_list[1][0][1], + ) + + @patch("tools.private.release.process_backports.datetime") + def test_process_backports_sync_branch_exists(self, mock_datetime): + mock_datetime.date.today.return_value = datetime.date(2026, 7, 1) + args = argparse.Namespace( + issue=123, remote="origin", dry_run=False, add=None, triggering_comment=None + ) + self.mock_gh.get_issue_title.return_value = "Release 2.0.0" + self.issue_body = """ +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch +- [ ] Sync Changelog #124 +- [ ] Tag Final + +## Backports +- [ ] #124 | status=pending +""" + self.mock_git.get_remote_tags.return_value = [] + + def mock_resolve(items): + for item in items: + if item.pr_ref == "#124": + item.commit = "abcdef12" + item.status = "done" + return items + + self.mock_gh.get_merge_commits_for_prs.side_effect = mock_resolve + + self.mock_git.sort_commits_chronologically.return_value = ["abcdef12"] + self.mock_git.get_commit_sha.side_effect = ["12345678", "12345678", "main_sha"] + self.mock_git.get_commit_message.return_value = 'Cherry-pick "fix bug"' + self.mock_git.get_modified_files.return_value = ["news/124.fixed.md"] + self.mock_git.diff.return_value = "version diff for 124" + self.mock_git.apply_check.return_value = True + self.mock_gh.create_pr.return_value = "https://github.com/foo/bar/pull/999" + + # Configure branch to exist + self.mock_git.branch_exists.return_value = True + + result = ProcessBackports(args, self.mock_git, self.mock_gh).run() + + self.assertEqual(result, 0) + self.mock_git.fetch.assert_has_calls( + [ + call("origin", tags=True, force=True), + call("origin"), + call("origin", refspec="main"), + ] + ) + self.mock_git.checkout.assert_has_calls( + [ + call("release/2.0", track_remote="origin"), + call("main", track_remote="origin"), + # Called without create_branch=True + call("prepare-2.0.0-backports-6affdae"), + call("release/2.0"), + ] + ) + # Verify reset_hard was called to reset the existing branch to main + self.mock_git.reset_hard.assert_has_calls( + [ + call(reset_to="main"), + ] + ) + self.mock_git.cherry_pick.assert_called_once_with("abcdef12") + self.mock_git.diff.assert_called_once() + self.mock_git.apply_check.assert_called_once_with(unittest.mock.ANY) + self.mock_git.apply.assert_called_once_with(unittest.mock.ANY) + self.mock_changelog_news.update_changelog.assert_has_calls( + [ + call("2.0.0", "2026-07-01"), + call( + "2.0.0", + "2026-07-01", + news_files=["news/124.fixed.md"], + delete_news=True, + ), + ] + ) + self.assertEqual(self.mock_git.add_modified_and_deleted.call_count, 2) + self.mock_replace_version_next.assert_called_once_with("2.0.0") + self.mock_git.commit.assert_has_calls( + [ + call('Cherry-pick "fix bug"\n\nWork towards #123', amend=True), + call("chore(release): sync changelog for v2.0.0 backports"), + ] + ) + self.mock_git.push.assert_has_calls( + [ + call("origin", "release/2.0"), + call( + "origin", + "prepare-2.0.0-backports-6affdae", + set_upstream=True, + force=True, + ), + ] + ) + + self.mock_gh.create_pr.assert_called_once_with( + title="chore(release): sync changelog for v2.0.0 backports", + body="Updates CHANGELOG.md and removes news files for backports:\n- #124\n\nWork towards #123\nRelease-Tracking-Issue: #123", + base="main", + labels=["type: sync-changelog"], + ) + self.mock_gh.enable_auto_merge.assert_called_once_with(999) + + self.assertEqual(self.mock_gh.update_issue_body.call_count, 2) + call_args_list = self.mock_gh.update_issue_body.call_args_list + self.assertEqual(call_args_list[0][0][0], 123) + self.assertIn( + "- [x] #124 | status=done rc=rc0 commit= 12345678", call_args_list[0][0][1] + ) + self.assertEqual(call_args_list[1][0][0], 123) + self.assertIn( + "- [ ] Sync Changelog #124 | status=pending pr=#999", + call_args_list[1][0][1], + ) + + @patch("tools.private.release.process_backports.datetime") + def test_process_backports_dry_run(self, mock_datetime): + mock_datetime.date.today.return_value = datetime.date(2026, 7, 1) + args = argparse.Namespace( + issue=123, remote="origin", dry_run=True, add=None, triggering_comment=None + ) + self.mock_gh.get_issue_title.return_value = "Release 2.0.0" + self.issue_body = """ +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch +- [ ] Sync Changelog #124 +- [ ] Tag Final + +## Backports +- [ ] #124 | status=pending +""" + self.mock_git.get_remote_tags.return_value = [] + + def mock_resolve(items): + for item in items: + if item.pr_ref == "#124": + item.commit = "abcdef12" + item.status = "done" + return items + + self.mock_gh.get_merge_commits_for_prs.side_effect = mock_resolve + + self.mock_git.sort_commits_chronologically.return_value = ["abcdef12"] + self.mock_git.get_commit_sha.side_effect = ["12345678", "main_sha"] + self.mock_git.get_commit_message.return_value = 'Cherry-pick "fix bug"' + self.mock_git.get_modified_files.return_value = ["news/124.fixed.md"] + self.mock_git.diff.return_value = "version diff for 124" + self.mock_git.apply_check.return_value = True + + result = ProcessBackports(args, self.mock_git, self.mock_gh).run() + + self.assertEqual(result, 0) + self.mock_git.fetch.assert_has_calls( + [ + call("origin", tags=True, force=True), + call("origin"), + call("origin", refspec="main"), + ] + ) + self.mock_git.checkout.assert_has_calls( + [ + call("release/2.0", track_remote="origin"), + call("main", track_remote="origin"), + call("release/2.0"), + ] + ) + self.mock_git.cherry_pick.assert_called_once_with("abcdef12") + self.mock_git.diff.assert_called_once() + self.mock_git.apply_check.assert_called_once_with(unittest.mock.ANY) + self.mock_git.apply.assert_not_called() + self.mock_changelog_news.update_changelog.assert_has_calls( + [ + call("2.0.0", "2026-07-01"), + call( + "2.0.0", + "2026-07-01", + news_files=["news/124.fixed.md"], + delete_news=True, + ), + ] + ) + self.assertEqual(self.mock_git.add_modified_and_deleted.call_count, 1) + self.mock_replace_version_next.assert_called_once_with("2.0.0") + self.mock_git.commit.assert_called_once_with( + 'Cherry-pick "fix bug"\n\nWork towards #123', amend=True + ) + self.mock_git.reset_hard.assert_has_calls( + [ + call(reset_to="12345678"), + call(reset_to="main_sha"), + ] + ) + self.mock_git.push.assert_not_called() + self.mock_gh.update_issue_body.assert_not_called() + + def test_process_backports_ignored_and_failed_states(self): + args = argparse.Namespace( + issue=123, remote="origin", dry_run=False, add=None, triggering_comment=None + ) + self.mock_gh.get_issue_title.return_value = "Release 2.0.0" + self.issue_body = """ +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch + +## Backports +- [ ] #124 | status=pending +- [ ] #125 | status=pending +- [ ] #126 | status=pending +""" + self.mock_git.get_remote_tags.return_value = [] + + def mock_resolve(items): + for item in items: + if item.pr_ref == "#124": + item.status = "open-pr" + elif item.pr_ref == "#125": + item.status = "draft-pr" + elif item.pr_ref == "#126": + item.status = "error-closed-pr" + return items + + self.mock_gh.get_merge_commits_for_prs.side_effect = mock_resolve + + result = ProcessBackports(args, self.mock_git, self.mock_gh).run() + + self.assertEqual(result, 1) + self.mock_gh.update_issue_body.assert_called_once() + call_args = self.mock_gh.update_issue_body.call_args[0] + self.assertEqual(call_args[0], 123) + self.assertIn("- [ ] #126 | status=error-closed-pr", call_args[1]) + self.assertNotIn("status=open-pr", call_args[1]) + self.assertNotIn("status=draft-pr", call_args[1]) + self.mock_git.checkout.assert_not_called() + self.mock_git.cherry_pick.assert_not_called() + + def test_process_backports_ignored_error_status(self): + args = argparse.Namespace( + issue=123, remote="origin", dry_run=False, add=None, triggering_comment=None + ) + self.mock_gh.get_issue_title.return_value = "Release 2.0.0" + self.issue_body = """ +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch + +## Backports +- [ ] #124 | status=error-merge-conflict +- [ ] #125 | status=error-some-other-error +""" + self.mock_git.get_remote_tags.return_value = [] + self.mock_gh.get_merge_commits_for_prs.return_value = [] + + result = ProcessBackports(args, self.mock_git, self.mock_gh).run() + + self.assertEqual(result, 0) + self.mock_gh.get_merge_commits_for_prs.assert_not_called() + self.mock_git.checkout.assert_not_called() + + @patch("tools.private.release.process_backports.datetime") + def test_process_backports_cherry_pick_failed(self, mock_datetime): + mock_datetime.date.today.return_value = datetime.date(2026, 7, 1) + args = argparse.Namespace( + issue=123, remote="origin", dry_run=False, add=None, triggering_comment=None + ) + self.mock_gh.get_issue_title.return_value = "Release 2.0.0" + self.issue_body = """ +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch + +## Backports +- [ ] #124 | status=pending +""" + self.mock_git.get_remote_tags.return_value = [] + + def mock_resolve(items): + for item in items: + if item.pr_ref == "#124": + item.commit = "abcdef12" + item.status = "done" + return items + + self.mock_gh.get_merge_commits_for_prs.side_effect = mock_resolve + + self.mock_git.sort_commits_chronologically.return_value = ["abcdef12"] + self.mock_git.cherry_pick.side_effect = Exception("Cherry-pick conflict") + + result = ProcessBackports(args, self.mock_git, self.mock_gh).run() + + self.assertEqual(result, 1) + self.mock_git.checkout.assert_called_once_with( + "release/2.0", track_remote="origin" + ) + self.mock_git.cherry_pick.assert_called_once_with("abcdef12") + self.mock_git.cherry_pick_abort.assert_called_once() + + self.mock_gh.update_issue_body.assert_called_once() + call_args = self.mock_gh.update_issue_body.call_args[0] + self.assertEqual(call_args[0], 123) + self.assertIn("- [ ] #124 | status=error-merge-conflict", call_args[1]) + + self.mock_git.commit.assert_not_called() + self.mock_git.push.assert_not_called() + + @patch("tools.private.release.process_backports.datetime") + def test_process_backports_add_backports_and_auto_add_rc_task(self, mock_datetime): + mock_datetime.date.today.return_value = datetime.date(2026, 7, 1) + args = argparse.Namespace( + issue=123, + remote="origin", + dry_run=False, + add=["https://github.com/bazel-contrib/rules_python/pull/124"], + triggering_comment=None, + ) + self.mock_gh.get_issue_title.return_value = "Release 2.0.0" + self.issue_body = """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12 +- [x] Tag RC0 | status=done tag=2.0.0-rc0 commit=abcdef12 +- [ ] Tag Final + +## Backports +""" + self.mock_git.get_remote_tags.return_value = ["2.0.0-rc0"] + self.mock_git.get_commit_sha.return_value = "12345678" + self.mock_git.get_commit_message.return_value = 'Cherry-pick "fix bug"' + + def mock_resolve(items): + for item in items: + if item.pr_ref == "#124": + item.commit = "abcdef12" + item.status = "done" + return items + + self.mock_gh.get_merge_commits_for_prs.side_effect = mock_resolve + self.mock_git.sort_commits_chronologically.return_value = ["abcdef12"] + + # Mock create_pr to return a string to avoid int(MagicMock) returning 1 + self.mock_gh.create_pr.return_value = "https://github.com/foo/bar/pull/999" + + result = ProcessBackports(args, self.mock_git, self.mock_gh).run() + + self.assertEqual(result, 0) + + # update_issue_body should be called 3 times: + # 1. When adding backports and auto-adding Tag RC1 task. + # 2. When updating the backport status to done. + # 3. When updating the sync task status to pending. + self.assertEqual(self.mock_gh.update_issue_body.call_count, 3) + + call1_args = self.mock_gh.update_issue_body.call_args_list[0][0] + call2_args = self.mock_gh.update_issue_body.call_args_list[1][0] + + self.assertEqual(call1_args[0], 123) + self.assertIn("- [ ] #124", call1_args[1]) + self.assertIn("- [ ] Tag RC1", call1_args[1]) + self.assertIn("- [ ] Sync Changelog #124", call1_args[1]) + self.assertIn( + "- [x] Tag RC0 | status=done tag=2.0.0-rc0 commit=abcdef12\n- [ ]" + " Tag RC1\n- [ ] Sync Changelog #124\n- [ ] Tag Final", + call1_args[1].strip(), + ) + + self.assertEqual(call2_args[0], 123) + self.assertIn("- [x] #124 | status=done rc=rc1 commit= 12345678", call2_args[1]) + + call3_args = self.mock_gh.update_issue_body.call_args_list[2][0] + self.assertEqual(call3_args[0], 123) + self.assertIn( + "- [ ] Sync Changelog #124 | status=pending pr=#999", call3_args[1] + ) + + @patch("tools.private.release.process_backports.datetime") + def test_process_backports_add_backports_marks_invalid(self, mock_datetime): + mock_datetime.date.today.return_value = datetime.date(2026, 7, 1) + args = argparse.Namespace( + issue=123, + remote="origin", + dry_run=False, + add=["124", "invalid", "125"], + triggering_comment=None, + ) + self.mock_gh.get_issue_title.return_value = "Release 2.0.0" + self.issue_body = """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12 +- [x] Tag RC0 | status=done tag=2.0.0-rc0 commit=abcdef12 +- [ ] Tag Final + +## Backports +""" + self.mock_git.get_remote_tags.return_value = ["2.0.0-rc0"] + self.mock_git.get_commit_sha.return_value = "1234567890" + self.mock_git.get_commit_message.return_value = 'Cherry-pick "fix bug"' + + def mock_resolve(items): + # Both 124 and 125 should be processed, 'invalid' should be ignored (it has error status) + for item in items: + if item.pr_ref == "#124": + item.commit = "sha_124" + item.status = "done" + elif item.pr_ref == "#125": + item.commit = "sha_125" + item.status = "done" + return items + + self.mock_gh.get_merge_commits_for_prs.side_effect = mock_resolve + self.mock_git.sort_commits_chronologically.return_value = ["sha_124", "sha_125"] + self.mock_gh.create_pr.return_value = "https://github.com/foo/bar/pull/999" + + result = ProcessBackports(args, self.mock_git, self.mock_gh).run() + + self.assertEqual(result, 0) + # Should have updated body to add 124, 125, and invalid + # update_issue_body should be called 4 times: + # 1. When adding backports. + # 2. When updating 124 status to done. + # 3. When updating 125 status to done. + # 4. When updating sync tasks status to pending. + self.assertEqual(self.mock_gh.update_issue_body.call_count, 4) + call1_args = self.mock_gh.update_issue_body.call_args_list[0][0] + self.assertIn("- [ ] #124", call1_args[1]) + self.assertIn("- [ ] #125", call1_args[1]) + self.assertIn("- [ ] invalid | status=error-invalid-pr", call1_args[1]) + self.assertIn("- [ ] Sync Changelog #124", call1_args[1]) + self.assertIn("- [ ] Sync Changelog #125", call1_args[1]) + + call4_args = self.mock_gh.update_issue_body.call_args_list[3][0] + self.assertIn( + "- [ ] Sync Changelog #124 | status=pending pr=#999", call4_args[1] + ) + self.assertIn( + "- [ ] Sync Changelog #125 | status=pending pr=#999", call4_args[1] + ) + + @patch("tools.private.release.process_backports.datetime") + def test_process_backports_version_sync_failure(self, mock_datetime): + mock_datetime.date.today.return_value = datetime.date(2026, 7, 1) + args = argparse.Namespace( + issue=123, remote="origin", dry_run=False, add=None, triggering_comment=None + ) + self.mock_gh.get_issue_title.return_value = "Release 2.0.0" + self.issue_body = """ +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch +- [ ] Sync Changelog #124 +- [ ] Sync Changelog #125 +- [ ] Tag Final + +## Backports +- [ ] #124 | status=pending +- [ ] #125 | status=pending +""" + self.mock_git.get_remote_tags.return_value = [] + + def mock_resolve(items): + for item in items: + if item.pr_ref in ("#124", "#125"): + item.commit = "sha_" + item.pr_ref.lstrip("#") + item.status = "done" + return items + + self.mock_gh.get_merge_commits_for_prs.side_effect = mock_resolve + + self.mock_git.sort_commits_chronologically.return_value = ["sha_124", "sha_125"] + self.mock_git.get_commit_sha.side_effect = [ + "12345678", + "sha_124_amended", + "sha_125_amended", + "main_sha", + ] + self.mock_git.get_commit_message.return_value = 'Cherry-pick "fix bug"' + self.mock_git.get_modified_files.side_effect = [ + ["news/124.fixed.md"], + ["news/125.fixed.md"], + ] + self.mock_git.diff.side_effect = ["diff 124", "diff 125"] + self.mock_git.apply_check.side_effect = [False, True] + self.mock_gh.create_pr.return_value = "https://github.com/foo/bar/pull/999" + + result = ProcessBackports(args, self.mock_git, self.mock_gh).run() + + self.assertEqual(result, 0) + self.mock_git.fetch.assert_has_calls( + [ + call("origin", tags=True, force=True), + call("origin"), + call("origin", refspec="main"), + ] + ) + self.mock_git.checkout.assert_has_calls( + [ + call("release/2.0", track_remote="origin"), + call("main", track_remote="origin"), + call("prepare-2.0.0-backports-b552a96", create_branch=True), + call("release/2.0"), + ] + ) + self.mock_git.cherry_pick.assert_has_calls( + [ + call("sha_124"), + call("sha_125"), + ] + ) + # diff should be called for each successful cherry-pick + self.assertEqual(self.mock_git.diff.call_count, 2) + # apply_check should be called for both patches + self.assertEqual(self.mock_git.apply_check.call_count, 2) + # apply should only be called for 125 (since 124 failed check) + self.mock_git.apply.assert_called_once_with(unittest.mock.ANY) + + self.mock_changelog_news.update_changelog.assert_has_calls( + [ + call("2.0.0", "2026-07-01"), + call("2.0.0", "2026-07-01"), + call( + "2.0.0", + "2026-07-01", + news_files=["news/124.fixed.md", "news/125.fixed.md"], + delete_news=True, + ), + ] + ) + # add_modified_and_deleted called: + # - once per cherry-pick (2) + # - once on main backport branch (1) + # Total = 3 + self.assertEqual(self.mock_git.add_modified_and_deleted.call_count, 3) + # replace_version_next called once per cherry-pick + self.assertEqual(self.mock_replace_version_next.call_count, 2) + + self.mock_git.commit.assert_has_calls( + [ + call('Cherry-pick "fix bug"\n\nWork towards #123', amend=True), + call('Cherry-pick "fix bug"\n\nWork towards #123', amend=True), + call("chore(release): sync changelog for v2.0.0 backports"), + ] + ) + self.mock_git.push.assert_has_calls( + [ + call("origin", "release/2.0"), + call("origin", "release/2.0"), + call( + "origin", + "prepare-2.0.0-backports-b552a96", + set_upstream=True, + force=True, + ), + ] + ) + + # PR body should contain warning about 124 + expected_body = ( + "Updates CHANGELOG.md and removes news files for backports:\n" + "- #124\n" + "- #125\n" + "\n" + "Warning: These PRs failed to update their version markers:\n" + "- #124\n" + "\n" + "Work towards #123\n" + "Release-Tracking-Issue: #123" + ) + self.mock_gh.create_pr.assert_called_once_with( + title="chore(release): sync changelog for v2.0.0 backports", + body=expected_body, + base="main", + labels=["type: sync-changelog"], + ) + self.mock_gh.enable_auto_merge.assert_called_once_with(999) + + # update_issue_body called 3 times (twice for backports, once for sync tasks) + self.assertEqual(self.mock_gh.update_issue_body.call_count, 3) + call3_args = self.mock_gh.update_issue_body.call_args_list[2][0] + self.assertIn( + "- [ ] Sync Changelog #124 | status=pending pr=#999", call3_args[1] + ) + self.assertIn( + "- [ ] Sync Changelog #125 | status=pending pr=#999", call3_args[1] + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/tools/private/release/promote_rc_test.py b/tests/tools/private/release/promote_rc_test.py new file mode 100644 index 0000000000..b5dff3e478 --- /dev/null +++ b/tests/tools/private/release/promote_rc_test.py @@ -0,0 +1,317 @@ +import argparse +import os +import tempfile +import unittest +from pathlib import Path +from unittest.mock import call, patch + +from tests.tools.private.release.release_test_helper import _mock_git_and_gh +from tools.private.release.gh import NoTrackingIssueError +from tools.private.release.promote_rc import PromoteRc + + +class CmdPromoteRcTest(unittest.TestCase): + def setUp(self): + _mock_git_and_gh(self) + self.test_dir = tempfile.TemporaryDirectory() + self.addCleanup(self.test_dir.cleanup) + + def test_promote_rc_success(self): + # Arrange + args = argparse.Namespace( + version="2.0.0", issue=123, dry_run=False, remote="my-remote" + ) + self.mock_git.get_remote_tags.return_value = ["2.0.0-rc0", "2.0.0-rc1"] + self.mock_git.get_commit_sha.return_value = "abcdef123456" + self.mock_git.tag_exists.return_value = False + initial_body = "- [ ] Tag Final" + self.mock_gh.get_issue_body.return_value = initial_body + + # Act + result = PromoteRc(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 0) + self.mock_git.fetch.assert_has_calls( + [ + call("my-remote", tags=True, force=True), + call("my-remote", refspec="release/2.0"), + ] + ) + self.mock_git.get_commit_sha.assert_has_calls( + [call("2.0.0-rc1"), call("my-remote/release/2.0")] + ) + self.mock_git.checkout.assert_not_called() + self.mock_git.tag_exists.assert_called_once_with("2.0.0") + self.mock_git.tag.assert_called_once_with("2.0.0", "abcdef123456") + self.mock_git.push.assert_called_once_with("my-remote", "2.0.0") + + # Verify issue update + self.mock_gh.get_issue_body.assert_called_once_with(123) + expected_updated_body = ( + "- [x] Tag Final | status=done tag=2.0.0 commit= abcdef12" + ) + self.mock_gh.update_issue_body.assert_called_once_with( + 123, expected_updated_body + ) + expected_comment = ( + "**New Release Tagged!** šŸšŸŒæ\n\n" + "Version **2.0.0** has been successfully generated and tagged on branch [`release/2.0`](https://github.com/bazel-contrib/rules_python/tree/release/2.0).\n\n" + "- [Github Release 2.0.0](https://github.com/bazel-contrib/rules_python/releases/tag/2.0.0)\n" + "- [BCR Entry 2.0.0](https://registry.bazel.build/modules/rules_python/2.0.0)\n" + "- [BCR PRs](https://github.com/bazelbuild/bazel-central-registry/pulls?q=is%3Apr%20%28%22bazel-contrib/rules_python%22%20in%3Atitle%29%20%28%22%402.0.0%22%20in%3Atitle%29)\n" + "- [Release workflow status](https://github.com/bazel-contrib/rules_python/actions/workflows/release_promote_rc.yaml)" + ) + self.mock_gh.post_issue_comment.assert_called_once_with(123, expected_comment) + + def test_promote_rc_writes_github_output(self): + # Arrange + github_output_path = os.path.join(self.test_dir.name, "github_output") + args = argparse.Namespace( + version="2.0.0", issue=123, dry_run=False, remote="my-remote" + ) + self.mock_git.get_remote_tags.return_value = ["2.0.0-rc0", "2.0.0-rc1"] + self.mock_git.get_commit_sha.return_value = "abcdef123456" + self.mock_git.tag_exists.return_value = False + initial_body = "- [ ] Tag Final" + self.mock_gh.get_issue_body.return_value = initial_body + + # Act + with patch.dict("os.environ", {"GITHUB_OUTPUT": github_output_path}): + result = PromoteRc(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 0) + self.assertTrue(os.path.exists(github_output_path)) + content = Path(github_output_path).read_text(encoding="utf-8") + self.assertEqual(content, "version=2.0.0\n") + + def test_promote_rc_resolve_issue_success(self): + # Arrange + args = argparse.Namespace( + version="2.0.0", issue=None, dry_run=False, remote="my-remote" + ) + self.mock_git.get_remote_tags.return_value = ["2.0.0-rc1"] + self.mock_git.tag_exists.return_value = False + self.mock_gh.get_release_tracking_issue.side_effect = None + self.mock_gh.get_release_tracking_issue.return_value = 123 + self.mock_git.get_commit_sha.return_value = "abcdef123456" + initial_body = "- [ ] Tag Final" + self.mock_gh.get_issue_body.return_value = initial_body + + # Act + result = PromoteRc(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 0) + self.mock_git.fetch.assert_has_calls( + [ + call("my-remote", tags=True, force=True), + call("my-remote", refspec="release/2.0"), + ] + ) + self.mock_gh.get_release_tracking_issue.assert_called_once_with("2.0.0") + self.mock_git.get_commit_sha.assert_has_calls( + [call("2.0.0-rc1"), call("my-remote/release/2.0")] + ) + self.mock_git.checkout.assert_not_called() + self.mock_git.tag.assert_called_once_with("2.0.0", "abcdef123456") + self.mock_git.push.assert_called_once_with("my-remote", "2.0.0") + self.mock_gh.get_issue_body.assert_called_once_with(123) + expected_updated_body = ( + "- [x] Tag Final | status=done tag=2.0.0 commit= abcdef12" + ) + self.mock_gh.update_issue_body.assert_called_once_with( + 123, expected_updated_body + ) + expected_comment = ( + "**New Release Tagged!** šŸšŸŒæ\n\n" + "Version **2.0.0** has been successfully generated and tagged on branch [`release/2.0`](https://github.com/bazel-contrib/rules_python/tree/release/2.0).\n\n" + "- [Github Release 2.0.0](https://github.com/bazel-contrib/rules_python/releases/tag/2.0.0)\n" + "- [BCR Entry 2.0.0](https://registry.bazel.build/modules/rules_python/2.0.0)\n" + "- [BCR PRs](https://github.com/bazelbuild/bazel-central-registry/pulls?q=is%3Apr%20%28%22bazel-contrib/rules_python%22%20in%3Atitle%29%20%28%22%402.0.0%22%20in%3Atitle%29)\n" + "- [Release workflow status](https://github.com/bazel-contrib/rules_python/actions/workflows/release_promote_rc.yaml)" + ) + self.mock_gh.post_issue_comment.assert_called_once_with(123, expected_comment) + + def test_promote_rc_resolves_version_from_issue(self): + # Arrange + args = argparse.Namespace( + version=None, issue=123, dry_run=False, remote="my-remote" + ) + self.mock_gh.get_issue_title.return_value = "Release 2.0.1" + self.mock_git.get_remote_tags.return_value = ["2.0.1-rc0"] + self.mock_git.get_commit_sha.return_value = "12345678" + self.mock_git.tag_exists.return_value = False + initial_body = "- [ ] Tag Final" + self.mock_gh.get_issue_body.return_value = initial_body + + # Act + result = PromoteRc(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 0) + self.mock_git.fetch.assert_has_calls( + [ + call("my-remote", tags=True, force=True), + call("my-remote", refspec="release/2.0"), + ] + ) + self.mock_git.get_current_branch.assert_not_called() + self.mock_git.get_tags.assert_not_called() + self.mock_git.get_remote_tags.assert_called_once_with("my-remote") + + self.mock_git.checkout.assert_not_called() + self.mock_git.get_commit_sha.assert_has_calls( + [call("2.0.1-rc0"), call("my-remote/release/2.0")] + ) + self.mock_git.tag.assert_called_once_with("2.0.1", "12345678") + self.mock_git.push.assert_called_once_with("my-remote", "2.0.1") + + expected_updated_body = ( + "- [x] Tag Final | status=done tag=2.0.1 commit= 12345678" + ) + self.mock_gh.update_issue_body.assert_called_once_with( + 123, expected_updated_body + ) + expected_comment = ( + "**New Release Tagged!** šŸšŸŒæ\n\n" + "Version **2.0.1** has been successfully generated and tagged on branch [`release/2.0`](https://github.com/bazel-contrib/rules_python/tree/release/2.0).\n\n" + "- [Github Release 2.0.1](https://github.com/bazel-contrib/rules_python/releases/tag/2.0.1)\n" + "- [BCR Entry 2.0.1](https://registry.bazel.build/modules/rules_python/2.0.1)\n" + "- [BCR PRs](https://github.com/bazelbuild/bazel-central-registry/pulls?q=is%3Apr%20%28%22bazel-contrib/rules_python%22%20in%3Atitle%29%20%28%22%402.0.1%22%20in%3Atitle%29)\n" + "- [Release workflow status](https://github.com/bazel-contrib/rules_python/actions/workflows/release_promote_rc.yaml)" + ) + self.mock_gh.post_issue_comment.assert_called_once_with(123, expected_comment) + + @patch("builtins.print") + def test_promote_rc_dry_run_success(self, mock_print): + # Arrange + args = argparse.Namespace( + version="2.0.0", issue=123, dry_run=True, remote="my-remote" + ) + self.mock_git.get_remote_tags.return_value = ["2.0.0-rc0", "2.0.0-rc1"] + self.mock_git.get_commit_sha.return_value = "abcdef123456" + self.mock_git.tag_exists.return_value = False + initial_body = "- [ ] Tag Final" + self.mock_gh.get_issue_body.return_value = initial_body + + # Act + result = PromoteRc(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 0) + self.mock_git.fetch.assert_has_calls( + [ + call("my-remote", tags=True, force=True), + call("my-remote", refspec="release/2.0"), + ] + ) + self.mock_git.get_commit_sha.assert_has_calls( + [call("2.0.0-rc1"), call("my-remote/release/2.0")] + ) + self.mock_git.tag_exists.assert_called_once_with("2.0.0") + + # Core dry-run assertions: NO modifications + self.mock_git.tag.assert_not_called() + self.mock_git.push.assert_not_called() + self.mock_gh.update_issue_body.assert_not_called() + self.mock_gh.post_issue_comment.assert_not_called() + + mock_print.assert_has_calls( + [ + call("Verifying tracking issue #123 format..."), + call("Fetching remote branch my-remote/release/2.0..."), + call( + "[DRY RUN] Pre-conditions passed successfully for promoting" + " 2.0.0-rc1 to 2.0.0." + ), + call("[DRY RUN] Would tag commit abcdef12 as 2.0.0"), + call("[DRY RUN] Would push tag 2.0.0 to my-remote"), + call("[DRY RUN] Would update tracking issue #123 checklist"), + call("[DRY RUN] Would post comment to tracking issue #123"), + ] + ) + + def test_promote_rc_tag_already_exists(self): + # Arrange + args = argparse.Namespace( + version="2.0.0", issue=123, dry_run=False, remote="my-remote" + ) + self.mock_git.get_remote_tags.return_value = ["2.0.0-rc1"] + self.mock_git.tag_exists.return_value = True + + # Act + result = PromoteRc(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 1) + self.mock_git.checkout.assert_not_called() + self.mock_git.tag.assert_not_called() + self.mock_git.push.assert_not_called() + self.mock_gh.get_issue_body.assert_not_called() + self.mock_gh.update_issue_body.assert_not_called() + + def test_promote_rc_issue_not_found(self): + # Arrange + args = argparse.Namespace( + version="2.0.0", issue=None, dry_run=False, remote="my-remote" + ) + self.mock_git.get_remote_tags.return_value = ["2.0.0-rc1"] + self.mock_git.tag_exists.return_value = False + self.mock_gh.get_release_tracking_issue.side_effect = NoTrackingIssueError( + "Not found" + ) + + # Act + result = PromoteRc(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 1) + self.mock_gh.get_release_tracking_issue.assert_called_once_with("2.0.0") + self.mock_git.checkout.assert_not_called() + self.mock_git.tag.assert_not_called() + self.mock_git.push.assert_not_called() + self.mock_gh.get_issue_body.assert_not_called() + + def test_promote_rc_issue_malformed(self): + # Arrange + args = argparse.Namespace( + version="2.0.0", issue=123, dry_run=False, remote="my-remote" + ) + self.mock_git.get_remote_tags.return_value = ["2.0.0-rc1"] + self.mock_git.tag_exists.return_value = False + self.mock_git.get_commit_sha.return_value = "abcdef123456" + initial_body = "malformed body" + self.mock_gh.get_issue_body.return_value = initial_body + + # Act + result = PromoteRc(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 1) + self.mock_gh.get_issue_body.assert_called_once_with(123) + self.mock_git.checkout.assert_not_called() + self.mock_git.tag.assert_not_called() + self.mock_git.push.assert_not_called() + self.mock_gh.update_issue_body.assert_not_called() + + def test_promote_rc_no_rc_found(self): + # Arrange + args = argparse.Namespace( + version="2.0.0", issue=123, dry_run=False, remote="my-remote" + ) + self.mock_git.get_remote_tags.return_value = [] + + # Act + result = PromoteRc(args, self.mock_git, self.mock_gh).run() + + # Assert + self.assertEqual(result, 1) + self.mock_git.checkout.assert_not_called() + self.mock_git.tag.assert_not_called() + self.mock_gh.get_issue_body.assert_not_called() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/tools/private/release/release_issue_test.py b/tests/tools/private/release/release_issue_test.py new file mode 100644 index 0000000000..36f0dbd704 --- /dev/null +++ b/tests/tools/private/release/release_issue_test.py @@ -0,0 +1,162 @@ +import unittest + +from tools.private.release.release_issue import ( + add_backports_to_body, + add_sync_changelog_task_to_body, + format_metadata_line, + parse_checklist_state, + parse_metadata_line, +) + + +class ReleaseIssueTest(unittest.TestCase): + def test_parse_metadata_line_spaces(self): + # Test with spaces around '=' + line = "- [ ] Tag Final | tag = 2.0.0 commit = abcdef12" + expected = { + "checked": False, + "name": "Tag Final", + "metadata": {"tag": "2.0.0", "commit": "abcdef12"}, + "original_line": line, + } + self.assertEqual(parse_metadata_line(line), expected) + + # Test with spaces after '=' + line = "- [ ] Tag Final | tag= 2.0.0 commit= abcdef12" + expected = { + "checked": False, + "name": "Tag Final", + "metadata": {"tag": "2.0.0", "commit": "abcdef12"}, + "original_line": line, + } + self.assertEqual(parse_metadata_line(line), expected) + + # Test with standard format (no spaces) + line = "- [ ] Tag Final | tag=2.0.0 commit=abcdef12" + expected = { + "checked": False, + "name": "Tag Final", + "metadata": {"tag": "2.0.0", "commit": "abcdef12"}, + "original_line": line, + } + self.assertEqual(parse_metadata_line(line), expected) + + # Test with no metadata + line = "- [ ] Tag Final" + expected = { + "checked": False, + "name": "Tag Final", + "metadata": {}, + "original_line": line, + } + self.assertEqual(parse_metadata_line(line), expected) + + def test_format_metadata_line(self): + # Test with commit metadata (should have space) + metadata = {"status": "done", "tag": "2.0.0", "commit": "abcdef12"} + expected = "- [x] Tag Final | status=done tag=2.0.0 commit= abcdef12" + self.assertEqual(format_metadata_line(True, "Tag Final", metadata), expected) + + # Test with other metadata (should not have space) + metadata = {"status": "done", "pr": "#122"} + expected = "- [x] Prepare Release | status=done pr=#122" + self.assertEqual( + format_metadata_line(True, "Prepare Release", metadata), expected + ) + + # Test with no metadata + expected = "- [ ] Tag Final" + self.assertEqual(format_metadata_line(False, "Tag Final", {}), expected) + + def test_add_backports_to_body(self): + body = """ +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch +- [ ] Tag Final + +## Backports +- [ ] #123 | status=done +""" + items = [ + {"ref": "124"}, + {"ref": "#124"}, + {"ref": "125"}, + {"ref": "#123"}, + ] + updated_body = add_backports_to_body(body, items) + expected_body = """ +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch +- [ ] Tag Final + +## Backports +- [ ] #123 | status=done +- [ ] #124 +- [ ] #125 +""" + self.assertEqual(updated_body.strip(), expected_body.strip()) + + def test_add_sync_changelog_task_to_body(self): + body = """ +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch +- [ ] Tag Final +""" + # Insert first task (should go before Tag Final) + body = add_sync_changelog_task_to_body(body, 124) + expected = """ +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch +- [ ] Sync Changelog #124 +- [ ] Tag Final +""" + self.assertEqual(body.strip(), expected.strip()) + + # Insert second task (should go after the last Sync Changelog task) + body = add_sync_changelog_task_to_body(body, 125) + expected = """ +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch +- [ ] Sync Changelog #124 +- [ ] Sync Changelog #125 +- [ ] Tag Final +""" + self.assertEqual(body.strip(), expected.strip()) + + # Insert duplicate (should be ignored) + body = add_sync_changelog_task_to_body(body, 124) + self.assertEqual(body.strip(), expected.strip()) + + def test_parse_checklist_state_with_sync_changelogs(self): + body = """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [x] Create Release branch | status=done branch=release/2.0 +- [ ] Sync Changelog #124 | status=pending pr=#125 +- [ ] Sync Changelog #126 +- [ ] Tag Final +""" + state = parse_checklist_state(body) + self.assertIn(124, state["sync_changelogs"]) + self.assertIn(126, state["sync_changelogs"]) + + task_124 = state["sync_changelogs"][124] + self.assertEqual(task_124.name, "Sync Changelog #124") + self.assertFalse(task_124.checked) + self.assertEqual(task_124.status, "pending") + self.assertEqual(task_124.pr, "#125") + + task_126 = state["sync_changelogs"][126] + self.assertEqual(task_126.name, "Sync Changelog #126") + self.assertFalse(task_126.checked) + self.assertIsNone(task_126.status) + self.assertIsNone(task_126.pr) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/tools/private/release/release_test.py b/tests/tools/private/release/release_test.py index 72a9a05cd6..c7ba1b17b7 100644 --- a/tests/tools/private/release/release_test.py +++ b/tests/tools/private/release/release_test.py @@ -1,213 +1,24 @@ -import datetime -import os -import pathlib -import shutil -import tempfile import unittest -from unittest.mock import patch from tools.private.release import release as releaser -_UNRELEASED_TEMPLATE = """ - -""" - - -class ReleaserTest(unittest.TestCase): - def setUp(self): - self.tmpdir = pathlib.Path(tempfile.mkdtemp()) - self.original_cwd = os.getcwd() - self.addCleanup(shutil.rmtree, self.tmpdir) - - os.chdir(self.tmpdir) - # NOTE: On windows, this must be done before files are deleted. - self.addCleanup(os.chdir, self.original_cwd) - - def test_update_changelog(self): - changelog = f""" -# Changelog - -{_UNRELEASED_TEMPLATE} - -{{#v0-0-0}} -## Unreleased - -[0.0.0]: https://github.com/bazel-contrib/rules_python/releases/tag/0.0.0 - -{{#v0-0-0-changed}} -### Changed -* Nothing changed - -{{#v0-0-0-fixed}} -### Fixed -* Nothing fixed - -{{#v0-0-0-added}} -### Added -* Nothing added - -{{#v0-0-0-removed}} -### Removed -* Nothing removed. -""" - changelog_path = self.tmpdir / "CHANGELOG.md" - changelog_path.write_text(changelog) - - # Act - releaser.update_changelog( - "1.23.4", - "2025-01-01", - changelog_path=changelog_path, - ) - - # Assert - new_content = changelog_path.read_text() - - self.assertIn( - _UNRELEASED_TEMPLATE, new_content, msg=f"ACTUAL:\n\n{new_content}\n\n" - ) - self.assertIn(f"## [1.23.4] - 2025-01-01", new_content) - self.assertIn( - f"[1.23.4]: https://github.com/bazel-contrib/rules_python/releases/tag/1.23.4", - new_content, - ) - self.assertIn("{#v1-23-4}", new_content) - self.assertIn("{#v1-23-4-changed}", new_content) - self.assertIn("{#v1-23-4-fixed}", new_content) - self.assertIn("{#v1-23-4-added}", new_content) - self.assertIn("{#v1-23-4-removed}", new_content) - - def test_replace_version_next(self): - # Arrange - mock_file_content = """ -:::{versionadded} VERSION_NEXT_FEATURE -blabla -::: - -:::{versionchanged} VERSION_NEXT_PATCH -blabla -::: -""" - (self.tmpdir / "mock_file.bzl").write_text(mock_file_content) - - releaser.replace_version_next("0.28.0") - - new_content = (self.tmpdir / "mock_file.bzl").read_text() - - self.assertIn(":::{versionadded} 0.28.0", new_content) - self.assertIn(":::{versionadded} 0.28.0", new_content) - self.assertNotIn("VERSION_NEXT_FEATURE", new_content) - self.assertNotIn("VERSION_NEXT_PATCH", new_content) - - def test_replace_version_next_excludes_bazel_dirs(self): - # Arrange - mock_file_content = """ -:::{versionadded} VERSION_NEXT_FEATURE -blabla -::: -""" - bazel_dir = self.tmpdir / "bazel-rules_python" - bazel_dir.mkdir() - (bazel_dir / "mock_file.bzl").write_text(mock_file_content) - - tools_dir = self.tmpdir / "tools" / "private" / "release" - tools_dir.mkdir(parents=True) - (tools_dir / "mock_file.bzl").write_text(mock_file_content) - - tests_dir = self.tmpdir / "tests" / "tools" / "private" / "release" - tests_dir.mkdir(parents=True) - (tests_dir / "mock_file.bzl").write_text(mock_file_content) - - version = "0.28.0" - - # Act - releaser.replace_version_next(version) - - # Assert - new_content = (bazel_dir / "mock_file.bzl").read_text() - self.assertIn("VERSION_NEXT_FEATURE", new_content) - - new_content = (tools_dir / "mock_file.bzl").read_text() - self.assertIn("VERSION_NEXT_FEATURE", new_content) - - new_content = (tests_dir / "mock_file.bzl").read_text() - self.assertIn("VERSION_NEXT_FEATURE", new_content) +class ReleaseCLITest(unittest.TestCase): def test_valid_version(self): # These should not raise an exception - releaser.create_parser().parse_args(["0.28.0"]) - releaser.create_parser().parse_args(["1.0.0"]) - releaser.create_parser().parse_args(["1.2.3rc4"]) + releaser.create_parser().parse_args(["prepare", "0.28.0"]) + releaser.create_parser().parse_args( + ["promote-rc", "1.0.0", "--remote", "origin"] + ) + releaser.create_parser().parse_args( + ["create-release-issue", "--version", "1.2.3rc4"] + ) def test_invalid_version(self): with self.assertRaises(SystemExit): - releaser.create_parser().parse_args(["0.28"]) + releaser.create_parser().parse_args(["prepare", "0.28"]) with self.assertRaises(SystemExit): - releaser.create_parser().parse_args(["a.b.c"]) - - -class GetLatestVersionTest(unittest.TestCase): - @patch("tools.private.release.release._get_git_tags") - def test_get_latest_version_success(self, mock_get_tags): - mock_get_tags.return_value = ["0.1.0", "1.0.0", "0.2.0"] - self.assertEqual(releaser.get_latest_version(), "1.0.0") - - @patch("tools.private.release.release._get_git_tags") - def test_get_latest_version_rc_is_latest(self, mock_get_tags): - mock_get_tags.return_value = ["0.1.0", "1.0.0", "1.1.0rc0"] - with self.assertRaisesRegex( - ValueError, "The latest version is a pre-release version: 1.1.0rc0" - ): - releaser.get_latest_version() - - @patch("tools.private.release.release._get_git_tags") - def test_get_latest_version_no_tags(self, mock_get_tags): - mock_get_tags.return_value = [] - with self.assertRaisesRegex( - RuntimeError, "No git tags found matching X.Y.Z or X.Y.ZrcN format." - ): - releaser.get_latest_version() - - @patch("tools.private.release.release._get_git_tags") - def test_get_latest_version_no_matching_tags(self, mock_get_tags): - mock_get_tags.return_value = ["v1.0", "latest"] - with self.assertRaisesRegex( - RuntimeError, "No git tags found matching X.Y.Z or X.Y.ZrcN format." - ): - releaser.get_latest_version() - - @patch("tools.private.release.release._get_git_tags") - def test_get_latest_version_only_rc_tags(self, mock_get_tags): - mock_get_tags.return_value = ["1.0.0rc0", "1.1.0rc0"] - with self.assertRaisesRegex( - ValueError, "The latest version is a pre-release version: 1.1.0rc0" - ): - releaser.get_latest_version() + releaser.create_parser().parse_args(["prepare", "a.b.c"]) if __name__ == "__main__": diff --git a/tests/tools/private/release/release_test_helper.py b/tests/tools/private/release/release_test_helper.py new file mode 100644 index 0000000000..c35cd0fa40 --- /dev/null +++ b/tests/tools/private/release/release_test_helper.py @@ -0,0 +1,46 @@ +import os +import pathlib +import shutil +import tempfile +import unittest +from unittest.mock import MagicMock, patch + +from tools.private.release.gh import ( + MultipleTrackingIssuesError, + NoTrackingIssueError, +) + + +def _mock_git_and_gh(test_case): + mock_git = MagicMock() + mock_gh = MagicMock() + test_case.mock_git = mock_git + test_case.mock_gh = mock_gh + + # Mock Git inside utils.py since it instantiates it locally + patch("tools.private.release.utils.Git", return_value=mock_git).start() + + mock_gh.MultipleTrackingIssuesError = MultipleTrackingIssuesError + mock_gh.NoTrackingIssueError = NoTrackingIssueError + + test_case.addCleanup(patch.stopall) + + # Apply safe defaults + mock_git.get_current_branch.return_value = None + mock_git.get_tags.return_value = [] + mock_git.get_remote_tags.return_value = [] + + mock_git.status.return_value = "" + mock_git.branch_exists.return_value = False + mock_git.tag_exists.return_value = False + mock_gh.get_release_tracking_issue.side_effect = NoTrackingIssueError("Not found") + mock_gh.get_open_pr.return_value = None + + +class TempDirTestCase(unittest.TestCase): + def setUp(self): + self.tmpdir = pathlib.Path(tempfile.mkdtemp()) + self.original_cwd = os.getcwd() + self.addCleanup(shutil.rmtree, self.tmpdir) + os.chdir(self.tmpdir) + self.addCleanup(os.chdir, self.original_cwd) diff --git a/tests/tools/private/release/utils_test.py b/tests/tools/private/release/utils_test.py new file mode 100644 index 0000000000..796bab1619 --- /dev/null +++ b/tests/tools/private/release/utils_test.py @@ -0,0 +1,266 @@ +import unittest +from unittest.mock import patch + +from tests.tools.private.release.release_test_helper import TempDirTestCase +from tools.private.release import utils + + +class GetLatestVersionTest(unittest.TestCase): + @patch("tools.private.release.git.Git.get_tags") + def test_get_latest_version_success(self, mock_get_tags): + mock_get_tags.return_value = ["0.1.0", "1.0.0", "0.2.0"] + self.assertEqual(utils.get_latest_version(), "1.0.0") + + @patch("tools.private.release.git.Git.get_tags") + def test_get_latest_version_rc_is_latest(self, mock_get_tags): + mock_get_tags.return_value = ["0.1.0", "1.0.0", "1.1.0rc0"] + with self.assertRaisesRegex( + ValueError, "The latest version is a pre-release version: 1.1.0rc0" + ): + utils.get_latest_version() + + @patch("tools.private.release.git.Git.get_tags") + def test_get_latest_version_no_tags(self, mock_get_tags): + mock_get_tags.return_value = [] + with self.assertRaisesRegex( + RuntimeError, "No git tags found matching X.Y.Z or X.Y.ZrcN format." + ): + utils.get_latest_version() + + @patch("tools.private.release.git.Git.get_tags") + def test_get_latest_version_no_matching_tags(self, mock_get_tags): + mock_get_tags.return_value = ["v1.0", "latest"] + with self.assertRaisesRegex( + RuntimeError, "No git tags found matching X.Y.Z or X.Y.ZrcN format." + ): + utils.get_latest_version() + + @patch("tools.private.release.git.Git.get_tags") + def test_get_latest_version_only_rc_tags(self, mock_get_tags): + mock_get_tags.return_value = ["1.0.0rc0", "1.1.0rc0"] + with self.assertRaisesRegex( + ValueError, "The latest version is a pre-release version: 1.1.0rc0" + ): + utils.get_latest_version() + + +class GetLatestRcTagTest(unittest.TestCase): + @patch("tools.private.release.git.Git.get_tags") + def test_get_latest_rc_tag_no_tags(self, mock_get_tags): + mock_get_tags.return_value = [] + self.assertIsNone(utils.get_latest_rc_tag("2.0.0")) + + @patch("tools.private.release.git.Git.get_tags") + def test_get_latest_rc_tag_no_matching_tags(self, mock_get_tags): + mock_get_tags.return_value = [ + "1.0.0", + "2.0.0", + "v2.0.0-rc0", + "2.1.0-rc0", + ] + self.assertIsNone(utils.get_latest_rc_tag("2.0.0")) + + @patch("tools.private.release.git.Git.get_tags") + def test_get_latest_rc_tag_success(self, mock_get_tags): + mock_get_tags.return_value = [ + "2.0.0-rc0", + "2.0.0-rc2", + "2.0.0-rc1", + "2.1.0-rc0", + ] + self.assertEqual(utils.get_latest_rc_tag("2.0.0"), "2.0.0-rc2") + + @patch("tools.private.release.git.Git.get_tags") + def test_get_latest_rc_tag_ignores_v_prefix(self, mock_get_tags): + mock_get_tags.return_value = ["v2.0.0-rc0", "2.0.0-rc1"] + self.assertEqual(utils.get_latest_rc_tag("2.0.0"), "2.0.0-rc1") + + @patch("tools.private.release.git.Git.get_remote_tags") + def test_get_latest_rc_tag_remote_success(self, mock_get_remote_tags): + mock_get_remote_tags.return_value = [ + "2.0.0-rc0", + "2.0.0-rc2", + "2.0.0-rc1", + "2.1.0-rc0", + ] + self.assertEqual(utils.get_latest_rc_tag("2.0.0", remote="origin"), "2.0.0-rc2") + mock_get_remote_tags.assert_called_once_with("origin") + + +class DetermineNextVersionTest(TempDirTestCase): + def setUp(self): + super().setUp() + self.mock_get_latest_version = patch( + "tools.private.release.utils.get_latest_version" + ).start() + self.mock_get_current_branch = patch( + "tools.private.release.git.Git.get_current_branch" + ).start() + self.mock_get_current_branch.return_value = "main" + self.addCleanup(patch.stopall) + + def test_no_markers(self): + (self.tmpdir / "mock_file.bzl").write_text("no markers here") + self.mock_get_latest_version.return_value = "1.2.3" + + next_version = utils.determine_next_version() + + self.assertEqual(next_version, "1.2.4") + + def test_only_patch(self): + (self.tmpdir / "mock_file.bzl").write_text( + ":::{versionchanged} VERSION_NEXT_PATCH" + ) + self.mock_get_latest_version.return_value = "1.2.3" + + next_version = utils.determine_next_version() + + self.assertEqual(next_version, "1.2.4") + + def test_only_feature(self): + (self.tmpdir / "mock_file.bzl").write_text( + ":::{versionadded} VERSION_NEXT_FEATURE" + ) + self.mock_get_latest_version.return_value = "1.2.3" + + next_version = utils.determine_next_version() + + self.assertEqual(next_version, "1.3.0") + + def test_both_markers(self): + (self.tmpdir / "mock_file_patch.bzl").write_text( + ":::{versionchanged} VERSION_NEXT_PATCH" + ) + (self.tmpdir / "mock_file_feature.bzl").write_text( + ":::{versionadded} VERSION_NEXT_FEATURE" + ) + self.mock_get_latest_version.return_value = "1.2.3" + + next_version = utils.determine_next_version() + + self.assertEqual(next_version, "1.3.0") + + @patch("tools.private.release.git.Git.get_current_branch") + @patch("tools.private.release.git.Git.get_tags") + def test_determine_next_version_on_release_branch_with_existing_tags( + self, mock_get_tags, mock_get_branch + ): + mock_get_branch.return_value = "release/0.37" + mock_get_tags.return_value = ["0.37.0", "0.37.1", "0.36.0"] + + next_version = utils.determine_next_version() + + self.assertEqual(next_version, "0.37.2") + + @patch("tools.private.release.git.Git.get_current_branch") + @patch("tools.private.release.git.Git.get_tags") + def test_determine_next_version_on_release_branch_no_tags( + self, mock_get_tags, mock_get_branch + ): + mock_get_branch.return_value = "release/0.38" + mock_get_tags.return_value = ["0.37.0"] # No 0.38.x tags + + next_version = utils.determine_next_version() + + self.assertEqual(next_version, "0.38.0") + + @patch("tools.private.release.git.Git.get_current_branch") + @patch("tools.private.release.git.Git.get_tags") + def test_determine_next_version_on_release_branch_with_active_rc( + self, mock_get_tags, mock_get_branch + ): + mock_get_branch.return_value = "release/0.37" + # 0.37.0-rc0 and rc1 exist, but no stable 0.37.0 yet + mock_get_tags.return_value = ["0.37.0-rc0", "0.37.0-rc1", "0.36.0"] + + next_version = utils.determine_next_version() + + # Should target 0.37.0, not 0.37.1 + self.assertEqual(next_version, "0.37.0") + + @patch("tools.private.release.git.Git.get_current_branch") + @patch("tools.private.release.git.Git.get_tags") + def test_determine_next_version_on_release_branch_with_stable_and_active_patch_rc( + self, mock_get_tags, mock_get_branch + ): + mock_get_branch.return_value = "release/0.37" + # 0.37.0 stable exists, and 0.37.1-rc0 exists (but no stable 0.37.1 yet) + mock_get_tags.return_value = ["0.37.0", "0.37.1-rc0", "0.36.0"] + + next_version = utils.determine_next_version() + + # Should target 0.37.1, not 0.37.2 + self.assertEqual(next_version, "0.37.1") + + @patch("tools.private.release.git.Git.get_current_branch") + def test_determine_next_version_on_main_branch_fallback(self, mock_get_branch): + mock_get_branch.return_value = "main" + # Should fallback to default behavior (which uses mock_get_latest_version from setUp) + self.mock_get_latest_version.return_value = "1.2.3" + (self.tmpdir / "mock_file.bzl").write_text("no markers here") + + next_version = utils.determine_next_version() + + self.assertEqual(next_version, "1.2.4") + + +class ReplaceVersionNextTest(TempDirTestCase): + def test_replace_version_next(self): + # Arrange + mock_file_content = """ +:::{versionadded} VERSION_NEXT_FEATURE +blabla +::: + +:::{versionchanged} VERSION_NEXT_PATCH +blabla +::: +""" + (self.tmpdir / "mock_file.bzl").write_text(mock_file_content) + + utils.replace_version_next("0.28.0") + + new_content = (self.tmpdir / "mock_file.bzl").read_text() + + self.assertIn(":::{versionadded} 0.28.0", new_content) + self.assertIn(":::{versionadded} 0.28.0", new_content) + self.assertNotIn("VERSION_NEXT_FEATURE", new_content) + self.assertNotIn("VERSION_NEXT_PATCH", new_content) + + def test_replace_version_next_excludes_bazel_dirs(self): + # Arrange + mock_file_content = """ +:::{versionadded} VERSION_NEXT_FEATURE +blabla +::: +""" + bazel_dir = self.tmpdir / "bazel-rules_python" + bazel_dir.mkdir() + (bazel_dir / "mock_file.bzl").write_text(mock_file_content) + + tools_dir = self.tmpdir / "tools" / "private" / "release" + tools_dir.mkdir(parents=True) + (tools_dir / "mock_file.bzl").write_text(mock_file_content) + + tests_dir = self.tmpdir / "tests" / "tools" / "private" / "release" + tests_dir.mkdir(parents=True) + (tests_dir / "mock_file.bzl").write_text(mock_file_content) + + version = "0.28.0" + + # Act + utils.replace_version_next(version) + + # Assert + new_content = (bazel_dir / "mock_file.bzl").read_text() + self.assertIn("VERSION_NEXT_FEATURE", new_content) + + new_content = (tools_dir / "mock_file.bzl").read_text() + self.assertIn("VERSION_NEXT_FEATURE", new_content) + + new_content = (tests_dir / "mock_file.bzl").read_text() + self.assertIn("VERSION_NEXT_FEATURE", new_content) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/tools/wheelmaker_test.py b/tests/tools/wheelmaker_test.py index 0efe1c9fbc..cc160869df 100644 --- a/tests/tools/wheelmaker_test.py +++ b/tests/tools/wheelmaker_test.py @@ -1,37 +1,174 @@ +import io import unittest +from dataclasses import dataclass, field import tools.wheelmaker as wheelmaker +class QuoteAllFilenamesTest(unittest.TestCase): + """Tests for quote_all_filenames behavior in _WhlFile. + + Some wheels (like torch) have all filenames quoted in their RECORD file. + When repacking, we preserve this style to minimize diffs. + """ + + def _make_whl_file(self, quote_all: bool) -> wheelmaker._WhlFile: + """Create a _WhlFile instance for testing.""" + buf = io.BytesIO() + return wheelmaker._WhlFile( + buf, + mode="w", + distribution_prefix="test-1.0.0", + quote_all_filenames=quote_all, + ) + + def test_quote_all_quotes_simple_filenames(self) -> None: + """When quote_all_filenames=True, all filenames are quoted.""" + whl = self._make_whl_file(quote_all=True) + self.assertEqual(whl._quote_filename("foo/bar.py"), '"foo/bar.py"') + + def test_quote_all_false_leaves_simple_filenames_unquoted(self) -> None: + """When quote_all_filenames=False, simple filenames stay unquoted.""" + whl = self._make_whl_file(quote_all=False) + self.assertEqual(whl._quote_filename("foo/bar.py"), "foo/bar.py") + + def test_quote_all_quotes_filenames_with_commas(self) -> None: + """Filenames with commas are always quoted, regardless of quote_all_filenames.""" + whl = self._make_whl_file(quote_all=True) + self.assertEqual(whl._quote_filename("foo,bar/baz.py"), '"foo,bar/baz.py"') + + whl = self._make_whl_file(quote_all=False) + self.assertEqual(whl._quote_filename("foo,bar/baz.py"), '"foo,bar/baz.py"') + + +@dataclass +class ArcNameTestCase: + name: str + expected: str + distribution_prefix: str = "" + strip_path_prefixes: list[str] = field(default_factory=list) + add_path_prefix: str = "" + + class ArcNameFromTest(unittest.TestCase): def test_arcname_from(self) -> None: - # (name, distribution_prefix, strip_path_prefixes, want) tuples - checks = [ - ("a/b/c/file.py", "", [], "a/b/c/file.py"), - ("a/b/c/file.py", "", ["a"], "/b/c/file.py"), - ("a/b/c/file.py", "", ["a/b/"], "c/file.py"), + test_cases = [ + ArcNameTestCase(name="a/b/c/file.py", expected="a/b/c/file.py"), + ArcNameTestCase( + name="a/b/c/file.py", + strip_path_prefixes=["a"], + expected="/b/c/file.py", + ), + ArcNameTestCase( + name="a/b/c/file.py", + strip_path_prefixes=["a/b/"], + expected="c/file.py", + ), # only first found is used and it's not cumulative. - ("a/b/c/file.py", "", ["a/", "b/"], "b/c/file.py"), + ArcNameTestCase( + name="a/b/c/file.py", + strip_path_prefixes=["a/", "b/"], + expected="b/c/file.py", + ), # Examples from docs - ("foo/bar/baz/file.py", "", ["foo", "foo/bar/baz"], "/bar/baz/file.py"), - ("foo/bar/baz/file.py", "", ["foo/bar/baz", "foo"], "/file.py"), - ("foo/file2.py", "", ["foo/bar/baz", "foo"], "/file2.py"), + ArcNameTestCase( + name="foo/bar/baz/file.py", + strip_path_prefixes=["foo", "foo/bar/baz"], + expected="/bar/baz/file.py", + ), + ArcNameTestCase( + name="foo/bar/baz/file.py", + strip_path_prefixes=["foo/bar/baz", "foo"], + expected="/file.py", + ), + ArcNameTestCase( + name="foo/file2.py", + strip_path_prefixes=["foo/bar/baz", "foo"], + expected="/file2.py", + ), # Files under the distribution prefix (eg mylib-1.0.0-dist-info) # are unmodified - ("mylib-0.0.1-dist-info/WHEEL", "mylib", [], "mylib-0.0.1-dist-info/WHEEL"), - ("mylib/a/b/c/WHEEL", "mylib", ["mylib"], "mylib/a/b/c/WHEEL"), + ArcNameTestCase( + name="mylib-0.0.1-dist-info/WHEEL", + distribution_prefix="mylib", + expected="mylib-0.0.1-dist-info/WHEEL", + ), + ArcNameTestCase( + name="mylib/a/b/c/WHEEL", + distribution_prefix="mylib", + strip_path_prefixes=["mylib"], + expected="mylib/a/b/c/WHEEL", + ), + # Check that prefixes are added + ArcNameTestCase( + name="a/b/c/file.py", + add_path_prefix="namespace/", + expected="namespace/a/b/c/file.py", + ), + ArcNameTestCase( + name="a/b/c/file.py", + strip_path_prefixes=["a"], + add_path_prefix="namespace", + expected="namespace/b/c/file.py", + ), + ArcNameTestCase( + name="a/b/c/file.py", + strip_path_prefixes=["a/b/"], + add_path_prefix="namespace_", + expected="namespace_c/file.py", + ), ] - for name, prefix, strip, want in checks: + for test_case in test_cases: with self.subTest( - name=name, - distribution_prefix=prefix, - strip_path_prefixes=strip, - want=want, + name=test_case.name, + distribution_prefix=test_case.distribution_prefix, + strip_path_prefixes=test_case.strip_path_prefixes, + add_path_prefix=test_case.add_path_prefix, + want=test_case.expected, ): got = wheelmaker.arcname_from( - name=name, distribution_prefix=prefix, strip_path_prefixes=strip + name=test_case.name, + distribution_prefix=test_case.distribution_prefix, + strip_path_prefixes=test_case.strip_path_prefixes, + add_path_prefix=test_case.add_path_prefix, ) - self.assertEqual(got, want) + self.assertEqual(got, test_case.expected) + + +class GetNewRequirementLineTest(unittest.TestCase): + def test_requirement(self): + result = wheelmaker.get_new_requirement_line("requests>=2.0", "") + self.assertEqual(result, "Requires-Dist: requests>=2.0") + + def test_requirement_and_extra(self): + result = wheelmaker.get_new_requirement_line("requests>=2.0", "extra=='dev'") + self.assertEqual(result, "Requires-Dist: requests>=2.0; extra=='dev'") + + def test_requirement_with_url(self): + result = wheelmaker.get_new_requirement_line( + "requests @ git+https://github.com/psf/requests.git@3aa6386c3", "" + ) + self.assertEqual( + result, + "Requires-Dist: requests @ git+https://github.com/psf/requests.git@3aa6386c3", + ) + + def test_requirement_with_marker(self): + result = wheelmaker.get_new_requirement_line( + "requests>=2.0; python_version>='3.6'", "" + ) + self.assertEqual( + result, 'Requires-Dist: requests>=2.0; python_version >= "3.6"' + ) + + def test_requirement_with_marker_and_extra(self): + result = wheelmaker.get_new_requirement_line( + "requests>=2.0; python_version>='3.6'", "extra=='dev'" + ) + self.assertEqual( + result, + "Requires-Dist: requests>=2.0; (python_version >= \"3.6\") and extra=='dev'", + ) if __name__ == "__main__": diff --git a/tests/tools/zipapp/BUILD.bazel b/tests/tools/zipapp/BUILD.bazel new file mode 100644 index 0000000000..b71e9b2589 --- /dev/null +++ b/tests/tools/zipapp/BUILD.bazel @@ -0,0 +1,19 @@ +load("//python:py_test.bzl", "py_test") + +py_test( + name = "zipper_test", + srcs = ["zipper_test.py"], + deps = ["//tools/private/zipapp:zipper_lib"], +) + +py_test( + name = "exe_zip_maker_test", + srcs = ["exe_zip_maker_test.py"], + deps = ["//tools/private/zipapp:exe_zip_maker_lib"], +) + +py_test( + name = "zip_main_maker_test", + srcs = ["zip_main_maker_test.py"], + deps = ["//tools/private/zipapp:zip_main_maker_lib"], +) diff --git a/tests/tools/zipapp/exe_zip_maker_test.py b/tests/tools/zipapp/exe_zip_maker_test.py new file mode 100644 index 0000000000..73c509bdbe --- /dev/null +++ b/tests/tools/zipapp/exe_zip_maker_test.py @@ -0,0 +1,71 @@ +import hashlib +import pathlib +import shutil +import stat +import tempfile +import unittest + +from tools.private.zipapp import exe_zip_maker + + +class ExeZipMakerTest(unittest.TestCase): + def setUp(self): + self.test_dir = pathlib.Path(tempfile.mkdtemp()) + self.preamble_path = self.test_dir / "preamble.txt" + self.zip_path = self.test_dir / "data.zip" + self.output_path = self.test_dir / "output.exe" + + def tearDown(self): + shutil.rmtree(self.test_dir) + + def assertStartsWith(self, actual, expected): + if not actual.startswith(expected): + self.fail(f"{actual!r} does not start with {expected!r}") + + def test_create_exe_zip(self): + # Create dummy zip file + zip_content = b"PK\x03\x04dummyzipcontent" + self.zip_path.write_bytes(zip_content) + + # Calculate expected hash + expected_hash = hashlib.sha256(zip_content).hexdigest().encode("utf-8") + + # Create preamble with placeholder + preamble_text = b"#!/bin/bash\nEXPECTED_HASH='%ZIP_HASH%'\n# ... logic ...\n" + self.preamble_path.write_bytes(preamble_text) + + # Call create_exe_zip directly + exe_zip_maker.create_exe_zip( + str(self.preamble_path), str(self.zip_path), str(self.output_path) + ) + + # Verify output exists + self.assertTrue( + self.output_path.exists(), + msg=f"Output path '{self.output_path}' should exist", + ) + + # Verify executable bit + st = self.output_path.stat() + self.assertTrue( + st.st_mode & stat.S_IEXEC, + msg=f"Output path '{self.output_path}' should be executable", + ) + + # Verify content + content = self.output_path.read_bytes() + + # Split content back into preamble and zip + # We know the preamble text length after substitution. + expected_preamble = preamble_text.replace(b"%ZIP_HASH%", expected_hash) + + self.assertStartsWith(content, expected_preamble) + self.assertTrue( + content.endswith(zip_content), + msg="Output content should end with the zip content", + ) + self.assertEqual(len(content), len(expected_preamble) + len(zip_content)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/tools/zipapp/zip_main_maker_test.py b/tests/tools/zipapp/zip_main_maker_test.py new file mode 100644 index 0000000000..dd8e8e8029 --- /dev/null +++ b/tests/tools/zipapp/zip_main_maker_test.py @@ -0,0 +1,101 @@ +import hashlib +import os +import tempfile +import unittest +from unittest import mock + +from tools.private.zipapp import zip_main_maker + + +class ZipMainMakerTest(unittest.TestCase): + def setUp(self): + self.temp_dir = tempfile.TemporaryDirectory() + self.addCleanup(self.temp_dir.cleanup) + + def test_creates_zip_main(self): + template_path = os.path.join(self.temp_dir.name, "template.py") + with open(template_path, "w", encoding="utf-8") as f: + f.write("hash=%APP_HASH%\nfoo=%FOO%\n") + + output_path = os.path.join(self.temp_dir.name, "output.py") + + file1_path = os.path.join(self.temp_dir.name, "file1.txt") + with open(file1_path, "wb") as f: + f.write(b"content1") + + file2_path = os.path.join(self.temp_dir.name, "file2.txt") + with open(file2_path, "wb") as f: + f.write(b"content2") + + # Add a symlink to test symlink hashing + symlink_path = os.path.join(self.temp_dir.name, "symlink.txt") + os.symlink(file1_path, symlink_path) + + manifest_path = os.path.join(self.temp_dir.name, "manifest.txt") + with open(manifest_path, "w", encoding="utf-8") as f: + f.write(f"rf-file|0|file1.txt|{file1_path}\n") + f.write(f"rf-file|0|file2.txt|{file2_path}\n") + f.write(f"rf-symlink|1|symlink.txt|{symlink_path}\n") + f.write("rf-empty|empty_file.txt\n") + + argv = [ + "zip_main_maker.py", + "--template", + template_path, + "--output", + output_path, + "--substitution", + "%FOO%=bar", + "--hash_files_manifest", + manifest_path, + ] + + with mock.patch("sys.argv", argv): + zip_main_maker.main() + + # Calculate expected hash + h = hashlib.sha256() + line1 = f"rf-file|0|file1.txt|{file1_path}" + line2 = f"rf-file|0|file2.txt|{file2_path}" + line3 = f"rf-symlink|1|symlink.txt|{symlink_path}" + line4 = "rf-empty|empty_file.txt" + + # Sort lines like the program does + lines = sorted([line1, line2, line3, line4]) + for line in lines: + parts = line.split("|") + if len(parts) > 1: + _, rest = line.split("|", 1) + h.update(rest.encode("utf-8")) + else: + h.update(line.encode("utf-8")) + + type_ = parts[0] + if type_ == "rf-empty": + continue + if len(parts) >= 4: + is_symlink_str = parts[1] + path = parts[-1] + if not path: + continue + if is_symlink_str == "-1": + is_symlink = not os.path.exists(path) + else: + is_symlink = is_symlink_str == "1" + + if is_symlink: + h.update(os.readlink(path).encode("utf-8")) + else: + with open(path, "rb") as f: + h.update(f.read()) + + expected_hash = h.hexdigest() + + with open(output_path, "r", encoding="utf-8") as f: + content = f.read() + + self.assertEqual(content, f"hash={expected_hash}\nfoo=bar\n") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/tools/zipapp/zipper_test.py b/tests/tools/zipapp/zipper_test.py new file mode 100644 index 0000000000..ac70917c30 --- /dev/null +++ b/tests/tools/zipapp/zipper_test.py @@ -0,0 +1,344 @@ +import os +import pathlib +import shutil +import tempfile +import unittest +import zipfile + +from tools.private.zipapp import zipper + + +def symlink_target_path(p): + return p.replace("/", os.sep) + + +class ZipperTest(unittest.TestCase): + def setUp(self): + self.test_dir = pathlib.Path(tempfile.mkdtemp()) + self.manifest_path = self.test_dir / "manifest.txt" + self.output_zip = self.test_dir / "output.zip" + + def tearDown(self): + shutil.rmtree(self.test_dir) + + def _create_zip(self, **kwargs): + defaults = { + "manifest_path": self.manifest_path, + "output_zip": self.output_zip, + "compress_level": 0, + "workspace_name": "my_ws", + "legacy_external_runfiles": False, + "runfiles_dir": "runfiles", + # We need to generate paths for the platform we're running on. + "platform_pathsep": os.sep, + } + defaults.update(kwargs) + zipper.create_zip(**defaults) + + def assertZipFileContent( + self, zf, path, content=None, is_symlink=False, target=None + ): + info = zf.getinfo(path) + if is_symlink: + self.assertTrue( + self.is_symlink(info), + f"{path} should be a symlink but is not", + ) + self.assertEqual(zf.read(path).decode(), target) + else: + self.assertFalse( + self.is_symlink(info), + f"{path} should NOT be a symlink but is", + ) + self.assertEqual(zf.read(path).decode(), content) + + def test_create_zip_with_files_and_symlinks(self): + file1_path = self.test_dir / "file1.txt" + file1_path.write_text("content1") + + link_target_path = "target.txt" # Relative target + symlink_path = self.test_dir / "symlink_source" + symlink_path.symlink_to(link_target_path) + + manifest_content = [ + f"regular|0|file1.txt|{file1_path}", + f"rf-file|0|foo/bar.txt|{file1_path}", + f"rf-symlink|1|link1|{symlink_path}", # Should read target 'target.txt' + f"rf-root-symlink|0|root_file|{file1_path}", + "rf-empty|empty_file", + ] + self.manifest_path.write_text("\n".join(manifest_content)) + + self._create_zip() + + self.assertTrue(self.output_zip.exists()) + + with zipfile.ZipFile(self.output_zip, "r") as zf: + self.assertEqual( + set(zf.namelist()), + { + "file1.txt", + "runfiles/my_ws/foo/bar.txt", + "runfiles/my_ws/link1", + "runfiles/root_file", + "runfiles/my_ws/empty_file", + }, + ) + + self.assertZipFileContent(zf, "file1.txt", content="content1") + self.assertZipFileContent( + zf, "runfiles/my_ws/foo/bar.txt", content="content1" + ) + self.assertZipFileContent( + zf, "runfiles/my_ws/link1", is_symlink=True, target="target.txt" + ) + self.assertZipFileContent(zf, "runfiles/root_file", content="content1") + self.assertZipFileContent(zf, "runfiles/my_ws/empty_file", content="") + + def test_create_zip_with_direct_symlink(self): + # Test the 'symlink' manifest entry type + manifest_content = [ + "symlink|path/to/link|target/path", + ] + self.manifest_path.write_text("\n".join(manifest_content)) + + self._create_zip() + + with zipfile.ZipFile(self.output_zip, "r") as zf: + self.assertEqual(zf.namelist(), ["runfiles/path/to/link"]) + self.assertZipFileContent( + zf, + "runfiles/path/to/link", + is_symlink=True, + target=symlink_target_path("../../target/path"), + ) + + def test_pathsep_normalization(self): + # Test that pathsep="\\" normalizes paths + file1_path = self.test_dir / "file1.txt" + file1_path.write_text("content1") + + manifest_content = [ + f"regular|0|dir/file.txt|{file1_path}", + "symlink|link/path|target/path", + ] + self.manifest_path.write_text("\n".join(manifest_content)) + + # Use backslash as platform_pathsep + self._create_zip(platform_pathsep="\\") + + with zipfile.ZipFile(self.output_zip, "r") as zf: + # zipfile.namelist() always returns with forward slashes + # But the content of the symlink should be normalized if it was passed through path_norm + self.assertEqual( + set(zf.namelist()), + {"dir/file.txt", "runfiles/link/path"}, + ) + # The target of the symlink should have backslashes + self.assertZipFileContent( + zf, + "runfiles/link/path", + is_symlink=True, + target="..\\target\\path", + ) + + def test_symlink_precedence(self): + # Test that 'symlink' entries take precedence over others for the same path + file1_path = self.test_dir / "file1.txt" + file1_path.write_text("content1") + + manifest_content = [ + # Same zip path: runfiles/my_ws/path/to/file + f"rf-file|0|path/to/file|{file1_path}", + "symlink|my_ws/path/to/file|symlink/target", + ] + self.manifest_path.write_text("\n".join(manifest_content)) + + self._create_zip() + + with zipfile.ZipFile(self.output_zip, "r") as zf: + self.assertEqual(zf.namelist(), ["runfiles/my_ws/path/to/file"]) + # It should be the symlink, not the file + self.assertZipFileContent( + zf, + "runfiles/my_ws/path/to/file", + is_symlink=True, + target=symlink_target_path("../../../symlink/target"), + ) + + def test_timestamps_are_deterministic(self): + # Create a content file with a specific recent timestamp + file1_path = self.test_dir / "file1.txt" + file1_path.write_text("content1") + + # Set mtime to something recent (e.g. now) + os.utime(file1_path, None) + + manifest_content = [ + f"regular|0|file1.txt|{file1_path}", + ] + + self.manifest_path.write_text("\n".join(manifest_content)) + + self._create_zip() + + with zipfile.ZipFile(self.output_zip, "r") as zf: + info = zf.getinfo("file1.txt") + # DOS epoch is 1980-01-01 00:00:00 + expected_date_time = (1980, 1, 1, 0, 0, 0) + self.assertEqual(info.date_time, expected_date_time) + + def test_runfiles_mapping_with_cross_repo_paths(self): + # Create content file + file1_path = self.test_dir / "file1.txt" + file1_path.write_text("content1") + + manifest_content = [ + f"rf-file|0|../other_repo/foo.txt|{file1_path}", + "rf-empty|../other_repo/empty_file", + ] + + self.manifest_path.write_text("\n".join(manifest_content)) + + self._create_zip(workspace_name="my_ws") + + with zipfile.ZipFile(self.output_zip, "r") as zf: + self.assertEqual( + set(zf.namelist()), + { + "runfiles/other_repo/foo.txt", + "runfiles/other_repo/empty_file", + }, + ) + self.assertZipFileContent( + zf, "runfiles/other_repo/foo.txt", content="content1" + ) + self.assertZipFileContent(zf, "runfiles/other_repo/empty_file", content="") + + def test_runfiles_mapping_with_legacy_external_paths(self): + file1_path = self.test_dir / "file1.txt" + file1_path.write_text("content1") + + manifest_content = [ + f"rf-file|0|external/other_repo/foo.txt|{file1_path}", + "rf-empty|external/other_repo/empty_file", + ] + + self.manifest_path.write_text("\n".join(manifest_content)) + + self._create_zip(workspace_name="my_ws", legacy_external_runfiles=True) + + with zipfile.ZipFile(self.output_zip, "r") as zf: + self.assertEqual( + set(zf.namelist()), + { + "runfiles/other_repo/foo.txt", + "runfiles/other_repo/empty_file", + }, + ) + self.assertZipFileContent( + zf, "runfiles/other_repo/foo.txt", content="content1" + ) + self.assertZipFileContent(zf, "runfiles/other_repo/empty_file", content="") + + def test_output_deterministic(self): + # Create files + file1 = self.test_dir / "file1" + file1.write_text("1") + file2 = self.test_dir / "file2" + file2.write_text("2") + file3 = self.test_dir / "file3" + file3.write_text("3") + + # Manifest entries mixed up + # We want the final order to be: + # 1. a/regular (regular) + # 2. runfiles/a_root_link (rf-root-symlink) + # 3. runfiles/my_ws/b_rf_file (rf-file) + # 4. runfiles/my_ws/c_rf_link (rf-symlink) + # 5. runfiles/my_ws/d_rf_empty (rf-empty) + # 6. z/regular (regular) + + manifest_content = [ + f"regular|0|z/regular|{file1}", + f"rf-file|0|b_rf_file|{file2}", # -> runfiles/my_ws/b_rf_file + f"rf-root-symlink|0|a_root_link|{file3}", # -> runfiles/a_root_link + f"regular|0|a/regular|{file3}", + "rf-empty|d_rf_empty", # -> runfiles/my_ws/d_rf_empty + f"rf-symlink|0|c_rf_link|{file3}", # -> runfiles/my_ws/c_rf_link + ] + + self.manifest_path.write_text("\n".join(manifest_content)) + + self._create_zip(workspace_name="my_ws") + + with zipfile.ZipFile(self.output_zip, "r") as zf: + self.assertEqual( + zf.namelist(), + [ + "a/regular", + "runfiles/a_root_link", + "runfiles/my_ws/b_rf_file", + "runfiles/my_ws/c_rf_link", + "runfiles/my_ws/d_rf_empty", + "z/regular", + ], + ) + + def _extract_zip(self, zip_path, extract_dir): + # Manually extract to preserve symlinks + with zipfile.ZipFile(zip_path, "r") as zf: + for info in zf.infolist(): + extract_path = extract_dir / info.filename + extract_path.parent.mkdir(parents=True, exist_ok=True) + if self.is_symlink(info): + target = zf.read(info).decode() + # On Windows, relative symlinks must use backslashes to be readable + os.symlink(target, extract_path) + else: + with zf.open(info) as src, open(extract_path, "wb") as dst: + shutil.copyfileobj(src, dst) + + def test_symlink_extraction(self): + # Test that 'symlink' entries extract correctly as relative symlinks + # Create a file that the symlink will point to + target_file = self.test_dir / "target_file.txt" + target_file.write_text("target content") + + manifest_content = [ + f"rf-file|0|target/path|{target_file}", + "symlink|my_ws/path/to/link|my_ws/target/path", + f"rf-file|0|same_dir_target|{target_file}", + "symlink|my_ws/same_dir_link|my_ws/same_dir_target", + ] + self.manifest_path.write_text("\n".join(manifest_content)) + + self._create_zip(workspace_name="my_ws") + + extract_dir = self.test_dir / "extract" + extract_dir.mkdir() + + self._extract_zip(self.output_zip, extract_dir) + + link_path = extract_dir / "runfiles/my_ws/path/to/link" + self.assertTrue(link_path.is_symlink(), f"{link_path} should be a symlink") + self.assertEqual( + os.readlink(link_path), "../../target/path".replace("/", os.path.sep) + ) + self.assertEqual(link_path.read_text(), "target content") + + link2_path = extract_dir / "runfiles/my_ws/same_dir_link" + self.assertTrue(link2_path.is_symlink(), f"{link2_path} should be a symlink") + # Relative path from runfiles/my_ws/ to runfiles/my_ws/same_dir_target is just same_dir_target + self.assertEqual(os.readlink(link2_path), "same_dir_target") + self.assertEqual(link2_path.read_text(), "target content") + + def is_symlink(self, zip_info): + # Check upper 4 bits of external_attr for S_IFLNK + # S_IFLNK is 0o120000 = 0xA000 + attr = zip_info.external_attr >> 16 + return (attr & 0xF000) == 0xA000 + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/uv/lock/BUILD.bazel b/tests/uv/lock/BUILD.bazel index 6b6902da44..60d680bde8 100644 --- a/tests/uv/lock/BUILD.bazel +++ b/tests/uv/lock/BUILD.bazel @@ -1,5 +1,15 @@ load(":lock_tests.bzl", "lock_test_suite") +load(":uv_lock_to_requirements_tests.bzl", "uv_lock_to_requirements_test_suite") + +exports_files( + glob(["testdata/*"]), + visibility = ["//tests:__subpackages__"], +) lock_test_suite( name = "lock_tests", ) + +uv_lock_to_requirements_test_suite( + name = "uv_lock_to_requirements_tests", +) diff --git a/tests/uv/lock/lock_run_test.py b/tests/uv/lock/lock_run_test.py index ef57f23d31..6de5a96378 100644 --- a/tests/uv/lock/lock_run_test.py +++ b/tests/uv/lock/lock_run_test.py @@ -1,5 +1,5 @@ +import os import subprocess -import sys import tempfile import unittest from pathlib import Path @@ -10,15 +10,139 @@ 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}") + """Find file in runfiles, handling Windows .bat/.exe wrappers.""" + # On Windows, try executable extensions first to avoid matching symlink + # entries in the runfiles manifest that point to non-executable files + # (e.g. a Python source file instead of the .exe launcher). + exts = (".exe", ".bat", "") if os.name == "nt" else ("", ".exe", ".bat") + for ext in exts: + p = (Path("_main") / "tests" / "uv" / "lock" / (path + ext)).as_posix() + rpath = rfiles.Rlocation(p) + if rpath: + rp = Path(rpath) + if rp.exists(): + return rp - return Path(rpath) + # Fallback: look in runfiles directory directly (handles .bat wrappers on + # Windows where Rlocation may return a runfiles link that doesn't exist) + runfiles_dir = os.environ.get("RUNFILES_DIR") + if runfiles_dir: + exts = (".exe", ".bat", "") if os.name == "nt" else ("", ".bat", ".exe") + for ext in exts: + rp = Path(runfiles_dir, "_main", "tests", "uv", "lock", path + ext) + if rp.exists(): + return rp + + raise ValueError(f"Could not find file in runfiles: {path}") + + +def _run_binary(path: Path, **kwargs): + """Run a binary, handling Windows .bat files.""" + if os.name == "nt": + return subprocess.run( + ["cmd.exe", "/c", str(path)], + **kwargs, + ) + return subprocess.run(path, **kwargs) class LockTests(unittest.TestCase): + def _subprocess_env(self, workspace_dir: Path) -> dict[str, str]: + env = { + "BUILD_WORKSPACE_DIRECTORY": str(workspace_dir), + } + # Inherit specific env vars needed for finding runfiles on Windows + for key in ( + "PATH", + "RUNFILES_DIR", + "RUNFILES_MANIFEST_FILE", + "SYSTEMROOT", + "PATHEXT", + ): + if key in os.environ: + env[key] = os.environ[key] + return env + + def test_requirements_run_script_for_new_file(self): + """Verify the requirements_new_file.run script has expected args.""" + run_script_path = _relative_rpath("requirements_new_file.run") + content = run_script_path.read_text() + + if os.name == "nt": + self.assertIn("@echo off", content) + else: + self.assertIn("#!/usr/bin/env bash", content) + self.assertIn("BUILD_WORKSPACE_DIRECTORY", content) + self.assertIn("--no-progress", content) + self.assertIn("--quiet", content) + self.assertIn("does_not_exist.txt", content) + + def test_uv_lock_run_script(self): + """Verify the uv_lock_test.run script has expected args.""" + run_script_path = _relative_rpath("uv_lock_test.run") + content = run_script_path.read_text() + + if os.name == "nt": + self.assertIn("@echo off", content) + else: + self.assertIn("#!/usr/bin/env bash", content) + self.assertIn("--no-progress", content) + self.assertIn("--quiet", content) + + def test_run_script_has_no_output_file_arg(self): + """Verify the uv lock .run script does NOT have --output-file (uv lock doesn't use it).""" + run_script_path = _relative_rpath("uv_lock_test.run") + content = run_script_path.read_text() + + self.assertNotIn("--output-file", content) + + def test_debug_requirements_diff(self): + """Temporary test to print diff of generated vs expected requirements on Windows.""" + expected_path = _relative_rpath("testdata/requirements.txt") + try: + # Pass "requirements.out" directly. We need to handle the case where + # _relative_rpath might append extensions on Windows. + # Actually, _relative_rpath has: + # exts = (".exe", ".bat", "") if os.name == "nt" else ... + # So on Windows it will try .exe, .bat, then empty. + # If "requirements.out" exists, the empty extension will match it. + generated_path = _relative_rpath("requirements.out") + except ValueError as e: + print(f"Could not find requirements.out: {e}") + # Dump runfiles directory to help debug if needed + runfiles_dir = os.environ.get("RUNFILES_DIR") + if runfiles_dir: + print(f"RUNFILES_DIR: {runfiles_dir}") + for root, _, files in os.walk(runfiles_dir): + for f in files: + if "requirements" in f: + print(os.path.join(root, f)) + raise + + expected = expected_path.read_text() + generated = generated_path.read_text() + + if expected != generated: + import difflib + + diff = list( + difflib.unified_diff( + expected.splitlines(keepends=True), + generated.splitlines(keepends=True), + fromfile="expected (testdata/requirements.txt)", + tofile="generated (requirements.out)", + ) + ) + print("\n=== DIFF START ===") + print("".join(diff)) + print("=== DIFF END ===\n") + + # Also print representation to see line endings + print(f"Expected line endings: {repr(expected[:100])}") + print(f"Generated line endings: {repr(generated[:100])}") + + self.assertEqual(expected, generated, "Files differ! See diff above.") + def test_requirements_updating_for_the_first_time(self): # Given copier_path = _relative_rpath("requirements_new_file.update") @@ -31,19 +155,18 @@ def test_requirements_updating_for_the_first_time(self): self.assertFalse( want_path.exists(), "The path should not exist after the test" ) - output = subprocess.run( + output = _run_binary( copier_path, capture_output=True, - env={ - "BUILD_WORKSPACE_DIRECTORY": f"{workspace_dir}", - }, + env=self._subprocess_env(workspace_dir), ) # Then self.assertEqual(0, output.returncode, output.stderr) + stdout = output.stdout.decode("utf-8").replace("\\", "/") self.assertIn( "cp /tests/uv/lock/requirements_new_file", - output.stdout.decode("utf-8"), + stdout, ) self.assertTrue(want_path.exists(), "The path should exist after the test") self.assertNotEqual(want_path.read_text(), "") @@ -51,8 +174,6 @@ def test_requirements_updating_for_the_first_time(self): 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: @@ -66,25 +187,22 @@ def test_requirements_updating(self): / "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( + output = _run_binary( copier_path, capture_output=True, - env={ - "BUILD_WORKSPACE_DIRECTORY": f"{workspace_dir}", - }, + env=self._subprocess_env(workspace_dir), ) # Then self.assertEqual(0, output.returncode) + stdout = output.stdout.decode("utf-8").replace("\\", "/") self.assertIn( "cp /tests/uv/lock/requirements", - output.stdout.decode("utf-8"), + stdout, ) - self.assertEqual(want_path.read_text(), want_text) + self.assertTrue(want_path.exists(), "The path should exist after the test") + self.assertNotEqual(want_path.read_text(), "") def test_requirements_run_on_the_first_time(self): # Given @@ -99,14 +217,12 @@ def test_requirements_run_on_the_first_time(self): want_path.parent.mkdir(parents=True) self.assertFalse( - want_path.exists(), "The path should not exist after the test" + want_path.exists(), "The path should not exist before the test" ) - output = subprocess.run( + output = _run_binary( copier_path, capture_output=True, - env={ - "BUILD_WORKSPACE_DIRECTORY": f"{workspace_dir}", - }, + env=self._subprocess_env(workspace_dir), ) # Then @@ -114,16 +230,33 @@ def test_requirements_run_on_the_first_time(self): 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"), - ) + # NOTE: stdout is typically empty because uv runs with --quiet --no-progress + + def test_requirements_run_script_has_expected_args(self): + """Verify the .run script template has expected args embedded.""" + run_script_path = _relative_rpath("requirements.run") + content = run_script_path.read_text() + + if os.name == "nt": + self.assertIn("@echo off", content) + self.assertIn("%*", content) + else: + self.assertIn("#!/usr/bin/env bash", content) + self.assertIn('"$@"', content) + self.assertIn("BUILD_WORKSPACE_DIRECTORY", content) + self.assertIn("--custom-compile-command", content) + self.assertIn("--generate-hashes", content) + self.assertIn("--no-strip-extras", content) + self.assertIn("--no-python-downloads", content) + self.assertIn("--no-cache", content) + self.assertIn("--no-progress", content) + self.assertIn("--quiet", content) + self.assertIn("--output-file", content) + self.assertIn("requirements.txt", content) 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: @@ -136,18 +269,12 @@ def test_requirements_run(self): / "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( + output = _run_binary( copier_path, capture_output=True, - env={ - "BUILD_WORKSPACE_DIRECTORY": f"{workspace_dir}", - }, + env=self._subprocess_env(workspace_dir), ) # Then @@ -155,10 +282,7 @@ def test_requirements_run(self): 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"), - ) + # NOTE: stdout is typically empty because uv runs with --quiet --no-progress if __name__ == "__main__": diff --git a/tests/uv/lock/lock_tests.bzl b/tests/uv/lock/lock_tests.bzl index 1eb5b1d903..e3e035717f 100644 --- a/tests/uv/lock/lock_tests.bzl +++ b/tests/uv/lock/lock_tests.bzl @@ -14,6 +14,7 @@ "" +load("@bazel_skylib//rules:diff_test.bzl", "diff_test") load("@bazel_skylib//rules:native_binary.bzl", "native_test") load("//python/uv:lock.bzl", "lock") load("//tests/support:py_reconfig.bzl", "py_reconfig_test") @@ -65,6 +66,8 @@ def lock_test_suite(name): "requirements.update", "requirements.run", "testdata/requirements.txt", + "uv_lock_test.run", + ":requirements", ], main = "lock_run_test.py", tags = [ @@ -77,19 +80,26 @@ def lock_test_suite(name): # `--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( + # Document and check that the action output matches the in-source file. + diff_test( name = "requirements_test", - src = ":requirements.update", + timeout = "short", + file1 = ":requirements", + file2 = "testdata/requirements.txt", + ) + + lock( + name = "uv_lock_test", + srcs = ["testdata/pyproject.toml"], + out = "testdata/uv_lock_expected.lock", + tags = ["no-remote-exec"], + ) + + native_test( + name = "uv_lock_test_check", + src = ":uv_lock_test.update", target_compatible_with = select({ "@platforms//os:windows": ["@platforms//:incompatible"], "//conditions:default": [], @@ -100,6 +110,8 @@ def lock_test_suite(name): name = name, tests = [ ":requirements_test", + "//tests/uv/lock/pyproject_toml:requirements_test", ":requirements_run_tests", + ":uv_lock_test_check", ], ) diff --git a/tests/uv/lock/pyproject_toml/BUILD.bazel b/tests/uv/lock/pyproject_toml/BUILD.bazel new file mode 100644 index 0000000000..d73d206293 --- /dev/null +++ b/tests/uv/lock/pyproject_toml/BUILD.bazel @@ -0,0 +1,31 @@ +load("@bazel_skylib//rules:diff_test.bzl", "diff_test") +load("//python/uv:lock.bzl", "lock") + +# This test verifies that the `lock` rule automatically passes `--project` to `uv pip compile` based +# on the package directory, so that `[tool.uv]` settings from a `pyproject.toml` in the same +# directory are applied. It will exclude a particular dependency from the lock-file, so it will be +# easy to see if we have any issues. +lock( + name = "requirements", + srcs = ["pyproject.toml"], + out = "requirements.txt", + build_constraints = [ + "//tests/uv/lock:testdata/build_constraints.txt", + "//tests/uv/lock:testdata/build_constraints2.txt", + ], + constraints = [ + "//tests/uv/lock:testdata/constraints.txt", + "//tests/uv/lock:testdata/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"], +) + +diff_test( + name = "requirements_test", + timeout = "short", + file1 = ":requirements", + file2 = "requirements.txt", + visibility = ["//tests/uv/lock:__pkg__"], +) diff --git a/tests/uv/lock/pyproject_toml/pyproject.toml b/tests/uv/lock/pyproject_toml/pyproject.toml new file mode 100644 index 0000000000..06b96309da --- /dev/null +++ b/tests/uv/lock/pyproject_toml/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "test" +version = "0.0.0" +dependencies = ["requests"] + +[tool.uv] +no-build-isolation = true +exclude-dependencies = ["charset-normalizer"] diff --git a/tests/uv/lock/pyproject_toml/requirements.txt b/tests/uv/lock/pyproject_toml/requirements.txt new file mode 100644 index 0000000000..2f4330e1ae --- /dev/null +++ b/tests/uv/lock/pyproject_toml/requirements.txt @@ -0,0 +1,26 @@ +# This file was autogenerated by uv via the following command: +# bazel run //tests/uv/lock/pyproject_toml:requirements.update +certifi==2025.1.31 \ + --hash=sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651 \ + --hash=sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe + # via + # -c tests/uv/lock/testdata/constraints.txt + # requests +idna==3.10 \ + --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ + --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 + # via + # -c tests/uv/lock/testdata/constraints.txt + # requests +requests==2.32.3 \ + --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ + --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 + # via + # -c tests/uv/lock/testdata/constraints.txt + # test (tests/uv/lock/pyproject_toml/pyproject.toml) +urllib3==2.3.0 \ + --hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \ + --hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d + # via + # -c tests/uv/lock/testdata/constraints.txt + # requests diff --git a/tests/uv/lock/testdata/constraints.txt b/tests/uv/lock/testdata/constraints.txt index 18ade2c5b9..1d4f29173d 100644 --- a/tests/uv/lock/testdata/constraints.txt +++ b/tests/uv/lock/testdata/constraints.txt @@ -1 +1,5 @@ charset-normalizer==3.4.0 +certifi==2025.1.31 +requests==2.32.3 +idna==3.10 +urllib3==2.3.0 diff --git a/tests/uv/lock/testdata/pyproject.toml b/tests/uv/lock/testdata/pyproject.toml new file mode 100644 index 0000000000..d72efcf7c0 --- /dev/null +++ b/tests/uv/lock/testdata/pyproject.toml @@ -0,0 +1,5 @@ +[project] +name = "test-project" +version = "0.0.1" +dependencies = ["requests"] +requires-python = ">=3.9" diff --git a/tests/uv/lock/testdata/requirements.txt b/tests/uv/lock/testdata/requirements.txt index d02844636d..8b718fb145 100644 --- a/tests/uv/lock/testdata/requirements.txt +++ b/tests/uv/lock/testdata/requirements.txt @@ -3,7 +3,9 @@ certifi==2025.1.31 \ --hash=sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651 \ --hash=sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe - # via requests + # via + # -c tests/uv/lock/testdata/constraints.txt + # requests charset-normalizer==3.4.0 \ --hash=sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621 \ --hash=sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6 \ @@ -117,12 +119,18 @@ charset-normalizer==3.4.0 \ idna==3.10 \ --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 - # via requests + # via + # -c tests/uv/lock/testdata/constraints.txt + # requests requests==2.32.3 \ --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 - # via -r tests/uv/lock/testdata/requirements.in + # via + # -c tests/uv/lock/testdata/constraints.txt + # -r tests/uv/lock/testdata/requirements.in urllib3==2.3.0 \ --hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \ --hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d - # via requests + # via + # -c tests/uv/lock/testdata/constraints.txt + # requests diff --git a/tests/uv/lock/testdata/uv_lock_expected.lock b/tests/uv/lock/testdata/uv_lock_expected.lock new file mode 100644 index 0000000000..fc027674ef --- /dev/null +++ b/tests/uv/lock/testdata/uv_lock_expected.lock @@ -0,0 +1,218 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version < '3.10'", +] + +[[package]] +name = "certifi" +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, + { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, + { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, + { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, + { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, + { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, + { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, + { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, + { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, + { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, + { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/ef725f8eb19b5a261b30f78efa9252ef9d017985cb499102f6f49834cd12/charset_normalizer-3.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217", size = 299121, upload-time = "2026-04-02T09:28:14.372Z" }, + { url = "https://files.pythonhosted.org/packages/a3/22/2f12878fbc680fbbb52386cd39a379801f62eaca74fc8b323381325f0f04/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5", size = 200612, upload-time = "2026-04-02T09:28:16.162Z" }, + { url = "https://files.pythonhosted.org/packages/bc/b6/10c84e789126ca97d4a7228863a30481e786980a8b8cfcbf4f30658ca63c/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9", size = 221041, upload-time = "2026-04-02T09:28:17.554Z" }, + { url = "https://files.pythonhosted.org/packages/21/7b/c414866a138400b2e81973d006da7f694cfeaf895ef07d2cba9a8743841a/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a", size = 216323, upload-time = "2026-04-02T09:28:18.863Z" }, + { url = "https://files.pythonhosted.org/packages/2e/92/bdcf94997e06b223d826df3abed45a5ad6e17f609b7df9d25cd23b5bde30/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc", size = 208419, upload-time = "2026-04-02T09:28:20.332Z" }, + { url = "https://files.pythonhosted.org/packages/1a/64/3f9142293c88b1b10e199649ed1330f070c2a68e305335a5819fa7f25fa7/charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00", size = 195016, upload-time = "2026-04-02T09:28:21.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d1/d8a6b7dd5c5636b76ce0d080bc57d8e56c7bbd6bc2ac941529a35e41d84a/charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776", size = 206115, upload-time = "2026-04-02T09:28:23.259Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8c/60ebe912379627d023eb96995b40bc50308729f210f43d66109ca0a7bbd2/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319", size = 204022, upload-time = "2026-04-02T09:28:24.779Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2a/41816ceda78a551cbfdfbeab6f3891152b0e3f758ce6580c2c18c829f774/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24", size = 195914, upload-time = "2026-04-02T09:28:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9b/7c7f4b7f11525fcbdfba752455314ac60646bae91cdd671d531c1f7a97c6/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42", size = 222159, upload-time = "2026-04-02T09:28:27.504Z" }, + { url = "https://files.pythonhosted.org/packages/9f/57/301682e7469bdbfa2ce219a804f0668b2266ab8520570d85d3b3ef483ea3/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4", size = 206154, upload-time = "2026-04-02T09:28:28.848Z" }, + { url = "https://files.pythonhosted.org/packages/20/ec/90339ff5cdc598b265748c1f231c7d7fbd9123a92cee10f757e0b1448de4/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67", size = 217423, upload-time = "2026-04-02T09:28:30.248Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e7/a7a6147f8e3375676309cf584b25c72a3bab784ea4085b0011fa07b23aeb/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274", size = 210604, upload-time = "2026-04-02T09:28:31.736Z" }, + { url = "https://files.pythonhosted.org/packages/1a/62/d9340c7a79c393e57807d7fb6c57e82060687891f81b74d3201958b919c1/charset_normalizer-3.4.7-cp39-cp39-win32.whl", hash = "sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366", size = 144631, upload-time = "2026-04-02T09:28:33.158Z" }, + { url = "https://files.pythonhosted.org/packages/21/e7/92901117e2ddc8facfe8235a3ecd4eb482185b2ad5d5b6606b37c1afea06/charset_normalizer-3.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444", size = 154710, upload-time = "2026-04-02T09:28:34.557Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4f/e1fb138201ad9a32499dd9a98aa4a5a5441fbf7f56b52b619a54b7ee8777/charset_normalizer-3.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c", size = 143716, upload-time = "2026-04-02T09:28:35.908Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "idna" +version = "3.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version < '3.10'" }, + { name = "charset-normalizer", marker = "python_full_version < '3.10'" }, + { name = "idna", marker = "python_full_version < '3.10'" }, + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version >= '3.10'" }, + { name = "charset-normalizer", marker = "python_full_version >= '3.10'" }, + { name = "idna", marker = "python_full_version >= '3.10'" }, + { name = "urllib3", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "test-project" +version = "0.0.1" +source = { virtual = "." } +dependencies = [ + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "requests", version = "2.34.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] + +[package.metadata] +requires-dist = [{ name = "requests" }] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] diff --git a/tests/uv/lock/uv_lock_to_requirements_tests.bzl b/tests/uv/lock/uv_lock_to_requirements_tests.bzl new file mode 100644 index 0000000000..1b947877c9 --- /dev/null +++ b/tests/uv/lock/uv_lock_to_requirements_tests.bzl @@ -0,0 +1,289 @@ +"""Tests for uv_lock_to_requirements.""" + +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("//python/uv/private:uv_lock_to_requirements.bzl", "uv_lock_to_requirements") # buildifier: disable=bzl-visibility + +_tests = [] + +def _test_empty(env): + got = uv_lock_to_requirements(json.decode("""{"package":[]}""")) + env.expect.that_str(got).equals("") + +_tests.append(_test_empty) + +def _test_simple_package(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"wheels":[{"hash":"sha256:deadbeef","url":"https://example.org/foo.whl"}]} + ]}""")) + env.expect.that_str(got).equals("""\ +foo==0.0.1 \\ + --hash=sha256:deadbeef +""") + +_tests.append(_test_simple_package) + +def _test_package_with_markers(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"resolution-markers":["python_full_version < '3.10'"],"wheels":[{"hash":"sha256:deadbeef","url":"https://example.org/foo.whl"}]} + ]}""")) + env.expect.that_str(got).equals("""\ +foo==0.0.1 ; python_full_version < '3.10' \\ + --hash=sha256:deadbeef +""") + +_tests.append(_test_package_with_markers) + +def _test_package_with_multiple_markers(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"resolution-markers":["python_full_version == '3.10.*'","python_full_version >= '3.14'"],"wheels":[{"hash":"sha256:deadbeef","url":"https://example.org/foo.whl"}]} + ]}""")) + env.expect.that_str(got).equals("""\ +foo==0.0.1 ; python_full_version == '3.10.*' or python_full_version >= '3.14' \\ + --hash=sha256:deadbeef +""") + +_tests.append(_test_package_with_multiple_markers) + +def _test_package_with_deps(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"bar","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"wheels":[{"hash":"sha256:baadbeef","url":"https://example.org/bar.whl"}]}, + {"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"dependencies":[{"name":"bar"}],"wheels":[{"hash":"sha256:deadbeef","url":"https://example.org/foo.whl"}]} + ]}""")) + env.expect.that_str(got).equals("""\ +bar==0.0.1 \\ + --hash=sha256:baadbeef + # via foo + +foo==0.0.1 \\ + --hash=sha256:deadbeef +""") + +_tests.append(_test_package_with_deps) + +def _test_package_with_optional_deps(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"bar","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"wheels":[{"hash":"sha256:baadbeef","url":"https://example.org/bar.whl"}]}, + {"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"optional-dependencies":{"extra1":[{"name":"bar"}]},"wheels":[{"hash":"sha256:deadbeef","url":"https://example.org/foo.whl"}]} + ]}""")) + env.expect.that_str(got).equals("""\ +bar==0.0.1 \\ + --hash=sha256:baadbeef + # via foo + +foo[extra1]==0.0.1 \\ + --hash=sha256:deadbeef +""") + +_tests.append(_test_package_with_optional_deps) + +def _test_self_edge_excluded(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"pydantic","version":"2.0.0","source":{"registry":"https://pypi.org/simple"},"dependencies":[{"name":"pydantic","extra":["email"]}],"wheels":[{"hash":"sha256:deadbeef","url":"https://example.org/pydantic.whl"}]} + ]}""")) + env.expect.that_str(got).equals("""\ +pydantic==2.0.0 \\ + --hash=sha256:deadbeef +""") + +_tests.append(_test_self_edge_excluded) + +def _test_multiple_dependents(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"common","version":"1.0.0","source":{"registry":"https://pypi.org/simple"},"wheels":[{"hash":"sha256:aaaa","url":"https://example.org/common.whl"}]}, + {"name":"pkg_a","version":"0.1.0","source":{"registry":"https://pypi.org/simple"},"dependencies":[{"name":"common"}],"wheels":[{"hash":"sha256:bbbb","url":"https://example.org/a.whl"}]}, + {"name":"pkg_b","version":"0.2.0","source":{"registry":"https://pypi.org/simple"},"dependencies":[{"name":"common"}],"wheels":[{"hash":"sha256:cccc","url":"https://example.org/b.whl"}]} + ]}""")) + env.expect.that_str(got).equals("""\ +common==1.0.0 \\ + --hash=sha256:aaaa + # via + # pkg_a + # pkg_b + +pkg_a==0.1.0 \\ + --hash=sha256:bbbb + +pkg_b==0.2.0 \\ + --hash=sha256:cccc +""") + +_tests.append(_test_multiple_dependents) + +def _test_git_source_skipped(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"foo","version":"0.1.0","source":{"git":"https://github.com/org/foo.git"}}, + {"name":"bar","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"wheels":[{"hash":"sha256:deadbeef","url":"https://example.org/bar.whl"}]} + ]}""")) + env.expect.that_str(got).equals("""\ +bar==0.0.1 \\ + --hash=sha256:deadbeef +""") + +_tests.append(_test_git_source_skipped) + +def _test_virtual_source_skipped(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"virtual-pkg","version":"0.0.0","source":{"virtual":true}}, + {"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"wheels":[{"hash":"sha256:deadbeef","url":"https://example.org/foo.whl"}]} + ]}""")) + env.expect.that_str(got).equals("""\ +foo==0.0.1 \\ + --hash=sha256:deadbeef +""") + +_tests.append(_test_virtual_source_skipped) + +def _test_sdist_hash(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"bar","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"sdist":{"hash":"sha256:deadb00f","url":"https://example.org/bar.tar.gz"}} + ]}""")) + env.expect.that_str(got).equals("""\ +bar==0.0.1 \\ + --hash=sha256:deadb00f +""") + +_tests.append(_test_sdist_hash) + +def _test_wheel_and_sdist_hashes(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"sdist":{"hash":"sha256:feedcafe","url":"https://example.org/foo.tar.gz"},"wheels":[{"hash":"sha256:deadbeef","url":"https://example.org/foo.whl"}]} + ]}""")) + env.expect.that_str(got).equals("""\ +foo==0.0.1 \\ + --hash=sha256:deadbeef \\ + --hash=sha256:feedcafe +""") + +_tests.append(_test_wheel_and_sdist_hashes) + +def _test_multiple_versions_same_package(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"resolution-markers":["python_full_version < '3.10'"],"wheels":[{"hash":"sha256:deadbeef","url":"https://example.org/foo-0.0.1.whl"}]}, + {"name":"foo","version":"0.0.2","source":{"registry":"https://pypi.org/simple"},"resolution-markers":["python_full_version >= '3.10'"],"wheels":[{"hash":"sha256:deadb11f","url":"https://example.org/foo-0.0.2.whl"}]} + ]}""")) + env.expect.that_str(got).equals("""\ +foo==0.0.1 ; python_full_version < '3.10' \\ + --hash=sha256:deadbeef + +foo==0.0.2 ; python_full_version >= '3.10' \\ + --hash=sha256:deadb11f +""") + +_tests.append(_test_multiple_versions_same_package) + +def _test_dep_with_extras(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"wheels":[{"hash":"sha256:deadbeef","url":"https://example.org/foo.whl"}]}, + {"name":"bar","version":"0.0.2","source":{"registry":"https://pypi.org/simple"},"dependencies":[{"name":"foo","extra":["extra1"]}],"wheels":[{"hash":"sha256:baadbeef","url":"https://example.org/bar.whl"}]} + ]}""")) + env.expect.that_str(got).equals("""\ +foo[extra1]==0.0.1 \\ + --hash=sha256:deadbeef + # via bar + +bar==0.0.2 \\ + --hash=sha256:baadbeef +""") + +_tests.append(_test_dep_with_extras) + +def _test_multiple_hashes_from_wheels(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"wheels":[ + {"hash":"sha256:aaaa","url":"https://example.org/foo-0.0.1-cp39.whl"}, + {"hash":"sha256:bbbb","url":"https://example.org/foo-0.0.1-py3-none-any.whl"} + ]} + ]}""")) + env.expect.that_str(got).equals("""\ +foo==0.0.1 \\ + --hash=sha256:aaaa \\ + --hash=sha256:bbbb +""") + +_tests.append(_test_multiple_hashes_from_wheels) + +def _test_package_no_hashes_no_deps(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"}} + ]}""")) + env.expect.that_str(got).equals("""\ +foo==0.0.1 +""") + +_tests.append(_test_package_no_hashes_no_deps) + +def _test_package_with_multiple_optional_deps(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"bar","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"wheels":[{"hash":"sha256:baadbeef","url":"https://example.org/bar.whl"}]}, + {"name":"baz","version":"0.0.2","source":{"registry":"https://pypi.org/simple"},"wheels":[{"hash":"sha256:deadc0de","url":"https://example.org/baz.whl"}]}, + {"name":"foo","version":"0.0.3","source":{"registry":"https://pypi.org/simple"},"optional-dependencies":{"extra1":[{"name":"bar"}],"extra2":[{"name":"baz"}]},"wheels":[{"hash":"sha256:feedcafe","url":"https://example.org/foo.whl"}]} + ]}""")) + env.expect.that_str(got).equals("""\ +bar==0.0.1 \\ + --hash=sha256:baadbeef + # via foo + +baz==0.0.2 \\ + --hash=sha256:deadc0de + # via foo + +foo[extra1,extra2]==0.0.3 \\ + --hash=sha256:feedcafe +""") + +_tests.append(_test_package_with_multiple_optional_deps) + +def _test_dep_with_multiple_extras(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"wheels":[{"hash":"sha256:deadbeef","url":"https://example.org/foo.whl"}]}, + {"name":"bar","version":"0.0.2","source":{"registry":"https://pypi.org/simple"},"dependencies":[{"name":"foo","extra":["extra1","extra2"]}],"wheels":[{"hash":"sha256:baadbeef","url":"https://example.org/bar.whl"}]} + ]}""")) + env.expect.that_str(got).equals("""\ +foo[extra1,extra2]==0.0.1 \\ + --hash=sha256:deadbeef + # via bar + +bar==0.0.2 \\ + --hash=sha256:baadbeef +""") + +_tests.append(_test_dep_with_multiple_extras) + +def _test_extras_from_multiple_dependents(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"common","version":"1.0.0","source":{"registry":"https://pypi.org/simple"},"wheels":[{"hash":"sha256:aaaa","url":"https://example.org/common.whl"}]}, + {"name":"pkg_a","version":"0.1.0","source":{"registry":"https://pypi.org/simple"},"dependencies":[{"name":"common","extra":["extra1"]}],"wheels":[{"hash":"sha256:bbbb","url":"https://example.org/a.whl"}]}, + {"name":"pkg_b","version":"0.2.0","source":{"registry":"https://pypi.org/simple"},"dependencies":[{"name":"common","extra":["extra2"]}],"wheels":[{"hash":"sha256:cccc","url":"https://example.org/b.whl"}]} + ]}""")) + env.expect.that_str(got).equals("""\ +common[extra1,extra2]==1.0.0 \\ + --hash=sha256:aaaa + # via + # pkg_a + # pkg_b + +pkg_a==0.1.0 \\ + --hash=sha256:bbbb + +pkg_b==0.2.0 \\ + --hash=sha256:cccc +""") + +_tests.append(_test_extras_from_multiple_dependents) + +def _test_requires_dist_extras(env): + """Test that extras from metadata.requires-dist are included.""" + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"pytest-bazel","version":"0.1.6","source":{"registry":"https://pypi.org/simple"},"wheels":[{"hash":"sha256:a29e80e1d67c3db801bdd4d0b6b742f2bfb48cd6841caa33401458e5c4e29c21","url":"https://example.org/pytest_bazel-0.1.6.whl"}]}, + {"name":"root-pkg","version":"0.0.0","source":{"virtual":"."},"dependencies":[{"name":"pytest-bazel"}],"metadata":{"requires-dist":[{"name":"pytest-bazel","extras":["all"]}]}} + ]}""")) + env.expect.that_str(got).equals("""\ +pytest-bazel[all]==0.1.6 \\ + --hash=sha256:a29e80e1d67c3db801bdd4d0b6b742f2bfb48cd6841caa33401458e5c4e29c21 + # via root-pkg +""") + +_tests.append(_test_requires_dist_extras) + +def uv_lock_to_requirements_test_suite(name): + test_suite(name = name, basic_tests = _tests) diff --git a/tests/uv/lock/workspaces/BUILD.bazel b/tests/uv/lock/workspaces/BUILD.bazel new file mode 100644 index 0000000000..aa1fabe2e3 --- /dev/null +++ b/tests/uv/lock/workspaces/BUILD.bazel @@ -0,0 +1,27 @@ +load("@bazel_skylib//rules:diff_test.bzl", "diff_test") +load("//python/uv:lock.bzl", "lock") +load("//tests/support:support.bzl", "NOT_WINDOWS") + +# This test verifies that the `lock` rule automatically passes `--project` to `uv pip compile` based +# on the package directory, so that `[tool.uv]` settings from a `pyproject.toml` in the same +# directory are applied. It will exclude a particular dependency from the lock-file, so it will be +# easy to see if we have any issues. +lock( + name = "requirements", + srcs = [ + "pyproject.toml", + "//tests/uv/lock/workspaces/packages", + ], + out = "requirements.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"], +) + +diff_test( + name = "requirements_test", + timeout = "short", + file1 = ":requirements", + file2 = "requirements.txt", + target_compatible_with = NOT_WINDOWS, +) diff --git a/tests/uv/lock/workspaces/packages/BUILD.bazel b/tests/uv/lock/workspaces/packages/BUILD.bazel new file mode 100644 index 0000000000..5461287d9f --- /dev/null +++ b/tests/uv/lock/workspaces/packages/BUILD.bazel @@ -0,0 +1,5 @@ +filegroup( + name = "packages", + srcs = ["//tests/uv/lock/workspaces/packages/foo:pyproject.toml"], + visibility = ["//tests/uv/lock/workspaces:__pkg__"], +) diff --git a/tests/uv/lock/workspaces/packages/foo/BUILD.bazel b/tests/uv/lock/workspaces/packages/foo/BUILD.bazel new file mode 100644 index 0000000000..d89a7b784e --- /dev/null +++ b/tests/uv/lock/workspaces/packages/foo/BUILD.bazel @@ -0,0 +1 @@ +exports_files(["pyproject.toml"]) diff --git a/tests/uv/lock/workspaces/packages/foo/pyproject.toml b/tests/uv/lock/workspaces/packages/foo/pyproject.toml new file mode 100644 index 0000000000..f26ea85b51 --- /dev/null +++ b/tests/uv/lock/workspaces/packages/foo/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "foo" +version = "0.0.0" +dependencies = ["black"] + +[tool.uv] +no-build-isolation = true diff --git a/tests/uv/lock/workspaces/pyproject.toml b/tests/uv/lock/workspaces/pyproject.toml new file mode 100644 index 0000000000..01e1e2e076 --- /dev/null +++ b/tests/uv/lock/workspaces/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "test" +version = "0.0.0" +dependencies = ["requests"] + +[tool.uv] +no-build-isolation = true + +[tool.uv.workspace] +members = ["packages/*"] diff --git a/tests/uv/lock/workspaces/requirements.txt b/tests/uv/lock/workspaces/requirements.txt new file mode 100644 index 0000000000..5ce35d7571 --- /dev/null +++ b/tests/uv/lock/workspaces/requirements.txt @@ -0,0 +1,242 @@ +# This file was autogenerated by uv via the following command: +# bazel run //tests/uv/lock/workspaces:requirements.update +black==26.5.1 \ + --hash=sha256:0e48b87e03bf109288e55cfceadcfa15ff5470aca2851a851950ed2926f450d7 \ + --hash=sha256:1037d5ac7b7b310b2632ad867ec8d0e4c4819dcdb0b820f63135da746a24e418 \ + --hash=sha256:1ef92b76f7733f282fd096ea406200b5a286c42947412b0eaff3a74e3616cefe \ + --hash=sha256:1f7ea64ebfa01b50f693508fc39f875e264446d3b097088f84f203b9d09618a0 \ + --hash=sha256:22f2cd76d069cc54c71f10360744ba8983fbb616903b4304a85b734915c8e1b4 \ + --hash=sha256:2b36cf2ddf5566e205f6535f782a62194a184d33e175b64ae8c40b1737522be3 \ + --hash=sha256:30d3c14661f2792e9142cce3eeeb1cbc175b3eb5f733be0c8eeb99651e52b0c3 \ + --hash=sha256:32d5ea7f6c8bdfa6e648326ebca1f02b0764e2a029edc6f8dce2627e19d468c3 \ + --hash=sha256:3915f256e75a2d7cf88d8953d37f780455dc586cc72dee059c528fe77f581217 \ + --hash=sha256:4ad6fa01f941920f54f2bbb35f3df7673428a0ef98a0b0840c2eaef3b110efa8 \ + --hash=sha256:4ed7f7da04046d2e488437170797d3b4a4ad83906683bcb7dfc68b673bbce5e2 \ + --hash=sha256:5119fa92ae61f786e8c3662fd60aece1d0a2dd5cca5d0c79417a95e7a4272a59 \ + --hash=sha256:577f21094ea469ef92ec1adaf2c9441a226d2144d01a5be2fa823cecf6543e50 \ + --hash=sha256:58b4bd92cf88aacf83d88479c8f9caee044b1ec55f2451a337354a7ea2590a22 \ + --hash=sha256:5c34b25da232ead53a6f335b76dbea124f4d152ad568b9080d6f944bc2b34b52 \ + --hash=sha256:87ed5c6f450580a2f6790bc7cbfb016dfc73bc750249762268a3695361315eef \ + --hash=sha256:89c93167a74d3a75dfaa38a5c7cca015537d5820dd7f17d63267d674a61cae90 \ + --hash=sha256:96ae2c733b2aabdd9986e2c5df628ff3473676cd1c5faded1ff496cf6d74083c \ + --hash=sha256:9942db8888e06943c5dde66ca0037dcff82a2a4ec1ad0ada9e0d2ee9d9823893 \ + --hash=sha256:9d98d4137277c75dfb898ec8d846c4fd68ba1e9cf77f95e2865c203dc18f4c3d \ + --hash=sha256:a1dca32d9f1784af512a13410ec204c6f7f0aa9797a111c42e1c03449821c264 \ + --hash=sha256:dd321f668053961824bcc1be1cc1df748b2d7e4fa28086b08331e577b0100a73 \ + --hash=sha256:e1a26503279b6b310669fb0b219c39e4820b77e8189fe80f522bb511f247db0a \ + --hash=sha256:e88976690a64b0af98312ca958415849cb42423423c5f2ee74af4b49a97a2168 \ + --hash=sha256:ea8d16dc41655aa113cd64665e7219446cd7e4ff2248d7178eaa905190c86b18 \ + --hash=sha256:ecb3e624844c798144e9bd986954e0adc81d8911a1f30f375e1252fe26e8c294 \ + --hash=sha256:ed1a20af114c301a0269bf01163d51dbef72737fd65f850001e7cbe7f3c7abae + # via foo (tests/uv/lock/workspaces/packages/foo/pyproject.toml) +certifi==2025.1.31 \ + --hash=sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651 \ + --hash=sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe + # via requests +charset-normalizer==3.4.7 \ + --hash=sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc \ + --hash=sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c \ + --hash=sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67 \ + --hash=sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4 \ + --hash=sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0 \ + --hash=sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c \ + --hash=sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5 \ + --hash=sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444 \ + --hash=sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153 \ + --hash=sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9 \ + --hash=sha256:16d971e29578a5e97d7117866d15889a4a07befe0e87e703ed63cd90cb348c01 \ + --hash=sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217 \ + --hash=sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b \ + --hash=sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c \ + --hash=sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a \ + --hash=sha256:1dc8b0ea451d6e69735094606991f32867807881400f808a106ee1d963c46a83 \ + --hash=sha256:1efde3cae86c8c273f1eb3b287be7d8499420cf2fe7585c41d370d3e790054a5 \ + --hash=sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7 \ + --hash=sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb \ + --hash=sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c \ + --hash=sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1 \ + --hash=sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42 \ + --hash=sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab \ + --hash=sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df \ + --hash=sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e \ + --hash=sha256:320ade88cfb846b8cd6b4ddf5ee9e80ee0c1f52401f2456b84ae1ae6a1a5f207 \ + --hash=sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18 \ + --hash=sha256:36836d6ff945a00b88ba1e4572d721e60b5b8c98c155d465f56ad19d68f23734 \ + --hash=sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38 \ + --hash=sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110 \ + --hash=sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18 \ + --hash=sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44 \ + --hash=sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d \ + --hash=sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48 \ + --hash=sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e \ + --hash=sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5 \ + --hash=sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d \ + --hash=sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53 \ + --hash=sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790 \ + --hash=sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c \ + --hash=sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b \ + --hash=sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116 \ + --hash=sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d \ + --hash=sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10 \ + --hash=sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6 \ + --hash=sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2 \ + --hash=sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776 \ + --hash=sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a \ + --hash=sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265 \ + --hash=sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008 \ + --hash=sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943 \ + --hash=sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374 \ + --hash=sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246 \ + --hash=sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e \ + --hash=sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5 \ + --hash=sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616 \ + --hash=sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15 \ + --hash=sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41 \ + --hash=sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960 \ + --hash=sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752 \ + --hash=sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e \ + --hash=sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72 \ + --hash=sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7 \ + --hash=sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8 \ + --hash=sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b \ + --hash=sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4 \ + --hash=sha256:82b271f5137d07749f7bf32f70b17ab6eaabedd297e75dce75081a24f76eb545 \ + --hash=sha256:84c018e49c3bf790f9c2771c45e9313a08c2c2a6342b162cd650258b57817706 \ + --hash=sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366 \ + --hash=sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb \ + --hash=sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a \ + --hash=sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e \ + --hash=sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00 \ + --hash=sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f \ + --hash=sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a \ + --hash=sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1 \ + --hash=sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66 \ + --hash=sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356 \ + --hash=sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319 \ + --hash=sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4 \ + --hash=sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad \ + --hash=sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d \ + --hash=sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5 \ + --hash=sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7 \ + --hash=sha256:aef65cd602a6d0e0ff6f9930fcb1c8fec60dd2cfcb6facaf4bdb0e5873042db0 \ + --hash=sha256:af21eb4409a119e365397b2adbaca4c9ccab56543a65d5dbd9f920d6ac29f686 \ + --hash=sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34 \ + --hash=sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49 \ + --hash=sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c \ + --hash=sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1 \ + --hash=sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e \ + --hash=sha256:bd9b23791fe793e4968dba0c447e12f78e425c59fc0e3b97f6450f4781f3ee60 \ + --hash=sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0 \ + --hash=sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274 \ + --hash=sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d \ + --hash=sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0 \ + --hash=sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae \ + --hash=sha256:c593052c465475e64bbfe5dbd81680f64a67fdc752c56d7a0ae205dc8aeefe0f \ + --hash=sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d \ + --hash=sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe \ + --hash=sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3 \ + --hash=sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393 \ + --hash=sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1 \ + --hash=sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af \ + --hash=sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44 \ + --hash=sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00 \ + --hash=sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c \ + --hash=sha256:dca4bbc466a95ba9c0234ef56d7dd9509f63da22274589ebd4ed7f1f4d4c54e3 \ + --hash=sha256:dd915403e231e6b1809fe9b6d9fc55cf8fb5e02765ac625d9cd623342a7905d7 \ + --hash=sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd \ + --hash=sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e \ + --hash=sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b \ + --hash=sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8 \ + --hash=sha256:e5f4d355f0a2b1a31bc3edec6795b46324349c9cb25eed068049e4f472fb4259 \ + --hash=sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859 \ + --hash=sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46 \ + --hash=sha256:e80c8378d8f3d83cd3164da1ad2df9e37a666cdde7b1cb2298ed0b558064be30 \ + --hash=sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b \ + --hash=sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46 \ + --hash=sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24 \ + --hash=sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a \ + --hash=sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24 \ + --hash=sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc \ + --hash=sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215 \ + --hash=sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063 \ + --hash=sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832 \ + --hash=sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6 \ + --hash=sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79 \ + --hash=sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464 + # via requests +click==8.4.1 \ + --hash=sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2 \ + --hash=sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96 + # via black +idna==3.10 \ + --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ + --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 + # via requests +mypy-extensions==1.1.0 \ + --hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \ + --hash=sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558 + # via black +packaging==26.2 \ + --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ + --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 + # via black +pathspec==1.1.1 \ + --hash=sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a \ + --hash=sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189 + # via black +platformdirs==4.10.0 \ + --hash=sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7 \ + --hash=sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a + # via black +pytokens==0.4.1 \ + --hash=sha256:0fc71786e629cef478cbf29d7ea1923299181d0699dbe7c3c0f4a583811d9fc1 \ + --hash=sha256:11edda0942da80ff58c4408407616a310adecae1ddd22eef8c692fe266fa5009 \ + --hash=sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083 \ + --hash=sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1 \ + --hash=sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de \ + --hash=sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2 \ + --hash=sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a \ + --hash=sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1 \ + --hash=sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5 \ + --hash=sha256:30f51edd9bb7f85c748979384165601d028b84f7bd13fe14d3e065304093916a \ + --hash=sha256:34bcc734bd2f2d5fe3b34e7b3c0116bfb2397f2d9666139988e7a3eb5f7400e3 \ + --hash=sha256:3ad72b851e781478366288743198101e5eb34a414f1d5627cdd585ca3b25f1db \ + --hash=sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68 \ + --hash=sha256:42f144f3aafa5d92bad964d471a581651e28b24434d184871bd02e3a0d956037 \ + --hash=sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321 \ + --hash=sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc \ + --hash=sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7 \ + --hash=sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f \ + --hash=sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918 \ + --hash=sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9 \ + --hash=sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c \ + --hash=sha256:682fa37ff4d8e95f7df6fe6fe6a431e8ed8e788023c6bcc0f0880a12eab80ad1 \ + --hash=sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1 \ + --hash=sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3 \ + --hash=sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b \ + --hash=sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb \ + --hash=sha256:941d4343bf27b605e9213b26bfa1c4bf197c9c599a9627eb7305b0defcfe40c1 \ + --hash=sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a \ + --hash=sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4 \ + --hash=sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa \ + --hash=sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78 \ + --hash=sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe \ + --hash=sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9 \ + --hash=sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d \ + --hash=sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975 \ + --hash=sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440 \ + --hash=sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16 \ + --hash=sha256:da5baeaf7116dced9c6bb76dc31ba04a2dc3695f3d9f74741d7910122b456edc \ + --hash=sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d \ + --hash=sha256:dcafc12c30dbaf1e2af0490978352e0c4041a7cde31f4f81435c2a5e8b9cabb6 \ + --hash=sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6 \ + --hash=sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324 + # via black +requests==2.32.3 \ + --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ + --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 + # via test (tests/uv/lock/workspaces/pyproject.toml) +urllib3==2.3.0 \ + --hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \ + --hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d + # via requests diff --git a/tests/uv/toolchain/uv_help_test.py b/tests/uv/toolchain/uv_help_test.py index be5e755d91..c4515277f7 100755 --- a/tests/uv/toolchain/uv_help_test.py +++ b/tests/uv/toolchain/uv_help_test.py @@ -14,9 +14,9 @@ def test_uv_help(self): data_rpath = os.environ["DATA"] uv_help_path = rfiles.Rlocation(data_rpath) - assert ( - uv_help_path is not None - ), f"the rlocation path was not found: {data_rpath}" + assert uv_help_path is not None, ( + f"the rlocation path was not found: {data_rpath}" + ) uv_help = Path(uv_help_path).read_text() diff --git a/tests/uv/uv/uv_tests.bzl b/tests/uv/uv/uv_tests.bzl index 8009405cec..d11bf065d8 100644 --- a/tests/uv/uv/uv_tests.bzl +++ b/tests/uv/uv/uv_tests.bzl @@ -21,10 +21,12 @@ load("//python/private:common_labels.bzl", "labels") # buildifier: disable=bzl- load("//python/uv:uv_toolchain_info.bzl", "UvToolchainInfo") load("//python/uv/private:uv.bzl", "process_modules") # buildifier: disable=bzl-visibility load("//python/uv/private:uv_toolchain.bzl", "uv_toolchain") # buildifier: disable=bzl-visibility +load("//tests/support/mocks:mocks.bzl", "mocks") +load("//tests/support/platforms:platforms.bzl", "platform_targets") _tests = [] -def _mock_mctx(*modules, download = None, read = None): +def _uv_mock_mctx(*modules, download = None): # Here we construct a fake minimal manifest file that we use to mock what would # be otherwise read from GH files manifest_files = { @@ -32,6 +34,7 @@ def _mock_mctx(*modules, download = None, read = None): x: { "checksum": x + ".sha256", "kind": "executable-zip", + "target_triples": [x], } for x in ["linux", "osx"] } | { @@ -45,6 +48,7 @@ def _mock_mctx(*modules, download = None, read = None): x: { "checksum": x + ".sha256", "kind": "executable-zip", + "target_triples": [x], } for x in ["linux", "os", "osx", "something_extra"] } | { @@ -65,39 +69,20 @@ def _mock_mctx(*modules, download = None, read = None): for fname, contents in manifest_files.items() } - return struct( - path = str, - download = download or (lambda *_, **__: struct( - success = True, - wait = lambda: struct( - success = True, - ), - )), - read = read or (lambda x: fake_fs[x]), - modules = [ - struct( - name = modules[0].name, - tags = modules[0].tags, - is_root = modules[0].is_root, - ), - ] + [ - struct( - name = mod.name, - tags = mod.tags, - is_root = False, - ) - for mod in modules[1:] - ], + return mocks.mctx( + modules = list(modules), + mock_downloads = { + "*": download, + } if download else {}, + mock_files = fake_fs, ) def _mod(*, name = None, default = [], configure = [], is_root = True): - return struct( - name = name, # module_name - tags = struct( - default = default, - configure = configure, - ), + return mocks.module( + name, is_root = is_root, + default = default, + configure = configure, ) def _process_modules(env, **kwargs): @@ -147,7 +132,7 @@ def _configure(urls = None, sha256 = None, **kwargs): def _test_only_defaults(env): uv = _process_modules( env, - module_ctx = _mock_mctx( + module_ctx = _uv_mock_mctx( _mod( default = [ _default( @@ -180,7 +165,7 @@ def _test_manual_url_spec(env): calls = [] uv = _process_modules( env, - module_ctx = _mock_mctx( + module_ctx = _uv_mock_mctx( _mod( default = [ _default( @@ -201,12 +186,11 @@ def _test_manual_url_spec(env): configure = [ _configure( platform = "linux", - urls = ["https://example.org/download.zip"], + urls = ["https://example.org/1.0.0/manifest.json.zip"], sha256 = "deadbeef", ), ], ), - read = lambda *args, **kwargs: fail(args, kwargs), ), uv_repository = lambda **kwargs: calls.append(kwargs), ) @@ -226,7 +210,7 @@ def _test_manual_url_spec(env): "name": "uv_1_0_0_linux", "platform": "linux", "sha256": "deadbeef", - "urls": ["https://example.org/download.zip"], + "urls": ["https://example.org/1.0.0/manifest.json.zip"], "version": "1.0.0", }, ]) @@ -237,7 +221,7 @@ def _test_defaults(env): calls = [] uv = _process_modules( env, - module_ctx = _mock_mctx( + module_ctx = _uv_mock_mctx( _mod( default = [ _default( @@ -285,7 +269,7 @@ def _test_default_building(env): calls = [] uv = _process_modules( env, - module_ctx = _mock_mctx( + module_ctx = _uv_mock_mctx( _mod( default = [ _default( @@ -349,7 +333,7 @@ def _test_complex_configuring(env): calls = [] uv = _process_modules( env, - module_ctx = _mock_mctx( + module_ctx = _uv_mock_mctx( _mod( default = [ _default( @@ -461,7 +445,7 @@ def _test_non_rules_python_non_root_is_ignored(env): calls = [] uv = _process_modules( env, - module_ctx = _mock_mctx( + module_ctx = _uv_mock_mctx( _mod( default = [ _default( @@ -481,6 +465,7 @@ def _test_non_rules_python_non_root_is_ignored(env): configure = [ _configure(version = "6.6.6"), # use defaults whatever they are ], + is_root = False, ), ), uv_repository = lambda **kwargs: calls.append(kwargs), @@ -512,7 +497,7 @@ def _test_rules_python_does_not_take_precedence(env): calls = [] uv = _process_modules( env, - module_ctx = _mock_mctx( + module_ctx = _uv_mock_mctx( _mod( default = [ _default( @@ -537,6 +522,7 @@ def _test_rules_python_does_not_take_precedence(env): compatible_with = ["@platforms//os:osx"], ), ], + is_root = False, ), ), uv_repository = lambda **kwargs: calls.append(kwargs), @@ -575,7 +561,7 @@ def _test_toolchain_precedence(name): "//command_line_option:extra_toolchains": [ str(Label("//tests/uv/uv_toolchains:all")), ], - "//command_line_option:platforms": str(Label("//tests/support:linux_aarch64")), + "//command_line_option:platforms": str(platform_targets.LINUX_AARCH64), }, ) diff --git a/tests/validate_test_main/BUILD.bazel b/tests/validate_test_main/BUILD.bazel new file mode 100644 index 0000000000..94163a2fde --- /dev/null +++ b/tests/validate_test_main/BUILD.bazel @@ -0,0 +1,10 @@ +load("//python:py_test.bzl", "py_test") + +py_test( + name = "validate_test_main_test", + srcs = ["validate_test_main_test.py"], + main = "validate_test_main_test.py", + deps = [ + "//python/private:py_test_main_validator_lib", + ], +) diff --git a/tests/validate_test_main/validate_test_main_test.py b/tests/validate_test_main/validate_test_main_test.py new file mode 100644 index 0000000000..4cbc55c937 --- /dev/null +++ b/tests/validate_test_main/validate_test_main_test.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python3 +import ast +import textwrap +import unittest + +from python.private.py_test_main_validator import module_runs_tests + + +def _runs_tests(source: str) -> bool: + tree = ast.parse(textwrap.dedent(source)) + return module_runs_tests(tree) + + +class ModuleRunsTestsTest(unittest.TestCase): + def test_only_definitions_is_rejected(self): + self.assertFalse( + _runs_tests( + """ + import unittest + + class MyTest(unittest.TestCase): + def test_foo(self): + self.assertTrue(True) + """ + ) + ) + + def test_definitions_with_assignments_is_rejected(self): + self.assertFalse( + _runs_tests( + """ + import unittest + + CONSTANT = 5 + + class MyTest(unittest.TestCase): + def test_foo(self): + pass + + def helper(): pass + """ + ) + ) + + def test_global_statement_with_definition_is_rejected(self): + self.assertFalse( + _runs_tests( + """ + global x + + class MyTest: + def test_foo(self): + pass + """ + ) + ) + + def test_assert_statement_with_definition_is_rejected(self): + # A bare `assert` with an inert condition runs no tests. + self.assertFalse( + _runs_tests( + """ + assert True + + class MyTest: + def test_foo(self): + pass + """ + ) + ) + + def test_assert_statement_with_active_expression_runs_tests(self): + # An assert whose condition runs a call actually runs the tests. + self.assertTrue( + _runs_tests( + """ + import unittest + + class MyTest(unittest.TestCase): + def test_foo(self): + pass + + assert unittest.main() + """ + ) + ) + + @unittest.skipUnless( + hasattr(ast, "TypeAlias"), "PEP 695 type aliases require Python 3.12+" + ) + def test_type_alias_with_definition_is_rejected(self): + self.assertFalse( + _runs_tests( + """ + type Alias = int + + class MyTest: + def test_foo(self): + pass + """ + ) + ) + + def test_type_checking_block_with_definition_is_rejected(self): + # `if TYPE_CHECKING:` guarding typing-only imports is inert; with a test + # definition and no runner, the module is still rejected. + self.assertFalse( + _runs_tests( + """ + from typing import TYPE_CHECKING + import unittest + + if TYPE_CHECKING: + from typing import Any + + class MyTest(unittest.TestCase): + def test_foo(self): + pass + """ + ) + ) + + def test_try_except_import_error_with_definition_is_rejected(self): + # `try: import foo except ImportError:` optional imports are inert. + self.assertFalse( + _runs_tests( + """ + try: + import foo + except ImportError: + foo = None + + import unittest + + class MyTest(unittest.TestCase): + def test_foo(self): + pass + """ + ) + ) + + def test_definitions_with_active_assignment_runs_tests(self): + # An assignment whose value runs a call actually runs the tests. + self.assertTrue( + _runs_tests( + """ + import unittest + + class MyTest(unittest.TestCase): + def test_foo(self): + pass + + exit_code = unittest.main() + """ + ) + ) + + def test_definitions_with_active_expression_runs_tests(self): + self.assertTrue( + _runs_tests( + """ + import sys + import unittest + + class MyTest(unittest.TestCase): + def test_foo(self): + pass + + sys.exit(unittest.main()) + """ + ) + ) + + def test_if_block_invoking_runner_runs_tests(self): + # A runner call inside an `if` body must still count as active. + self.assertTrue( + _runs_tests( + """ + import sys + import unittest + + class MyTest(unittest.TestCase): + def test_foo(self): + pass + + if "--run" in sys.argv: + unittest.main() + """ + ) + ) + + def test_import_only_module_is_allowed(self): + # A module that defines nothing and only imports other modules is not + # the "defined but never run" case, so it is allowed. + self.assertTrue( + _runs_tests( + """ + import my_tests + from my_pkg.tests import suite + """ + ) + ) + + def test_imports_and_assignments_without_definitions_is_allowed(self): + self.assertTrue( + _runs_tests( + """ + import my_tests + + CONSTANT = 5 + """ + ) + ) + + def test_empty_module_is_allowed(self): + self.assertTrue(_runs_tests("")) + + def test_docstring_only_is_allowed(self): + self.assertTrue(_runs_tests('"""A module docstring."""')) + + def test_if_name_main_guard_runs_tests(self): + self.assertTrue( + _runs_tests( + """ + import unittest + + class MyTest(unittest.TestCase): + def test_foo(self): + pass + + if __name__ == "__main__": + unittest.main() + """ + ) + ) + + def test_bare_call_runs_tests(self): + self.assertTrue( + _runs_tests( + """ + import pytest + pytest.main() + """ + ) + ) + + def test_top_level_loop_runs_tests(self): + self.assertTrue( + _runs_tests( + """ + def f(): pass + + for _ in range(1): + f() + """ + ) + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/venv_site_packages_libs/BUILD.bazel b/tests/venv_site_packages_libs/BUILD.bazel index 92d5dec6d3..6a7b3b9e12 100644 --- a/tests/venv_site_packages_libs/BUILD.bazel +++ b/tests/venv_site_packages_libs/BUILD.bazel @@ -1,6 +1,6 @@ +load("@rules_shell//shell:sh_test.bzl", "sh_test") load("//python:py_library.bzl", "py_library") load("//tests/support:py_reconfig.bzl", "py_reconfig_test") -load("//tests/support:support.bzl", "SUPPORTS_BOOTSTRAP_SCRIPT") py_library( name = "user_lib", @@ -15,15 +15,28 @@ py_library( ], ) +sh_test( + name = "py_binary_other_module_test", + srcs = [ + "py_binary_other_module_test.sh", + ], + data = [ + "@other//:venv_bin", + ], + env = { + "VENV_BIN": "$(rootpath @other//:venv_bin)", + }, +) + py_reconfig_test( name = "venvs_site_packages_libs_test", srcs = ["bin.py"], bootstrap_impl = "script", main = "bin.py", - target_compatible_with = SUPPORTS_BOOTSTRAP_SCRIPT, venvs_site_packages = "yes", deps = [ ":closer_lib", + "//tests/venv_site_packages_libs/nested_with_pth", "//tests/venv_site_packages_libs/nspkg_alpha", "//tests/venv_site_packages_libs/nspkg_beta", "//tests/venv_site_packages_libs/pkgutil_top", @@ -32,5 +45,57 @@ py_reconfig_test( "@other//nspkg_gamma", "@other//nspkg_single", "@other//with_external_data", + "@whl_with_data1//:pkg", + "@whl_with_data2//:pkg", + ], +) + +py_reconfig_test( + name = "shared_lib_loading_test", + srcs = ["shared_lib_loading_test.py"], + bootstrap_impl = select({ + "@platforms//os:windows": "system_python", + "//conditions:default": "script", + }), + main = "shared_lib_loading_test.py", + venvs_site_packages = "yes", + deps = select({ + "@platforms//os:windows": [ + "@dev_pip//markupsafe", + ], + "//conditions:default": [ + "//tests/venv_site_packages_libs/ext_with_libs", + "@dev_pip//macholib", + "@dev_pip//pyelftools", + ], + }), +) + +py_reconfig_test( + name = "whl_scripts_runnable_test", + srcs = ["whl_scripts_runnable_test.py"], + bootstrap_impl = select({ + "@platforms//os:windows": "system_python", + "//conditions:default": "script", + }), + main = "whl_scripts_runnable_test.py", + venvs_site_packages = "yes", + deps = [ + "@whl_with_data1//:pkg", + "@whl_with_data2//:pkg", + ], +) + +py_reconfig_test( + name = "importlib_metadata_test", + srcs = ["importlib_metadata_test.py"], + bootstrap_impl = select({ + "@platforms//os:windows": "system_python", + "//conditions:default": "script", + }), + main = "importlib_metadata_test.py", + venvs_site_packages = "yes", + deps = [ + "@whl_with_data1//:pkg", ], ) diff --git a/tests/venv_site_packages_libs/app_files_building/app_files_building_tests.bzl b/tests/venv_site_packages_libs/app_files_building/app_files_building_tests.bzl index 0a0265eb8c..663ab6963e 100644 --- a/tests/venv_site_packages_libs/app_files_building/app_files_building_tests.bzl +++ b/tests/venv_site_packages_libs/app_files_building/app_files_building_tests.bzl @@ -1,16 +1,42 @@ "" -load("@bazel_skylib//lib:paths.bzl", "paths") load("@rules_testing//lib:analysis_test.bzl", "analysis_test") load("@rules_testing//lib:test_suite.bzl", "test_suite") -load("//python/private:py_info.bzl", "VenvSymlinkEntry", "VenvSymlinkKind") # buildifier: disable=bzl-visibility -load("//python/private:venv_runfiles.bzl", "build_link_map") # buildifier: disable=bzl-visibility +load("//python:py_info.bzl", "PyInfo", "VenvSymlinkEntry", "VenvSymlinkKind") +load("//python:py_library.bzl", "py_library") +load("//python/private:common_labels.bzl", "labels") # buildifier: disable=bzl-visibility +load("//python/private:venv_runfiles.bzl", "build_link_map", "get_venv_symlinks") # buildifier: disable=bzl-visibility +load("//tests/support:support.bzl", "SUPPORTS_BZLMOD") + +def _empty_files_impl(ctx): + files = [] + for p in ctx.attr.paths: + f = ctx.actions.declare_file(p) + ctx.actions.write(output = f, content = "") + files.append(f) + return [DefaultInfo(files = depset(files))] + +empty_files = rule( + implementation = _empty_files_impl, + attrs = { + "paths": attr.string_list( + doc = "A list of paths to create as files.", + mandatory = True, + ), + }, +) _tests = [] +# NOTE: In bzlmod, the workspace name is always "_main". +# Under workspace, the workspace name is the name configured in WORKSPACE, +# or "__main__" if was unspecified. +# NOTE: ctx.workspace_name is always the root workspace, not the workspace +# of the target being processed (ctx.label). def _ctx(workspace_name = "_main"): return struct( workspace_name = workspace_name, + label = Label("@@FAKE-CTX//:fake_ctx"), ) def _file(short_path): @@ -18,34 +44,33 @@ def _file(short_path): short_path = short_path, ) -def _entry(venv_path, link_to_path, files = [], **kwargs): +def _venv_symlink(venv_path, *, link_to_path = None, files = []): + return struct( + link_to_path = link_to_path, + venv_path = venv_path, + files = files, + ) + +def _venv_symlinks_from_entries(entries): + result = [] + for symlink_entry in entries: + result.append(struct( + venv_path = symlink_entry.venv_path, + link_to_path = symlink_entry.link_to_path, + files = [f.short_path for f in symlink_entry.files.to_list()], + )) + return sorted(result, key = lambda e: (e.link_to_path, e.venv_path)) + +def _entry(venv_path, link_to_path, files, **kwargs): kwargs.setdefault("kind", VenvSymlinkKind.LIB) kwargs.setdefault("package", None) kwargs.setdefault("version", None) - - def short_pathify(path): - path = paths.join(link_to_path, path) - - # In tests, `../` is used to step out of the link_to_path scope. - path = paths.normalize(path) - - # Treat paths starting with "+" as external references. This matches - # how bzlmod names things. - if link_to_path.startswith("+"): - # File.short_path to external repos have `../` prefixed - path = paths.join("../", path) - else: - # File.short_path in main repo is main-repo relative - _, _, path = path.partition("/") - return path + kwargs.setdefault("link_to_file", None) return VenvSymlinkEntry( venv_path = venv_path, link_to_path = link_to_path, - files = depset([ - _file(short_pathify(f)) - for f in files - ]), + files = depset(files), **kwargs ) @@ -60,27 +85,443 @@ _tests.append(_test_conflict_merging) def _test_conflict_merging_impl(env, _): entries = [ - _entry("a", "+pypi_a/site-packages/a", ["a.txt"]), - _entry("a/b", "+pypi_a_b/site-packages/a/b", ["b.txt"]), - _entry("x", "_main/src/x", ["x.txt"]), - _entry("x/p", "_main/src-dev/x/p", ["p.txt"]), - _entry("duplicate", "+dupe_a/site-packages/duplicate", ["d.py"]), - # This entry also provides a/x.py, but since the "a" entry is shorter - # and comes first, its version of x.py should win. - _entry("duplicate", "+dupe_b/site-packages/duplicate", ["d.py"]), + _entry("a", "+pypi_a/site-packages/a", [ + _file("../+pypi_a/site-packages/a/a.txt"), + ]), + _entry("a-1.0.dist-info", "+pypi_a/site-packages/a-1.0.dist-info", [ + _file("../+pypi_a/site-packages/a-1.0.dist-info/METADATA"), + ]), + _entry("a/b", "+pypi_a_b/site-packages/a/b", [ + _file("../+pypi_a_b/site-packages/a/b/b.txt"), + ]), + _entry("x", "_main/src/x", [ + _file("src/x/x.txt"), + ]), + _entry("x/p", "_main/src-dev/x/p", [ + _file("src-dev/x/p/p.txt"), + ]), + _entry("duplicate", "+dupe_a/site-packages/duplicate", [ + _file("../+dupe_a/site-packages/duplicate/d.py"), + ]), + _entry("duplicate", "+dupe_b/site-packages/duplicate", [ + _file("../+dupe_b/site-packages/duplicate/d.py"), + ]), + # Case: two distributions provide the same file (instead of directory) + _entry("ff/fmod.py", "+ff_a/site-packages/ff/fmod.py", [ + _file("../+ff_a/site-packages/ff/fmod.py"), + ]), + _entry("ff/fmod.py", "+ff_b/site-packages/ff/fmod.py", [ + _file("../+ff_b/site-packages/ff/fmod.py"), + ]), ] - actual = build_link_map(_ctx(), entries) + actual, conflicts = build_link_map(_ctx(), entries, return_conflicts = True) expected_libs = { + "a-1.0.dist-info": "+pypi_a/site-packages/a-1.0.dist-info", "a/a.txt": _file("../+pypi_a/site-packages/a/a.txt"), "a/b/b.txt": _file("../+pypi_a_b/site-packages/a/b/b.txt"), "duplicate/d.py": _file("../+dupe_a/site-packages/duplicate/d.py"), + "ff/fmod.py": _file("../+ff_a/site-packages/ff/fmod.py"), "x/p/p.txt": _file("src-dev/x/p/p.txt"), "x/x.txt": _file("src/x/x.txt"), } env.expect.that_dict(actual[VenvSymlinkKind.LIB]).contains_exactly(expected_libs) env.expect.that_dict(actual).keys().contains_exactly([VenvSymlinkKind.LIB]) + env.expect.that_int(len(conflicts)).is_greater_than(0) + +def _test_optimized_grouping_complex(name): + empty_files( + name = name + "_files", + paths = [ + "site-packages/pkg1/a.txt", + "site-packages/pkg1/b/b_mod_so", + "site-packages/pkg1/c/c1.txt", + "site-packages/pkg1/c/c2.txt", + "site-packages/pkg1/d/d1.txt", + "site-packages/pkg1/dd/dd1.txt", + "site-packages/pkg1/q1/q1.txt", + "site-packages/pkg1/q1/q2a/libq.so", + "site-packages/pkg1/q1/q2a/q2.txt", + "site-packages/pkg1/q1/q2a/q3/q3a.txt", + "site-packages/pkg1/q1/q2a/q3/q3b.txt", + "site-packages/pkg1/q1/q2b/q2b.txt", + "site-packages/pkg1/q1/q2c/c_mod.so", + "site-packages/pkg1/q1/q2c/q2.txt", + "site-packages/pkg1/q1/q2c/q3/q3a.txt", + "site-packages/pkg1/q1/q2c/q3/q3b.txt", + ], + ) + analysis_test( + name = name, + impl = _test_optimized_grouping_complex_impl, + target = name + "_files", + ) + +_tests.append(_test_optimized_grouping_complex) + +def _test_optimized_grouping_complex_impl(env, target): + test_ctx = _ctx(workspace_name = env.ctx.workspace_name) + entries = get_venv_symlinks( + test_ctx, + target.files.to_list(), + package = "pkg1", + version_str = "1.0", + site_packages_root = env.ctx.label.package + "/site-packages", + ) + actual = _venv_symlinks_from_entries(entries) + + rr = "{}/{}/site-packages/".format(test_ctx.workspace_name, env.ctx.label.package) + expected = [ + _venv_symlink( + "pkg1/a.txt", + link_to_path = rr + "pkg1/a.txt", + files = [ + "tests/venv_site_packages_libs/app_files_building/site-packages/pkg1/a.txt", + ], + ), + _venv_symlink( + "pkg1/b", + link_to_path = rr + "pkg1/b", + files = [ + "tests/venv_site_packages_libs/app_files_building/site-packages/pkg1/b/b_mod_so", + ], + ), + _venv_symlink("pkg1/c", link_to_path = rr + "pkg1/c", files = [ + "tests/venv_site_packages_libs/app_files_building/site-packages/pkg1/c/c1.txt", + "tests/venv_site_packages_libs/app_files_building/site-packages/pkg1/c/c2.txt", + ]), + _venv_symlink("pkg1/d", link_to_path = rr + "pkg1/d", files = [ + "tests/venv_site_packages_libs/app_files_building/site-packages/pkg1/d/d1.txt", + ]), + _venv_symlink("pkg1/dd", link_to_path = rr + "pkg1/dd", files = [ + "tests/venv_site_packages_libs/app_files_building/site-packages/pkg1/dd/dd1.txt", + ]), + _venv_symlink("pkg1/q1/q1.txt", link_to_path = rr + "pkg1/q1/q1.txt", files = [ + "tests/venv_site_packages_libs/app_files_building/site-packages/pkg1/q1/q1.txt", + ]), + _venv_symlink("pkg1/q1/q2a/libq.so", link_to_path = rr + "pkg1/q1/q2a/libq.so", files = [ + "tests/venv_site_packages_libs/app_files_building/site-packages/pkg1/q1/q2a/libq.so", + ]), + _venv_symlink("pkg1/q1/q2a/q2.txt", link_to_path = rr + "pkg1/q1/q2a/q2.txt", files = [ + "tests/venv_site_packages_libs/app_files_building/site-packages/pkg1/q1/q2a/q2.txt", + ]), + _venv_symlink("pkg1/q1/q2a/q3", link_to_path = rr + "pkg1/q1/q2a/q3", files = [ + "tests/venv_site_packages_libs/app_files_building/site-packages/pkg1/q1/q2a/q3/q3a.txt", + "tests/venv_site_packages_libs/app_files_building/site-packages/pkg1/q1/q2a/q3/q3b.txt", + ]), + _venv_symlink("pkg1/q1/q2b", link_to_path = rr + "pkg1/q1/q2b", files = [ + "tests/venv_site_packages_libs/app_files_building/site-packages/pkg1/q1/q2b/q2b.txt", + ]), + _venv_symlink("pkg1/q1/q2c/c_mod.so", link_to_path = rr + "pkg1/q1/q2c/c_mod.so", files = [ + "tests/venv_site_packages_libs/app_files_building/site-packages/pkg1/q1/q2c/c_mod.so", + ]), + _venv_symlink("pkg1/q1/q2c/q2.txt", link_to_path = rr + "pkg1/q1/q2c/q2.txt", files = [ + "tests/venv_site_packages_libs/app_files_building/site-packages/pkg1/q1/q2c/q2.txt", + ]), + _venv_symlink("pkg1/q1/q2c/q3", link_to_path = rr + "pkg1/q1/q2c/q3", files = [ + "tests/venv_site_packages_libs/app_files_building/site-packages/pkg1/q1/q2c/q3/q3a.txt", + "tests/venv_site_packages_libs/app_files_building/site-packages/pkg1/q1/q2c/q3/q3b.txt", + ]), + ] + expected = sorted(expected, key = lambda e: (e.link_to_path, e.venv_path)) + env.expect.that_collection( + actual, + ).contains_exactly(expected) + _, conflicts = build_link_map(test_ctx, entries, return_conflicts = True) + + # The point of the optimization is to avoid having to merge conflicts. + env.expect.that_collection(conflicts).contains_exactly([]) + +def _test_optimized_grouping_single_toplevel(name): + empty_files( + name = name + "_files", + paths = [ + "site-packages/pkg2/__init__.py", + "site-packages/pkg2/a.txt", + "site-packages/pkg2/b_mod_so", + ], + ) + analysis_test( + name = name, + impl = _test_optimized_grouping_single_toplevel_impl, + target = name + "_files", + ) + +_tests.append(_test_optimized_grouping_single_toplevel) + +def _test_optimized_grouping_single_toplevel_impl(env, target): + test_ctx = _ctx(workspace_name = env.ctx.workspace_name) + entries = get_venv_symlinks( + test_ctx, + target.files.to_list(), + package = "pkg2", + version_str = "1.0", + site_packages_root = env.ctx.label.package + "/site-packages", + ) + actual = _venv_symlinks_from_entries(entries) + + rr = "{}/{}/site-packages/".format(test_ctx.workspace_name, env.ctx.label.package) + expected = [ + _venv_symlink( + "pkg2", + link_to_path = rr + "pkg2", + files = [ + "tests/venv_site_packages_libs/app_files_building/site-packages/pkg2/__init__.py", + "tests/venv_site_packages_libs/app_files_building/site-packages/pkg2/a.txt", + "tests/venv_site_packages_libs/app_files_building/site-packages/pkg2/b_mod_so", + ], + ), + ] + expected = sorted(expected, key = lambda e: (e.link_to_path, e.venv_path)) + + env.expect.that_collection( + actual, + ).contains_exactly(expected) + + _, conflicts = build_link_map(test_ctx, entries, return_conflicts = True) + + # The point of the optimization is to avoid having to merge conflicts. + env.expect.that_collection(conflicts).contains_exactly([]) + +def _test_optimized_grouping_implicit_namespace_packages(name): + empty_files( + name = name + "_files", + paths = [ + # NOTE: An alphanumeric name with underscores is used to verify + # name matching is correct. + "site-packages/name_space9/part1/foo.py", + "site-packages/name_space9/part2/bar.py", + "site-packages/name_space9-1.0.dist-info/METADATA", + ], + ) + analysis_test( + name = name, + impl = _test_optimized_grouping_implicit_namespace_packages_impl, + target = name + "_files", + ) + +_tests.append(_test_optimized_grouping_implicit_namespace_packages) + +def _test_optimized_grouping_implicit_namespace_packages_impl(env, target): + test_ctx = _ctx(workspace_name = env.ctx.workspace_name) + entries = get_venv_symlinks( + test_ctx, + target.files.to_list(), + package = "pkg3", + version_str = "1.0", + site_packages_root = env.ctx.label.package + "/site-packages", + ) + actual = _venv_symlinks_from_entries(entries) + + rr = "{}/{}/site-packages/".format(test_ctx.workspace_name, env.ctx.label.package) + expected = [ + _venv_symlink( + "name_space9/part1", + link_to_path = rr + "name_space9/part1", + files = [ + "tests/venv_site_packages_libs/app_files_building/site-packages/name_space9/part1/foo.py", + ], + ), + _venv_symlink( + "name_space9/part2", + link_to_path = rr + "name_space9/part2", + files = [ + "tests/venv_site_packages_libs/app_files_building/site-packages/name_space9/part2/bar.py", + ], + ), + _venv_symlink( + "name_space9-1.0.dist-info", + link_to_path = rr + "name_space9-1.0.dist-info", + files = [ + "tests/venv_site_packages_libs/app_files_building/site-packages/name_space9-1.0.dist-info/METADATA", + ], + ), + ] + expected = sorted(expected, key = lambda e: (e.link_to_path, e.venv_path)) + env.expect.that_collection( + actual, + ).contains_exactly(expected) + + _, conflicts = build_link_map(test_ctx, entries, return_conflicts = True) + + # The point of the optimization is to avoid having to merge conflicts. + env.expect.that_collection(conflicts).contains_exactly([]) + +def _test_optimized_grouping_pkgutil_namespace_packages(name): + empty_files( + name = name + "_files", + paths = [ + "site-packages/pkgutilns/__init__.py", + "site-packages/pkgutilns/foo.py", + # Special cases: These dirnames under site-packages are always + # treated as namespace packages + "site-packages/nvidia/whatever/w.py", + ], + ) + analysis_test( + name = name, + impl = _test_optimized_grouping_pkgutil_namespace_packages_impl, + target = name + "_files", + ) + +_tests.append(_test_optimized_grouping_pkgutil_namespace_packages) + +def _test_optimized_grouping_pkgutil_namespace_packages_impl(env, target): + test_ctx = _ctx(workspace_name = env.ctx.workspace_name) + files = target.files.to_list() + ns_inits = [f for f in files if f.basename == "__init__.py"] + + entries = get_venv_symlinks( + test_ctx, + files, + package = "pkgutilns", + version_str = "1.0", + site_packages_root = env.ctx.label.package + "/site-packages", + namespace_package_files = ns_inits, + ) + actual = _venv_symlinks_from_entries(entries) + + rr = "{}/{}/site-packages/".format(test_ctx.workspace_name, env.ctx.label.package) + expected = [ + _venv_symlink( + "pkgutilns/__init__.py", + link_to_path = rr + "pkgutilns/__init__.py", + files = [ + "tests/venv_site_packages_libs/app_files_building/site-packages/pkgutilns/__init__.py", + ], + ), + _venv_symlink( + "pkgutilns/foo.py", + link_to_path = rr + "pkgutilns/foo.py", + files = [ + "tests/venv_site_packages_libs/app_files_building/site-packages/pkgutilns/foo.py", + ], + ), + _venv_symlink( + "nvidia/whatever", + link_to_path = rr + "nvidia/whatever", + files = [ + "tests/venv_site_packages_libs/app_files_building/site-packages/nvidia/whatever/w.py", + ], + ), + ] + expected = sorted(expected, key = lambda e: (e.link_to_path, e.venv_path)) + env.expect.that_collection( + actual, + ).contains_exactly(expected) + + _, conflicts = build_link_map(test_ctx, entries, return_conflicts = True) + + # The point of the optimization is to avoid having to merge conflicts. + env.expect.that_collection(conflicts).contains_exactly([]) + +def _test_optimized_grouping_pkgutil_whls(name): + """Verify that the whl_library pkgutli style detection logic works.""" + py_library( + name = name + "_lib", + deps = [ + "@pkgutil_nspkg1//:pkg", + "@pkgutil_nspkg2//:pkg", + ], + target_compatible_with = SUPPORTS_BZLMOD, + ) + analysis_test( + name = name, + impl = _test_optimized_grouping_pkgutil_whls_impl, + target = name + "_lib", + config_settings = { + labels.VENVS_SITE_PACKAGES: "yes", + }, + attr_values = dict( + target_compatible_with = SUPPORTS_BZLMOD, + ), + ) + +_tests.append(_test_optimized_grouping_pkgutil_whls) + +def _test_optimized_grouping_pkgutil_whls_impl(env, target): + test_ctx = _ctx(workspace_name = env.ctx.workspace_name) + actual_raw_entries = target[PyInfo].venv_symlinks.to_list() + + actual = _venv_symlinks_from_entries(actual_raw_entries) + + # The important condition is that the top-level 'nspkg' directory + # is NOT linked because it's a pkgutil namespace package. + env.expect.that_collection(actual).contains_exactly([ + # Entries from pkgutil_ns1 + _venv_symlink( + "nspkg/__init__.py", + link_to_path = "+internal_dev_deps+pkgutil_nspkg1/site-packages/nspkg/__init__.py", + files = [ + "../+internal_dev_deps+pkgutil_nspkg1/site-packages/nspkg/__init__.py", + ], + ), + _venv_symlink( + "nspkg/one", + link_to_path = "+internal_dev_deps+pkgutil_nspkg1/site-packages/nspkg/one", + files = [ + "../+internal_dev_deps+pkgutil_nspkg1/site-packages/nspkg/one/a.txt", + ], + ), + _venv_symlink( + "pkgutil_nspkg1-1.0.dist-info", + link_to_path = "+internal_dev_deps+pkgutil_nspkg1/site-packages/pkgutil_nspkg1-1.0.dist-info", + files = [ + "../+internal_dev_deps+pkgutil_nspkg1/site-packages/pkgutil_nspkg1-1.0.dist-info/INSTALLER", + "../+internal_dev_deps+pkgutil_nspkg1/site-packages/pkgutil_nspkg1-1.0.dist-info/METADATA", + "../+internal_dev_deps+pkgutil_nspkg1/site-packages/pkgutil_nspkg1-1.0.dist-info/RECORD", + "../+internal_dev_deps+pkgutil_nspkg1/site-packages/pkgutil_nspkg1-1.0.dist-info/WHEEL", + ], + ), + # Entries from pkgutil_ns2 + _venv_symlink( + "nspkg/__init__.py", + link_to_path = "+internal_dev_deps+pkgutil_nspkg2/site-packages/nspkg/__init__.py", + files = [ + "../+internal_dev_deps+pkgutil_nspkg2/site-packages/nspkg/__init__.py", + ], + ), + _venv_symlink( + "nspkg/two", + link_to_path = "+internal_dev_deps+pkgutil_nspkg2/site-packages/nspkg/two", + files = [ + "../+internal_dev_deps+pkgutil_nspkg2/site-packages/nspkg/two/b.txt", + ], + ), + _venv_symlink( + "pkgutil_nspkg2-1.0.dist-info", + link_to_path = "+internal_dev_deps+pkgutil_nspkg2/site-packages/pkgutil_nspkg2-1.0.dist-info", + files = [ + "../+internal_dev_deps+pkgutil_nspkg2/site-packages/pkgutil_nspkg2-1.0.dist-info/INSTALLER", + "../+internal_dev_deps+pkgutil_nspkg2/site-packages/pkgutil_nspkg2-1.0.dist-info/METADATA", + "../+internal_dev_deps+pkgutil_nspkg2/site-packages/pkgutil_nspkg2-1.0.dist-info/RECORD", + "../+internal_dev_deps+pkgutil_nspkg2/site-packages/pkgutil_nspkg2-1.0.dist-info/WHEEL", + ], + ), + ]) + + # Verifying that the expected VenvSymlink structure is processed with minimal number + # of conflicts (Just the single pkgutil style __init__.py file) + _, conflicts = build_link_map(test_ctx, actual_raw_entries, return_conflicts = True) + env.expect.that_collection(_venv_symlinks_from_entries(conflicts[0])).contains_exactly([ + _venv_symlink( + "nspkg/__init__.py", + link_to_path = "+internal_dev_deps+pkgutil_nspkg1/site-packages/nspkg/__init__.py", + files = [ + "../+internal_dev_deps+pkgutil_nspkg1/site-packages/nspkg/__init__.py", + ], + ), + _venv_symlink( + "nspkg/__init__.py", + link_to_path = "+internal_dev_deps+pkgutil_nspkg2/site-packages/nspkg/__init__.py", + files = [ + "../+internal_dev_deps+pkgutil_nspkg2/site-packages/nspkg/__init__.py", + ], + ), + ]) + env.expect.that_collection(conflicts).has_size(1) + def _test_package_version_filtering(name): analysis_test( name = name, @@ -92,8 +533,12 @@ _tests.append(_test_package_version_filtering) def _test_package_version_filtering_impl(env, _): entries = [ - _entry("foo", "+pypi_v1/site-packages/foo", ["foo.txt"], package = "foo", version = "1.0"), - _entry("foo", "+pypi_v2/site-packages/foo", ["bar.txt"], package = "foo", version = "2.0"), + _entry("foo", "+pypi_v1/site-packages/foo", [ + _file("../+pypi_v1/site-packages/foo/foo.txt"), + ], package = "foo", version = "1.0"), + _entry("foo", "+pypi_v2/site-packages/foo", [ + _file("../+pypi_v2/site-packages/foo/bar.txt"), + ], package = "foo", version = "2.0"), ] actual = build_link_map(_ctx(), entries) @@ -118,7 +563,7 @@ def _test_malformed_entry_impl(env, _): "a", "+pypi_a/site-packages/a", # This file is outside the link_to_path, so it should be ignored. - ["../outside.txt"], + [_file("../+pypi_a/site-packages/outside.txt")], ), # A second, conflicting, entry is added to force merging of the known # files. Without this, there's no conflict, so files is never @@ -126,7 +571,7 @@ def _test_malformed_entry_impl(env, _): _entry( "a", "+pypi_b/site-packages/a", - ["../outside.txt"], + [_file("../+pypi_b/site-packages/outside.txt")], ), ] @@ -146,11 +591,21 @@ _tests.append(_test_complex_namespace_packages) def _test_complex_namespace_packages_impl(env, _): entries = [ - _entry("a/b", "+pypi_a_b/site-packages/a/b", ["b.txt"]), - _entry("a/c", "+pypi_a_c/site-packages/a/c", ["c.txt"]), - _entry("x/y/z", "+pypi_x_y_z/site-packages/x/y/z", ["z.txt"]), - _entry("foo", "+pypi_foo/site-packages/foo", ["foo.txt"]), - _entry("foobar", "+pypi_foobar/site-packages/foobar", ["foobar.txt"]), + _entry("a/b", "+pypi_a_b/site-packages/a/b", [ + _file("../+pypi_a_b/site-packages/a/b/b.txt"), + ]), + _entry("a/c", "+pypi_a_c/site-packages/a/c", [ + _file("../+pypi_a_c/site-packages/a/cc.txt"), + ]), + _entry("x/y/z", "+pypi_x_y_z/site-packages/x/y/z", [ + _file("../+pypi_x_y_z/site-packages/x/y/z/z.txt"), + ]), + _entry("foo", "+pypi_foo/site-packages/foo", [ + _file("../+pypi_foo/site-packages/foo/foo.txt"), + ]), + _entry("foobar", "+pypi_foobar/site-packages/foobar", [ + _file("../+pypi_foobar/site-packages/foobar/foobar.txt"), + ]), ] actual = build_link_map(_ctx(), entries) @@ -198,20 +653,19 @@ def _test_multiple_venv_symlink_kinds_impl(env, _): _entry( "libfile", "+pypi_lib/site-packages/libfile", - ["lib.txt"], - kind = - VenvSymlinkKind.LIB, + [_file("../+pypi_lib/site-packages/libfile/lib.txt")], + kind = VenvSymlinkKind.LIB, ), _entry( "binfile", "+pypi_bin/bin/binfile", - ["bin.txt"], + [_file("../+pypi_bin/bin/binfile/bin.txt")], kind = VenvSymlinkKind.BIN, ), _entry( "includefile", "+pypi_include/include/includefile", - ["include.h"], + [_file("../+pypi_include/include/includefile/include.h")], kind = VenvSymlinkKind.INCLUDE, ), @@ -240,6 +694,68 @@ def _test_multiple_venv_symlink_kinds_impl(env, _): VenvSymlinkKind.INCLUDE, ]) +def _test_shared_library_symlinking(name): + empty_files( + name = name + "_files", + # NOTE: Test relies upon order + paths = [ + "site-packages/bar/libs/liby.so", + "site-packages/bar/x.py", + "site-packages/bar/y.so", + "site-packages/foo.libs/libx.so", + "site-packages/foo/a.py", + "site-packages/foo/b.so", + "site-packages/root.pth", + "site-packages/root.py", + "site-packages/root.so", + ], + ) + analysis_test( + name = name, + impl = _test_shared_library_symlinking_impl, + target = name + "_files", + ) + +_tests.append(_test_shared_library_symlinking) + +def _test_shared_library_symlinking_impl(env, target): + srcs = target.files.to_list() + actual_entries = get_venv_symlinks( + _ctx(), + srcs, + package = "foo", + version_str = "1.0", + site_packages_root = env.ctx.label.package + "/site-packages", + ) + + actual = _venv_symlinks_from_entries(actual_entries) + + env.expect.that_collection(actual).contains_at_least([ + _venv_symlink( + "bar/libs/liby.so", + link_to_path = "_main/tests/venv_site_packages_libs/app_files_building/site-packages/bar/libs/liby.so", + files = [ + "tests/venv_site_packages_libs/app_files_building/site-packages/bar/libs/liby.so", + ], + ), + _venv_symlink( + "foo.libs/libx.so", + link_to_path = "_main/tests/venv_site_packages_libs/app_files_building/site-packages/foo.libs/libx.so", + files = [ + "tests/venv_site_packages_libs/app_files_building/site-packages/foo.libs/libx.so", + ], + ), + ]) + + actual = build_link_map(_ctx(), actual_entries) + + # The important condition is that each lib*.so file is linked directly. + expected_libs = { + "bar/libs/liby.so": srcs[0], + "foo.libs/libx.so": srcs[3], + } + env.expect.that_dict(actual[VenvSymlinkKind.LIB]).contains_at_least(expected_libs) + def app_files_building_test_suite(name): test_suite( name = name, diff --git a/tests/venv_site_packages_libs/bin.py b/tests/venv_site_packages_libs/bin.py index 772925f00e..b14ff54144 100644 --- a/tests/venv_site_packages_libs/bin.py +++ b/tests/venv_site_packages_libs/bin.py @@ -1,5 +1,6 @@ import importlib import sys +import sysconfig import unittest from pathlib import Path @@ -9,7 +10,23 @@ def setUp(self): super().setUp() if sys.prefix == sys.base_prefix: raise AssertionError("Not running under a venv") - self.venv = sys.prefix + self.venv = Path(sys.prefix) + self.site_packages = Path(sysconfig.get_paths()["purelib"]) + + is_windows = sys.platform == "win32" + if is_windows: + self.bin_dir_name = Path("Scripts") + self.include_dir_name = Path("Include") + else: + self.bin_dir_name = Path("bin") + self.include_dir_name = Path("include") + + def assert_venv_path_exists(self, rel_path): + path = self.venv / rel_path + self.assertTrue( + path.exists(), + f"Expected {path} to exist. {path.parent.name} contents: {list(path.parent.iterdir()) if path.parent.exists() else 'N/A'}", + ) def assert_imported_from_venv(self, module_name): module = importlib.import_module(module_name) @@ -20,10 +37,12 @@ def assert_imported_from_venv(self, module_name): + f"__file__ set, but got None. {module=}", ) self.assertTrue( - module.__file__.startswith(self.venv), + module.__file__.startswith(str(self.venv)), f"\n{module_name} was imported, but not from the venv.\n" - + f"venv : {self.venv}\n" - + f"actual: {module.__file__}", + + f" venv: {self.venv}\n" + + f"module file: {module.__file__}\n" + + "sys.path:\n" + + "\n".join(sys.path), ) return module @@ -40,16 +59,14 @@ def test_imported_from_venv(self): self.assert_imported_from_venv("nspkg.subnspkg.delta") self.assert_imported_from_venv("single_file") self.assert_imported_from_venv("simple") + m = self.assert_imported_from_venv("nested_with_pth") + self.assertEqual(m.WHOAMI, "nested_with_pth") def test_data_is_included(self): self.assert_imported_from_venv("simple") - module = importlib.import_module("simple") - module_path = Path(module.__file__) - - site_packages = module_path.parent.parent - + module = importlib.import_module("simple") # noqa: F841 # Ensure that packages from simple v1 are not present - files = [p.name for p in site_packages.glob("*")] + files = [p.name for p in self.site_packages.glob("*")] self.assertIn("simple_v1_extras", files) def test_override_pkg(self): @@ -62,31 +79,78 @@ def test_override_pkg(self): def test_dirs_from_replaced_package_are_not_present(self): self.assert_imported_from_venv("simple") - module = importlib.import_module("simple") - module_path = Path(module.__file__) - - site_packages = module_path.parent.parent - dist_info_dirs = [p.name for p in site_packages.glob("*.dist-info")] + module = importlib.import_module("simple") # noqa: F841 + dist_info_dirs = [p.name for p in self.site_packages.glob("simple*.dist-info")] self.assertEqual( ["simple-1.0.0.dist-info"], dist_info_dirs, ) # Ensure that packages from simple v1 are not present - files = [p.name for p in site_packages.glob("*")] + files = [p.name for p in self.site_packages.glob("*")] self.assertNotIn("simple.libs", files) def test_data_from_another_pkg_is_included_via_copy_file(self): self.assert_imported_from_venv("simple") - module = importlib.import_module("simple") - module_path = Path(module.__file__) - - site_packages = module_path.parent.parent + module = importlib.import_module("simple") # noqa: F841 # Ensure that packages from simple v1 are not present - d = site_packages / "external_data" + d = self.site_packages / "external_data" files = [p.name for p in d.glob("*")] self.assertIn("another_module_data.txt", files) + def test_whl_with_data1_included(self): + module = self.assert_imported_from_venv("whl_with_data1") # noqa: F841 + site_packages_rel = self.site_packages.relative_to(self.venv) + # purelib + self.assert_venv_path_exists(site_packages_rel / "whl_with_data1/data_file.txt") + + # platlib + self.assert_venv_path_exists( + site_packages_rel / "whl_with_data1/platlib_file.txt" + ) + + venv_root = self.venv # noqa: F841 + + # data + self.assert_venv_path_exists("whl_with_data1/data_data_file.txt") + + # scripts + self.assert_venv_path_exists(self.bin_dir_name / "whl_script.sh") + + # headers + self.assert_venv_path_exists( + self.include_dir_name / "whl_with_data1/header_file.h" + ) + + def test_whl_with_data2_included(self): + module = self.assert_imported_from_venv("whl_with_data2") # noqa: F841 + + site_packages_rel = self.site_packages.relative_to(self.venv) + self.assert_venv_path_exists(site_packages_rel / "whl_with_data2/data_file.txt") + + self.assert_venv_path_exists(self.bin_dir_name / "whl_script.sh") + + # Ensure that `data` files are unpacked in `venv/root/` + # and then linked as `venv/whl_with_data1/data_data_file.txt`. + self.assert_venv_path_exists("whl_with_data2/data_data_file.txt") + + self.assert_venv_path_exists( + self.include_dir_name / "whl_with_data2/header_file.h" + ) + + def test_whl_with_data_overlap(self): + self.assert_venv_path_exists("overlap/both.txt") + self.assert_venv_path_exists("overlap/data1.txt") + self.assert_venv_path_exists("overlap/data2.txt") + + self.assert_venv_path_exists(self.bin_dir_name / "overlap/both.sh") + self.assert_venv_path_exists(self.bin_dir_name / "overlap/script1.sh") + self.assert_venv_path_exists(self.bin_dir_name / "overlap/script2.sh") + + self.assert_venv_path_exists(self.include_dir_name / "overlap/both.h") + self.assert_venv_path_exists(self.include_dir_name / "overlap/header1.h") + self.assert_venv_path_exists(self.include_dir_name / "overlap/header2.h") + if __name__ == "__main__": unittest.main() diff --git a/tests/venv_site_packages_libs/ext_with_libs/BUILD.bazel b/tests/venv_site_packages_libs/ext_with_libs/BUILD.bazel new file mode 100644 index 0000000000..a3a277dbb4 --- /dev/null +++ b/tests/venv_site_packages_libs/ext_with_libs/BUILD.bazel @@ -0,0 +1,94 @@ +load("@rules_cc//cc:cc_library.bzl", "cc_library") +load("@rules_cc//cc:cc_shared_library.bzl", "cc_shared_library") +load("//python:py_library.bzl", "py_library") +load("//tests/support:copy_file.bzl", "copy_file_to_dir") + +package( + default_visibility = ["//visibility:public"], +) + +cc_library( + name = "increment_impl", + srcs = ["increment.c"], + deps = [":increment_headers"], +) + +cc_library( + name = "increment_headers", + hdrs = ["increment.h"], +) + +cc_shared_library( + name = "increment", + user_link_flags = select({ + "@platforms//os:osx": [ + # Needed so that DT_NEEDED=libincrement.dylib can find + # this shared library + "-Wl,-install_name,@rpath/libincrement.dylib", + ], + "//conditions:default": [], + }), + deps = [":increment_impl"], +) + +cc_library( + name = "adder_impl", + srcs = ["adder.c"], + deps = [ + ":increment_headers", + "@rules_python//python/cc:current_py_cc_headers", + ], +) + +cc_shared_library( + name = "adder", + # Necessary for several reasons: + # 1. Ensures the output doesn't include increment itself (avoids ODRs) + # 2. Adds -lincrement (DT_NEEDED for libincrement.so) + # 3. Ensures libincrement.so is available at link time to satisfy (2) + dynamic_deps = [":increment"], + shared_lib_name = "adder.so", + tags = ["manual"], + # NOTE: cc_shared_library adds Bazelized rpath entries, too. + user_link_flags = [ + ] + select({ + "@platforms//os:osx": [ + "-Wl,-rpath,@loader_path/libs", + "-undefined", + "dynamic_lookup", + "-Wl,-exported_symbol", + "-Wl,_PyInit_adder", + ], + # Assume linux default + "//conditions:default": [ + "-Wl,-rpath,$ORIGIN/libs", + ], + }), + deps = [":adder_impl"], +) + +copy_file_to_dir( + name = "relocate_adder", + src = ":adder", + out_dir = "site-packages/ext_with_libs", + tags = ["manual"], +) + +copy_file_to_dir( + name = "relocate_increment", + src = ":increment", + out_dir = "site-packages/ext_with_libs/libs", + tags = ["manual"], +) + +py_library( + name = "ext_with_libs", + srcs = glob(["site-packages/**/*.py"]), + data = [ + ":relocate_adder", + ":relocate_increment", + ], + experimental_venvs_site_packages = "//python/config_settings:venvs_site_packages", + imports = ["site-packages"], + tags = ["manual"], +) diff --git a/tests/venv_site_packages_libs/ext_with_libs/adder.c b/tests/venv_site_packages_libs/ext_with_libs/adder.c new file mode 100644 index 0000000000..8b04b1721f --- /dev/null +++ b/tests/venv_site_packages_libs/ext_with_libs/adder.c @@ -0,0 +1,15 @@ +#include + +#include "increment.h" + +static PyObject *do_add(PyObject *self, PyObject *Py_UNUSED(args)) { + return PyLong_FromLong(increment(1)); +} + +static PyMethodDef AdderMethods[] = { + {"do_add", do_add, METH_NOARGS, "Add one"}, {NULL, NULL, 0, NULL}}; + +static struct PyModuleDef addermodule = {PyModuleDef_HEAD_INIT, "adder", NULL, + -1, AdderMethods}; + +PyMODINIT_FUNC PyInit_adder(void) { return PyModule_Create(&addermodule); } diff --git a/tests/venv_site_packages_libs/ext_with_libs/increment.c b/tests/venv_site_packages_libs/ext_with_libs/increment.c new file mode 100644 index 0000000000..b194325ac7 --- /dev/null +++ b/tests/venv_site_packages_libs/ext_with_libs/increment.c @@ -0,0 +1,3 @@ +#include "increment.h" + +int increment(int val) { return val + 1; } diff --git a/tests/venv_site_packages_libs/ext_with_libs/increment.h b/tests/venv_site_packages_libs/ext_with_libs/increment.h new file mode 100644 index 0000000000..8a13bf5621 --- /dev/null +++ b/tests/venv_site_packages_libs/ext_with_libs/increment.h @@ -0,0 +1,6 @@ +#ifndef TESTS_VENV_SITE_PACKAGES_LIBS_EXT_WITH_LIBS_INCREMENT_H_ +#define TESTS_VENV_SITE_PACKAGES_LIBS_EXT_WITH_LIBS_INCREMENT_H_ + +int increment(int); + +#endif // TESTS_VENV_SITE_PACKAGES_LIBS_EXT_WITH_LIBS_INCREMENT_H_ diff --git a/tests/venv_site_packages_libs/ext_with_libs/site-packages/ext_with_libs/__init__.py b/tests/venv_site_packages_libs/ext_with_libs/site-packages/ext_with_libs/__init__.py new file mode 100644 index 0000000000..ea0485cb1b --- /dev/null +++ b/tests/venv_site_packages_libs/ext_with_libs/site-packages/ext_with_libs/__init__.py @@ -0,0 +1,2 @@ +# This just marks the directory as a Pyton package. Python C extension modules +# and C libraries are populated in this directory at build time. diff --git a/tests/venv_site_packages_libs/importlib_metadata_test.py b/tests/venv_site_packages_libs/importlib_metadata_test.py new file mode 100644 index 0000000000..963d43b6e0 --- /dev/null +++ b/tests/venv_site_packages_libs/importlib_metadata_test.py @@ -0,0 +1,23 @@ +import importlib.metadata +import unittest + + +class ImportlibMetadataTest(unittest.TestCase): + def test_importlib_metadata_files(self): + files = importlib.metadata.files("whl-with-data1") + self.assertIsNotNone(files, "importlib.metadata.files returned None") + self.assertGreater( + len(files), 0, "importlib.metadata.files returned empty list" + ) + + # Verify it contains some expected files. + # The RECORD file lists paths relative to the installation root (site-packages). + # whl_with_data1-1.0.data/purelib/data_overlap.py should be installed as data_overlap.py + # whl_with_data1-1.0.data/platlib/whl_with_data1/platlib_file.txt should be whl_with_data1/platlib_file.txt + + file_names = [f.name for f in files] + self.assertIn("data_overlap.py", file_names) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/venv_site_packages_libs/nested_with_pth/BUILD.bazel b/tests/venv_site_packages_libs/nested_with_pth/BUILD.bazel new file mode 100644 index 0000000000..f0339270b4 --- /dev/null +++ b/tests/venv_site_packages_libs/nested_with_pth/BUILD.bazel @@ -0,0 +1,11 @@ +load("//python:py_library.bzl", "py_library") + +package(default_visibility = ["//visibility:public"]) + +py_library( + name = "nested_with_pth", + srcs = glob(["site-packages/**/*.py"]), + data = glob(["site-packages/*.pth"]), + experimental_venvs_site_packages = "//python/config_settings:venvs_site_packages", + imports = ["site-packages"], +) diff --git a/tests/venv_site_packages_libs/nested_with_pth/site-packages/nested.pth b/tests/venv_site_packages_libs/nested_with_pth/site-packages/nested.pth new file mode 100644 index 0000000000..924e0dccc8 --- /dev/null +++ b/tests/venv_site_packages_libs/nested_with_pth/site-packages/nested.pth @@ -0,0 +1 @@ +nested_sdk diff --git a/tests/venv_site_packages_libs/nested_with_pth/site-packages/nested_sdk/nested_with_pth/__init__.py b/tests/venv_site_packages_libs/nested_with_pth/site-packages/nested_sdk/nested_with_pth/__init__.py new file mode 100644 index 0000000000..adde64d3ef --- /dev/null +++ b/tests/venv_site_packages_libs/nested_with_pth/site-packages/nested_sdk/nested_with_pth/__init__.py @@ -0,0 +1 @@ +WHOAMI = "nested_with_pth" diff --git a/tests/venv_site_packages_libs/nspkg_alpha/BUILD.bazel b/tests/venv_site_packages_libs/nspkg_alpha/BUILD.bazel index aec415f7a0..9c0aa192a9 100644 --- a/tests/venv_site_packages_libs/nspkg_alpha/BUILD.bazel +++ b/tests/venv_site_packages_libs/nspkg_alpha/BUILD.bazel @@ -6,5 +6,5 @@ py_library( name = "nspkg_alpha", srcs = glob(["site-packages/**/*.py"]), experimental_venvs_site_packages = "//python/config_settings:venvs_site_packages", - imports = [package_name() + "/site-packages"], + imports = ["site-packages"], ) diff --git a/tests/venv_site_packages_libs/nspkg_beta/BUILD.bazel b/tests/venv_site_packages_libs/nspkg_beta/BUILD.bazel index 5d402183bd..d67ebf57d6 100644 --- a/tests/venv_site_packages_libs/nspkg_beta/BUILD.bazel +++ b/tests/venv_site_packages_libs/nspkg_beta/BUILD.bazel @@ -6,5 +6,5 @@ py_library( name = "nspkg_beta", srcs = glob(["site-packages/**/*.py"]), experimental_venvs_site_packages = "//python/config_settings:venvs_site_packages", - imports = [package_name() + "/site-packages"], + imports = ["site-packages"], ) diff --git a/tests/venv_site_packages_libs/pkgutil_top/BUILD.bazel b/tests/venv_site_packages_libs/pkgutil_top/BUILD.bazel index c805b1ad53..7a0d961ad8 100644 --- a/tests/venv_site_packages_libs/pkgutil_top/BUILD.bazel +++ b/tests/venv_site_packages_libs/pkgutil_top/BUILD.bazel @@ -6,5 +6,5 @@ py_library( name = "pkgutil_top", srcs = glob(["site-packages/**/*.py"]), experimental_venvs_site_packages = "//python/config_settings:venvs_site_packages", - imports = [package_name() + "/site-packages"], + imports = ["site-packages"], ) diff --git a/tests/venv_site_packages_libs/pkgutil_top_sub/BUILD.bazel b/tests/venv_site_packages_libs/pkgutil_top_sub/BUILD.bazel index 9d771628a0..52980fa790 100644 --- a/tests/venv_site_packages_libs/pkgutil_top_sub/BUILD.bazel +++ b/tests/venv_site_packages_libs/pkgutil_top_sub/BUILD.bazel @@ -6,5 +6,5 @@ py_library( name = "pkgutil_top_sub", srcs = glob(["site-packages/**/*.py"]), experimental_venvs_site_packages = "//python/config_settings:venvs_site_packages", - imports = [package_name() + "/site-packages"], + imports = ["site-packages"], ) diff --git a/tests/venv_site_packages_libs/py_binary_other_module_test.sh b/tests/venv_site_packages_libs/py_binary_other_module_test.sh new file mode 100755 index 0000000000..9fb71d5bc7 --- /dev/null +++ b/tests/venv_site_packages_libs/py_binary_other_module_test.sh @@ -0,0 +1,6 @@ +# Test that for a py_binary from a dependency module, we place links created via +# runfiles(...) in the right place. This tests the fix made for issues/3503 + +set -eu +echo "[*] Testing running the binary" +"$VENV_BIN" diff --git a/tests/venv_site_packages_libs/shared_lib_loading_test.py b/tests/venv_site_packages_libs/shared_lib_loading_test.py new file mode 100644 index 0000000000..a3f7bfcd5a --- /dev/null +++ b/tests/venv_site_packages_libs/shared_lib_loading_test.py @@ -0,0 +1,170 @@ +import importlib.util +import os +import sys +import unittest +from pathlib import Path + +# Optional imports for ELF/Mach-O analysis +if os.name == "posix" and sys.platform != "darwin": + from elftools.elf.elffile import ELFFile +else: + ELFFile = None + +if sys.platform == "darwin": + from macholib import mach_o + from macholib.MachO import MachO +else: + mach_o = None + MachO = None + +ELF_MAGIC = b"\x7fELF" +MACHO_MAGICS = ( + b"\xce\xfa\xed\xfe", # 32-bit big-endian + b"\xcf\xfa\xed\xfe", # 64-bit big-endian + b"\xfe\xed\xfa\xce", # 32-bit little-endian + b"\xfe\xed\xfa\xcf", # 64-bit little-endian +) + + +class SharedLibLoadingTest(unittest.TestCase): + def setUp(self): + super().setUp() + if sys.prefix == sys.base_prefix: + raise AssertionError("Not running under a venv") + self.venv = Path(sys.prefix) + + @unittest.skipIf(os.name == "nt", "Tests Unix-specific extension loading") + def test_shared_library_linking_unix(self): + try: + import ext_with_libs.adder + except ImportError as e: + spec = importlib.util.find_spec("ext_with_libs.adder") + if not spec or not spec.origin: + self.fail(f"Import failed and could not find module spec: {e}") + + info = self._get_linking_info(spec.origin) + + # Give a useful error message for debugging. + self.fail( + f"Failed to import adder extension.\n" + f"Original error: {e}\n" + f"Linking info for {spec.origin}:\n" + f" RPATHs: {info.get('rpaths', 'N/A')}\n" + f" Needed libs: {info.get('needed', 'N/A')}" + ) + + # Check that the module was loaded from the venv. + self.assertIn(".venv/", ext_with_libs.adder.__file__) + + adder_path = os.path.realpath(ext_with_libs.adder.__file__) + + with open(adder_path, "rb") as f: + magic_bytes = f.read(4) + + if magic_bytes == ELF_MAGIC: + self._assert_elf_linking(adder_path) + elif magic_bytes in MACHO_MAGICS: + self._assert_macho_linking(adder_path) + else: + self.fail(f"Unsupported file format for adder: magic bytes {magic_bytes!r}") + + # Check the function works regardless of format. + self.assertEqual(ext_with_libs.adder.do_add(), 2) + + @unittest.skipUnless(os.name == "nt", "Tests Windows-specific extension loading") + def test_shared_library_loading_windows(self): + # We import markupsafe._speedups (a .cp311-win_amd64.pyd extension) + try: + import markupsafe._speedups + + module = markupsafe._speedups + except ImportError as e: + self.fail( + f"Failed to import markupsafe._speedups: {e}\n" + + "sys.path:\n" + + "\n".join(sys.path) + ) + + # Verify it's in the venv + # Normalize paths for Windows comparison. + # We DON'T use resolve() here because we want to see the path Python used, + # which should be within the venv's site-packages (even if it's a symlink). + actual_file = str(Path(module.__file__)).lower() + expected_prefix = str(self.venv).lower() + + self.assertTrue( + actual_file.startswith(expected_prefix), + f"Module {module.__name__} not loaded from venv.\n" + f"Venv: {expected_prefix}\n" + f"Module file: {actual_file}\n" + f"sys.path:\n" + "\n".join(sys.path), + ) + + # Verify it's a shared library (.pyd) + self.assertTrue( + actual_file.endswith(".pyd"), + f"Expected .pyd extension, got {module.__file__}", + ) + + def _get_linking_info(self, path): + """Parses a shared library and returns its rpaths and dependencies.""" + path = os.path.realpath(path) + with open(path, "rb") as f: + magic_bytes = f.read(4) + + if magic_bytes == ELF_MAGIC: + return self._get_elf_info(path) + elif magic_bytes in MACHO_MAGICS: + return self._get_macho_info(path) + return {} + + def _get_elf_info(self, path): + """Extracts linking information from an ELF file.""" + info = {"rpaths": [], "needed": [], "undefined_symbols": []} + with open(path, "rb") as f: + elf = ELFFile(f) + dynamic = elf.get_section_by_name(".dynamic") + if dynamic: + for tag in dynamic.iter_tags(): + if tag.entry.d_tag == "DT_NEEDED": + info["needed"].append(tag.needed) + elif tag.entry.d_tag == "DT_RPATH": + info["rpaths"].append(tag.rpath) + elif tag.entry.d_tag == "DT_RUNPATH": + info["rpaths"].append(tag.runpath) + + dynsym = elf.get_section_by_name(".dynsym") + if dynsym: + info["undefined_symbols"] = [ + s.name + for s in dynsym.iter_symbols() + if s.entry["st_shndx"] == "SHN_UNDEF" + ] + return info + + def _get_macho_info(self, path): + """Extracts linking information from a Mach-O file.""" + info = {"rpaths": [], "needed": []} + macho = MachO(path) + for header in macho.headers: + for cmd_load, cmd, data in header.commands: + if cmd_load.cmd == mach_o.LC_LOAD_DYLIB: + info["needed"].append(data.decode().strip("\x00")) + elif cmd_load.cmd == mach_o.LC_RPATH: + info["rpaths"].append(data.decode().strip("\x00")) + return info + + def _assert_elf_linking(self, path): + """Asserts dynamic linking properties for an ELF file.""" + info = self._get_elf_info(path) + self.assertIn("libincrement.so", info["needed"]) + self.assertIn("increment", info["undefined_symbols"]) + + def _assert_macho_linking(self, path): + """Asserts dynamic linking properties for a Mach-O file.""" + info = self._get_macho_info(path) + self.assertIn("@rpath/libincrement.dylib", info["needed"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/venv_site_packages_libs/whl_scripts_runnable_test.py b/tests/venv_site_packages_libs/whl_scripts_runnable_test.py new file mode 100644 index 0000000000..a0c4210f71 --- /dev/null +++ b/tests/venv_site_packages_libs/whl_scripts_runnable_test.py @@ -0,0 +1,116 @@ +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +BAZEL_8_OR_LATER = bool(int(os.environ.get("BAZEL_8_OR_LATER", "0"))) + + +class WhlScriptsRunnableTest(unittest.TestCase): + maxDiff = None + + def _get_script_path(self, name): + is_windows = sys.platform == "win32" + if is_windows: + bin_dir = Path(sys.prefix) / "Scripts" + pathexts = os.environ.get("PATHEXT", ".COM;.EXE;.BAT;.CMD").split(";") + for ext in [""] + [e.lower() for e in pathexts]: + script_path = bin_dir / f"{name}{ext}" + if script_path.exists(): + return script_path + return bin_dir / name + else: + bin_dir = Path(sys.prefix) / "bin" + script_path = bin_dir / name + return script_path + + def test_script_is_runnable(self): + script_path = self._get_script_path("whl_with_data1_script") + self.assertTrue(script_path.exists(), f"Script not found at {script_path}") + + result = subprocess.run( + [str(script_path)], + capture_output=True, + text=True, + check=True, + ) + + output = result.stdout.splitlines() + self.assertIn("hello from whl_with_data1_script", output) + + # The script prints sys.executable as its second line + # Depending on how it's invoked, it might have more output, + # but the user said it prints the hello message AND sys.executable. + script_executable = output[-1].strip() + self.assertEqual(script_executable, sys.executable) + + def test_entry_point_is_runnable(self): + script_path = self._get_script_path("whl_with_data2_bin") + self.assertTrue(script_path.exists(), f"Entry point not found at {script_path}") + + result = subprocess.run( + [str(script_path)], + capture_output=True, + text=True, + check=True, + ) + + output = result.stdout.splitlines() + self.assertIn("hello from whl_with_data2_bin", output) + + script_executable = output[-1].strip() + self.assertEqual(script_executable, sys.executable) + + def test_pythonw_script(self): + script_path = self._get_script_path("whl_with_data1_pythonw") + self.assertTrue(script_path.exists(), f"Script not found at {script_path}") + + with open(script_path, "r", encoding="utf-8") as f: + first_line = f.readline() + + is_windows = sys.platform == "win32" + if is_windows: + # On Windows, the shebang is replaced with a batch wrapper that + # invokes the interpreter. + self.assertIn("pythonw.exe", first_line) + self.assertTrue( + first_line.startswith("@setlocal") + or first_line.startswith("@echo off"), + f"Expected Windows batch wrapper, got {first_line}", + ) + else: + self.assertTrue( + first_line.startswith("#!/bin/sh"), + f"Expected #!/bin/sh, got {first_line}", + ) + + # For some reason, on Windows, the subprocess can't write + # to the temporary files unless mkstemp is used. + temp_fd, temp_str = tempfile.mkstemp() + try: + os.close(temp_fd) + out_path = Path(temp_str) + subprocess.run( + [str(script_path), str(out_path)], + capture_output=True, + text=True, + check=True, + ) + output = out_path.read_text().splitlines() + finally: + os.unlink(temp_str) + self.assertIn("hello from whl_with_data1_pythonw", output) + + script_executable = output[-1].strip() + + if is_windows: + self.assertTrue( + script_executable.endswith("pythonw.exe"), + f"Expected pythonw.exe, got {script_executable}", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/whl_filegroup/extract_wheel_files_test.py b/tests/whl_filegroup/extract_wheel_files_test.py index 125d7f312c..4bf1bf3f11 100644 --- a/tests/whl_filegroup/extract_wheel_files_test.py +++ b/tests/whl_filegroup/extract_wheel_files_test.py @@ -26,7 +26,7 @@ def test_get_wheel_record(self) -> None: self.assertEqual(list(record), list(expected)) def test_get_files(self) -> None: - pattern = "(examples/wheel/lib/.*\.txt$|.*main)" + pattern = r"(examples/wheel/lib/.*\.txt$|.*main)" record = extract_wheel_files.get_record(_WHEEL) files = extract_wheel_files.get_files(record, pattern) expected = [ diff --git a/tests/whl_with_build_files/BUILD.bazel b/tests/whl_with_build_files/BUILD.bazel index e26dc1c3a6..1202876485 100644 --- a/tests/whl_with_build_files/BUILD.bazel +++ b/tests/whl_with_build_files/BUILD.bazel @@ -1,9 +1,9 @@ load("//python:py_test.bzl", "py_test") -load("//tests/support:support.bzl", "SUPPORTS_BZLMOD_UNIXY") +load("//tests/support:support.bzl", "SUPPORTS_BZLMOD") py_test( name = "verify_files_test", srcs = ["verify_files_test.py"], - target_compatible_with = SUPPORTS_BZLMOD_UNIXY, + target_compatible_with = SUPPORTS_BZLMOD, deps = ["@somepkg_with_build_files//:pkg"], ) diff --git a/tests/whl_with_build_files/testdata/somepkg-1.0.dist-info/METADATA b/tests/whl_with_build_files/testdata/somepkg-1.0.dist-info/METADATA index e69de29bb2..0829d1f756 100644 --- a/tests/whl_with_build_files/testdata/somepkg-1.0.dist-info/METADATA +++ b/tests/whl_with_build_files/testdata/somepkg-1.0.dist-info/METADATA @@ -0,0 +1,2 @@ +Name: somepkg +Version: 1.0 diff --git a/tests/whl_with_build_files/verify_files_test.py b/tests/whl_with_build_files/verify_files_test.py index cfbbaa3aff..8d16b34348 100644 --- a/tests/whl_with_build_files/verify_files_test.py +++ b/tests/whl_with_build_files/verify_files_test.py @@ -2,7 +2,6 @@ class VerifyFilestest(unittest.TestCase): - def test_wheel_with_build_files_importable(self): # If the BUILD files are present, then these imports should fail # because globs won't pass package boundaries, and the necessary @@ -10,7 +9,7 @@ def test_wheel_with_build_files_importable(self): import somepkg import somepkg.a import somepkg.subpkg - import somepkg.subpkg.b + import somepkg.subpkg.b # noqa: F401 if __name__ == "__main__": diff --git a/tools/BUILD.bazel b/tools/BUILD.bazel index 0fcce8f729..7829b33318 100644 --- a/tools/BUILD.bazel +++ b/tools/BUILD.bazel @@ -31,6 +31,7 @@ filegroup( "wheelmaker.py", "//tools/launcher:distribution", "//tools/precompiler:distribution", + "//tools/private:distribution", "//tools/publish:distribution", ], visibility = ["//:__pkg__"], diff --git a/tools/build_defs/python/private/BUILD.bazel b/tools/build_defs/python/private/BUILD.bazel index 0a7f308f02..e189b18880 100644 --- a/tools/build_defs/python/private/BUILD.bazel +++ b/tools/build_defs/python/private/BUILD.bazel @@ -20,8 +20,9 @@ filegroup( visibility = ["//python:__subpackages__"], ) +# keep bzl_library( - name = "py_internal_renamed_bzl", + name = "py_internal_renamed", srcs = ["py_internal_renamed.bzl"], - visibility = ["@rules_python_internal//:__subpackages__"], + visibility = ["//:__subpackages__"], ) diff --git a/tools/precompiler/BUILD.bazel b/tools/precompiler/BUILD.bazel index 268f41b032..e055980c72 100644 --- a/tools/precompiler/BUILD.bazel +++ b/tools/precompiler/BUILD.bazel @@ -14,6 +14,7 @@ load("@bazel_skylib//rules:common_settings.bzl", "string_list_flag") load("//python/private:py_interpreter_program.bzl", "py_interpreter_program") # buildifier: disable=bzl-visibility +load("//python/private:visibility.bzl", "NOT_ACTUALLY_PUBLIC") # buildifier: disable=bzl-visibility filegroup( name = "distribution", @@ -25,11 +26,9 @@ py_interpreter_program( name = "precompiler", execution_requirements = ":execution_requirements", main = "precompiler.py", - visibility = [ - # Not actually public. Only public so rules_python-generated toolchains - # are able to reference it. - "//visibility:public", - ], + # Not actually public. Only public so rules_python-generated toolchains + # are able to reference it. + visibility = NOT_ACTUALLY_PUBLIC, ) string_list_flag( diff --git a/tools/precompiler/precompiler.py b/tools/precompiler/precompiler.py index e7c693c195..f83dd15951 100644 --- a/tools/precompiler/precompiler.py +++ b/tools/precompiler/precompiler.py @@ -79,7 +79,7 @@ def _compile(options: "argparse.Namespace") -> None: class _SerialPersistentWorker: """Simple, synchronous, serial persistent worker.""" - def __init__(self, instream: "typing.TextIO", outstream: "typing.TextIO"): + def __init__(self, instream: "typing.TextIO", outstream: "typing.TextIO"): # noqa: F821 self._instream = instream self._outstream = outstream self._parser = _create_parser() @@ -148,7 +148,7 @@ def _send_response(self, response: "JsonWorkResponse") -> None: class _AsyncPersistentWorker: """Asynchronous, concurrent, persistent worker.""" - def __init__(self, reader: "typing.TextIO", writer: "typing.TextIO"): + def __init__(self, reader: "typing.TextIO", writer: "typing.TextIO"): # noqa: F821 self._reader = reader self._writer = writer self._parser = _create_parser() @@ -156,13 +156,15 @@ def __init__(self, reader: "typing.TextIO", writer: "typing.TextIO"): self._task_to_request_id = {} @classmethod - async def main(cls, instream: "typing.TextIO", outstream: "typing.TextIO") -> None: + async def main(cls, instream: "typing.TextIO", outstream: "typing.TextIO") -> None: # noqa: F821 reader, writer = await cls._connect_streams(instream, outstream) await cls(reader, writer).run() @classmethod async def _connect_streams( - cls, instream: "typing.TextIO", outstream: "typing.TextIO" + cls, + instream: "typing.TextIO", # noqa: F821 + outstream: "typing.TextIO", # noqa: F821 ) -> "tuple[asyncio.StreamReader, asyncio.StreamWriter]": loop = asyncio.get_event_loop() reader = asyncio.StreamReader() @@ -214,7 +216,7 @@ async def _process_request(self, request: "JsonWorkRequest") -> None: # We don't send a response because we assume the request that # triggered cancelling sent the response raise - except: + except Exception: _logger.exception("Unhandled error: request=%s", request) self._send_response( { diff --git a/tools/private/BUILD.bazel b/tools/private/BUILD.bazel index e69de29bb2..ae6951c245 100644 --- a/tools/private/BUILD.bazel +++ b/tools/private/BUILD.bazel @@ -0,0 +1,18 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + +package( + default_visibility = ["//:__subpackages__"], +) + +filegroup( + name = "distribution", + srcs = glob(["**"]) + [ + "//tools/private/zipapp:distribution", + ], +) + +bzl_library( + name = "publish_deps", + srcs = ["publish_deps.bzl"], + deps = ["//python/uv/private:lock"], +) diff --git a/tools/private/debug/print_defined_toolchains.sh b/tools/private/debug/print_defined_toolchains.sh new file mode 100755 index 0000000000..1694dc975c --- /dev/null +++ b/tools/private/debug/print_defined_toolchains.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Programmatically probe which repository target name is resolved successfully inside this workspace +if bazel query @pythons_hub//... >/dev/null 2>&1; then + HUB_REPO="@pythons_hub" +elif bazel query @@rules_python++python+pythons_hub//... >/dev/null 2>&1; then + HUB_REPO="@@rules_python++python+pythons_hub" +else + HUB_REPO="@@+python+pythons_hub" +fi + +# Query standard toolchains inside the resolved hub repository, excluding CC and Exec Tools toolchains. +bazel query "kind('toolchain', ${HUB_REPO}//...) - filter('_py_cc_toolchain$', ${HUB_REPO}//...) - filter('_py_exec_tools_toolchain$', ${HUB_REPO}//...)" "$@" diff --git a/tools/private/gazelle/BUILD.bazel b/tools/private/gazelle/BUILD.bazel new file mode 100644 index 0000000000..2a38e8f3a1 --- /dev/null +++ b/tools/private/gazelle/BUILD.bazel @@ -0,0 +1,21 @@ +load("@bazel_gazelle//:def.bzl", "gazelle", "gazelle_binary") + +package( + default_visibility = ["//:__subpackages__"], +) + +gazelle_binary( + name = "gazelle_bin", + languages = [ + "@bazel_skylib_gazelle_plugin//bzl", + ], + # Marked manual to avoid building in CI, which fails due to copt settings. + tags = ["manual"], +) + +gazelle( + name = "gazelle", + gazelle = ":gazelle_bin", + # Marked manual to avoid building in CI, which fails due to copt settings. + tags = ["manual"], +) diff --git a/tools/private/release/BUILD.bazel b/tools/private/release/BUILD.bazel index 31cc3a0239..a63d2111f7 100644 --- a/tools/private/release/BUILD.bazel +++ b/tools/private/release/BUILD.bazel @@ -1,12 +1,29 @@ -load("@rules_python//python:defs.bzl", "py_binary") +load("@rules_python//python:defs.bzl", "py_binary", "py_library") package(default_visibility = ["//visibility:public"]) +py_library( + name = "changelog_news", + srcs = ["changelog_news.py"], +) + +py_library( + name = "release_lib", + srcs = glob( + ["*.py"], + exclude = ["changelog_news.py"], + ), + deps = [ + ":changelog_news", + "@dev_pip//packaging", + ], +) + py_binary( name = "release", srcs = ["release.py"], main = "release.py", deps = [ - "@dev_pip//packaging", + ":release_lib", ], ) diff --git a/tools/private/release/__init__.py b/tools/private/release/__init__.py new file mode 100644 index 0000000000..e68f0391f8 --- /dev/null +++ b/tools/private/release/__init__.py @@ -0,0 +1 @@ +"""Release tools package.""" diff --git a/tools/private/release/add_backports.py b/tools/private/release/add_backports.py new file mode 100644 index 0000000000..63c70af7d7 --- /dev/null +++ b/tools/private/release/add_backports.py @@ -0,0 +1,118 @@ +"""Subcommand to add PRs to the release tracking issue backports checklist.""" + +from tools.private.release.gh import GitHub +from tools.private.release.release_issue import ( + add_backports_to_body, + add_rc_task_to_body, + add_sync_changelog_task_to_body, + parse_checklist_state, +) + + +class AddBackports: + """Class to add PRs to the release tracking issue.""" + + def __init__(self, args, gh: GitHub): + self.args = args + self.gh = gh + + def run(self) -> int: + """Executes the add-backports subcommand.""" + args = self.args + + issue_num = args.issue + if not issue_num: + print( + "No issue specified. Trying to auto-discover active release" + " tracking issue..." + ) + try: + open_issues = self.gh.get_open_tracking_issues() + if not open_issues: + print("Error: No open release tracking issues found.") + return 1 + if len(open_issues) > 1: + print( + "Error: Multiple open release tracking issues found." + " Cannot determine active one:" + ) + for issue in open_issues: + print(f"- #{issue['number']}: {issue['title']}") + return 1 + issue_num = open_issues[0]["number"] + print(f"Auto-discovered active release tracking issue: #{issue_num}") + except Exception as e: + print(f"Error auto-discovering tracking issue: {e}") + return 1 + + resolved_prs = [] + for pr_ref in args.prs: + try: + pr_num = self.gh.resolve_pr_number(pr_ref) + resolved_prs.append(pr_num) + except Exception as e: + print(f"Error resolving PR ref '{pr_ref}': {e}") + return 1 + + print( + f"Adding backports {resolved_prs} (resolved from {args.prs}) to tracking issue #{issue_num}..." + ) + try: + body = self.gh.get_issue_body(issue_num) + items_to_add = [{"ref": f"#{pr}"} for pr in resolved_prs] + body = add_backports_to_body(body, items_to_add) + for pr in resolved_prs: + body = add_sync_changelog_task_to_body(body, pr) + state = parse_checklist_state(body) + rc_tags = state.get("rc_tags", {}) + has_pending_rc = any( + not task.checked and task.status != "done" for task in rc_tags.values() + ) + next_rc_num = max(rc_tags.keys()) + 1 if rc_tags else 0 + if not has_pending_rc: + print( + f"No pending RC task found. Adding 'Tag" + f" RC{next_rc_num}' to checklist..." + ) + body = add_rc_task_to_body(body, next_rc_num) + except ValueError as e: + print(f"Error: {e}") + return 1 + except Exception as e: + print(f"Failed to update tracking issue: {e}") + return 1 + + try: + self.gh.update_issue_body(issue_num, body) + print("Successfully updated tracking issue checklist.") + except Exception as e: + print(f"Failed to update tracking issue body: {e}") + return 1 + + return 0 + + @classmethod + def add_parser(cls, subparsers): + """Adds parser for add-backports subcommand.""" + parser = subparsers.add_parser( + "add-backports", + help="Add PRs to the release tracking issue backports checklist.", + ) + parser.add_argument( + "prs", + type=str, + nargs="+", + help="PR references (numbers, #numbers, or URLs) to add (positional, space-separated).", + ) + parser.add_argument( + "--issue", + type=int, + help="The tracking issue number. If omitted, will try to auto-discover the active release tracking issue.", + ) + parser.set_defaults(command=cls.run_from_args) + + @classmethod + def run_from_args(cls, args): + """Instantiates and runs the command from parsed args.""" + gh = GitHub() + return cls(args, gh).run() diff --git a/tools/private/release/changelog_news.py b/tools/private/release/changelog_news.py new file mode 100644 index 0000000000..e09b082ade --- /dev/null +++ b/tools/private/release/changelog_news.py @@ -0,0 +1,339 @@ +"""Utility functions for handling news entries and merging them into CHANGELOG.md.""" + +import pathlib +import re + + +def _get_sub_category(content): + """Extracts the sub-category in parentheses from the entry content.""" + match = re.match(r"^(?:\*|-)\s*\(([^)]+)\)", content) + if match: + return match.group(1).lower() + return "" + + +def is_news_file(path): + """Checks if a file path is a valid news file.""" + path = pathlib.Path(path) + if not path.is_file(): + return False + if path.suffix != ".md": + return False + parts = path.name.split(".") + if len(parts) < 3: + return False + return True + + +def _get_news_files(news_dir): + """Returns a list of news files matching the ..md pattern.""" + news_path = pathlib.Path(news_dir) + if not news_path.exists(): + return [] + + return [p for p in news_path.iterdir() if is_news_file(p)] + + +def _parse_new_files(news_files): + """Parses news files and groups them by category.""" + entries = {} + for p in news_files: + if not is_news_file(p): + continue + parts = p.name.split(".") + category = parts[1].lower() + + content = p.read_text(encoding="utf-8").strip() + + if not content: + continue + + # Format as list item if not already + if not (content.startswith("* ") or content.startswith("- ")): + content = f"* {content}" + + if category not in entries: + entries[category] = [] + entries[category].append(content) + + return entries + + +def generate_release_block(version, release_date, news_entries): + """Generates the markdown block for the release.""" + header_version = version.replace(".", "-") + lines = [ + f"{{#v{header_version}}}", + f"## [{version}] - {release_date}", + "", + f"[{version}]: https://github.com/bazel-contrib/rules_python/releases/tag/{version}", + "", + ] + + # Standard categories in preferred order + category_order = ["removed", "changed", "fixed", "added"] + # Add any other categories found + for cat in news_entries: + if cat not in category_order: + category_order.append(cat) + + has_entries = False + for cat in category_order: + if cat in news_entries and news_entries[cat]: + has_entries = True + lines.append(f"{{#v{header_version}-{cat}}}") + lines.append(f"### {cat.capitalize()}") + + # Sort entries by sub-category, then by content + sorted_entries = sorted( + news_entries[cat], key=lambda e: (_get_sub_category(e), e) + ) + + for entry in sorted_entries: + lines.append(entry) + lines.append("") + + if not has_entries: + lines.append("No notable changes.") + lines.append("") + + return "\n".join(lines) + + +def _parse_simple_version(ver_str): + """Parses a version string (X-Y-Z or X.Y.Z) into a tuple of ints.""" + normalized = ver_str.replace("-", ".") + return tuple(int(x) for x in normalized.split(".")) + + +def _find_insertion_point(changelog_content, new_version): + """Finds the character index to insert the new version block. + + It should be inserted before the first version in the changelog that is + smaller than new_version. + """ + # Find all version anchors: {#vX-Y-Z} + matches = list(re.finditer(r"\{#v(?P\d+-\d+-\d+)\}", changelog_content)) + + parsed_new_ver = _parse_simple_version(new_version) + + for m in matches: + ver_str = m.group("ver") + parsed_ver = _parse_simple_version(ver_str) + if parsed_ver < parsed_new_ver: + return m.start() + + raise ValueError( + f"Could not find a version in CHANGELOG.md smaller than {new_version} to insert before." + ) + + +def _add_news_to_changelog(input_path, output_path, version, entries, release_date): + """Adds or merges news entries into CHANGELOG.md.""" + input_path = pathlib.Path(input_path) + output_path = pathlib.Path(output_path) + changelog_content = input_path.read_text(encoding="utf-8") + + if version == "unreleased": + header_version = "unreleased" + version_anchor = "{#unreleased}" + category_anchor_fmt = "{{#unreleased-{cat}}}" + category_anchor_pattern = r"\{#unreleased-(?P[a-z]+)\}" + else: + header_version = version.replace(".", "-") + version_anchor = f"{{#v{header_version}}}" + category_anchor_fmt = "{{#v" + header_version + "-{cat}}}" + category_anchor_pattern = ( + r"\{#v" + re.escape(header_version) + r"-(?P[a-z]+)\}" + ) + + version_exists = version_anchor in changelog_content + + if version_exists: + if not entries and version != "unreleased": + print( + f"Version {version} already exists and no news entries found" + " to merge. Doing nothing." + ) + output_path.write_text(changelog_content, encoding="utf-8") + return + + print(f"Version {version} already exists in changelog. Merging news entries...") + # Extract the existing version block + pattern = ( + r"(?P" + + re.escape(version_anchor) + + r")(?P.*?)(?=\n\s*\{#v\d+-\d+-\d+\}|\Z)" + ) + match = re.search(pattern, changelog_content, re.DOTALL) + if not match: + raise RuntimeError( + f"Could not find content for existing version {version} in CHANGELOG.md" + ) + + content_block = match.group("content") + + # Strip the "Unreleased changes..." sentence for Unreleased preview + if version == "unreleased": + content_block = re.sub( + r"Unreleased\s+changes\s+are\s+tracked\s+as\s+individual\s+files\s+in\s+the\s+\[news/\]\(\./news\)\s+directory,\s+or\s+view\s+the\s+\[latest\s+generated\s+changelog\]\(https://rules-python\.readthedocs\.io/en/latest/changelog\.html\)\.\s*\n*", + "", + content_block, + ) + + # Parse existing categories + match_cat = re.search(category_anchor_pattern, content_block) + if match_cat: + header_end_idx = match_cat.start() + header_str = content_block[:header_end_idx] + categories_str = content_block[header_end_idx:] + else: + header_str = content_block + categories_str = "" + + existing_entries = {} + if categories_str: + cat_matches = list(re.finditer(category_anchor_pattern, categories_str)) + for i, m in enumerate(cat_matches): + cat = m.group("cat") + start_idx = m.end() + end_idx = ( + cat_matches[i + 1].start() + if i + 1 < len(cat_matches) + else len(categories_str) + ) + cat_content = categories_str[start_idx:end_idx].strip() + + lines = cat_content.splitlines() + cat_entries = [] + current_entry = [] + for line in lines: + if not line.strip() or line.strip().startswith("### "): + continue + if line.startswith("* ") or line.startswith("- "): + if current_entry: + cat_entries.append("\n".join(current_entry)) + current_entry = [line] + else: + if current_entry: + current_entry.append(line) + if current_entry: + cat_entries.append("\n".join(current_entry)) + existing_entries[cat] = cat_entries + + # Merge news entries + merged_entries = dict(existing_entries) + for cat, cat_entries in entries.items(): + if cat not in merged_entries: + merged_entries[cat] = [] + merged_entries[cat].extend(cat_entries) + + # Reconstruct categories + reconstructed_lines = [] + category_order = ["removed", "changed", "fixed", "added"] + for cat in merged_entries: + if cat not in category_order: + category_order.append(cat) + + for cat in category_order: + if cat in merged_entries and merged_entries[cat]: + reconstructed_lines.append(category_anchor_fmt.format(cat=cat)) + reconstructed_lines.append(f"### {cat.capitalize()}") + + sorted_entries = sorted( + merged_entries[cat], key=lambda e: (_get_sub_category(e), e) + ) + + for entry in sorted_entries: + reconstructed_lines.append(entry) + reconstructed_lines.append("") + + new_categories_str = "\n".join(reconstructed_lines) + new_release_block = ( + header_str.rstrip() + "\n\n" + new_categories_str.strip() + "\n" + ) + if version == "unreleased" and not new_categories_str.strip(): + new_release_block = ( + header_str.rstrip() + "\n\nNo notable unreleased changes.\n" + ) + + # Replace in changelog_content + new_content = re.sub( + pattern, + r"\g\n" + new_release_block.strip() + "\n", + changelog_content, + count=1, + flags=re.DOTALL, + ) + output_path.write_text(new_content, encoding="utf-8") + + else: + print( + f"Version {version} does not exist in changelog. Creating new" + " release section from news entries..." + ) + new_release_block = generate_release_block(version, release_date, entries) + + # Find insertion point + insertion_point = _find_insertion_point(changelog_content, version) + + new_content = ( + changelog_content[:insertion_point] + + new_release_block + + "\n\n" + + changelog_content[insertion_point:] + ) + output_path.write_text(new_content, encoding="utf-8") + + +def merge_new_into_changelog( + changelog_path, + output_path, + news_dir, + version, + release_date, + delete_news=False, + news_files=None, +): + """Merges news entries from news_dir into changelog_path and writes to output_path.""" + if news_files is None: + news_files = _get_news_files(news_dir) + else: + news_files = [pathlib.Path(f) for f in news_files] + entries = _parse_new_files(news_files) + _add_news_to_changelog( + input_path=changelog_path, + output_path=output_path, + version=version, + entries=entries, + release_date=release_date, + ) + if delete_news: + for p in news_files: + if p.exists(): + p.unlink() + if news_files: + print(f"Removed {len(news_files)} processed news files.") + + +def update_changelog( + version, + release_date, + changelog_path="CHANGELOG.md", + output_path=None, + news_dir="news", + delete_news=True, + news_files=None, +): + """Performs the version replacements in CHANGELOG.md.""" + if output_path is None: + output_path = changelog_path + merge_new_into_changelog( + changelog_path=changelog_path, + output_path=output_path, + news_dir=news_dir, + version=version, + release_date=release_date, + delete_news=delete_news, + news_files=news_files, + ) diff --git a/tools/private/release/complete_prepare.py b/tools/private/release/complete_prepare.py new file mode 100644 index 0000000000..8a60cbf0f3 --- /dev/null +++ b/tools/private/release/complete_prepare.py @@ -0,0 +1,81 @@ +"""Subcommand to mark preparation task as complete.""" + +import re + +from tools.private.release.gh import GitHub +from tools.private.release.release_issue import update_task_in_body + + +class CompletePrepare: + """Class to mark preparation task as complete.""" + + def __init__(self, args, gh: GitHub): + self.args = args + self.gh = gh + + def run(self) -> int: + """Executes the complete-prepare subcommand (Phase 2 PR merged).""" + args = self.args + print(f"Completing preparation for PR #{args.pr}...") + + pr_info = self.gh.get_pr_info(args.pr) + if not pr_info or pr_info.get("state") != "MERGED": + state = pr_info.get("state", "UNKNOWN") + print(f"Error: PR #{args.pr} is not merged yet (state: {state}).") + return 1 + + # Resolve issue number from PR body + pr_body = pr_info.get("body") or "" + match = re.search(r"Work towards #(\d+)", pr_body) + if not match: + match = re.search(r"#(\d+)", pr_body) + if not match: + print( + f"Error: Could not determine tracking issue number from PR" + f" #{args.pr} body: {pr_body}" + ) + return 1 + + issue_num = int(match.group(1)) + print(f"Resolved tracking issue #{issue_num} from PR #{args.pr} body.") + + commit_sha = pr_info["mergeCommit"]["oid"] + short_commit = commit_sha[:8] + print( + f"PR #{args.pr} merged at commit {commit_sha}. Updating tracking issue..." + ) + + # Update checklist: mark Prepare Release as done (checked) and set SUCCESS + body = self.gh.get_issue_body(issue_num) + metadata = { + "status": "done", + "pr": f"#{args.pr}", + "commit": short_commit, + } + updated_body = update_task_in_body( + body, "Prepare Release", checked=True, metadata=metadata + ) + self.gh.update_issue_body(issue_num, updated_body) + print("Prepare Release task marked complete successfully!") + return 0 + + @classmethod + def add_parser(cls, subparsers): + """Adds parser for complete-prepare subcommand.""" + parser = subparsers.add_parser( + "complete-prepare", + help="Mark the Prepare Release task as complete in the tracking issue.", + ) + parser.add_argument( + "--pr", + type=int, + required=True, + help="The merged preparation PR number.", + ) + parser.set_defaults(command=cls.run_from_args) + + @classmethod + def run_from_args(cls, args): + """Instantiates and runs the command from parsed args.""" + gh = GitHub() + return cls(args, gh).run() diff --git a/tools/private/release/complete_sync_changelog.py b/tools/private/release/complete_sync_changelog.py new file mode 100644 index 0000000000..9c8d9028f0 --- /dev/null +++ b/tools/private/release/complete_sync_changelog.py @@ -0,0 +1,97 @@ +"""Subcommand to mark sync changelog tasks as complete.""" + +import re + +from tools.private.release.gh import GitHub +from tools.private.release.release_issue import ( + parse_checklist_state, + update_task_in_body, +) + + +class CompleteSyncChangelog: + """Class to mark sync changelog tasks as complete.""" + + def __init__(self, args, gh: GitHub): + self.args = args + self.gh = gh + + def run(self) -> int: + """Executes the complete-sync-changelog subcommand.""" + args = self.args + print(f"Completing sync changelog for PR #{args.pr}...") + + pr_info = self.gh.get_pr_info(args.pr) + if not pr_info or pr_info.get("state") != "MERGED": + state = pr_info.get("state", "UNKNOWN") + print(f"Error: PR #{args.pr} is not merged yet (state: {state}).") + return 1 + + # Resolve issue number from PR body using Release-Tracking-Issue: # + pr_body = pr_info.get("body") or "" + match = re.search(r"Release-Tracking-Issue:\s*#(\d+)", pr_body) + if not match: + print( + f"Error: Could not find 'Release-Tracking-Issue: #' in" + f" PR #{args.pr} body: {pr_body}" + ) + return 1 + + issue_num = int(match.group(1)) + print(f"Resolved tracking issue #{issue_num} from PR #{args.pr} body.") + + commit_sha = pr_info["mergeCommit"]["oid"] + short_commit = commit_sha[:8] + print( + f"PR #{args.pr} merged at commit {commit_sha}. Updating tracking issue..." + ) + + # Update checklist: mark all Sync Changelog tasks pointing to this PR as done + body = self.gh.get_issue_body(issue_num) + state = parse_checklist_state(body) + sync_changelogs = state.get("sync_changelogs", {}) + + updated_any = False + for pr_num, task in sync_changelogs.items(): + # Check if this task points to our merged PR + task_pr = task.metadata.get("pr") + if task_pr == f"#{args.pr}": + print(f"Marking task '{task.name}' as complete...") + metadata = { + "status": "done", + "pr": f"#{args.pr}", + "commit": short_commit, + } + body = update_task_in_body( + body, task.name, checked=True, metadata=metadata + ) + updated_any = True + + if not updated_any: + print(f"Warning: No 'Sync Changelog' tasks found pointing to PR #{args.pr}") + return 0 + + self.gh.update_issue_body(issue_num, body) + print("Sync changelog tasks marked complete successfully!") + return 0 + + @classmethod + def add_parser(cls, subparsers): + """Adds parser for complete-sync-changelog subcommand.""" + parser = subparsers.add_parser( + "complete-sync-changelog", + help="Mark the Sync Changelog tasks as complete in the tracking issue.", + ) + parser.add_argument( + "--pr", + type=int, + required=True, + help="The merged sync changelog PR number.", + ) + parser.set_defaults(command=cls.run_from_args) + + @classmethod + def run_from_args(cls, args): + """Instantiates and runs the command from parsed args.""" + gh = GitHub() + return cls(args, gh).run() diff --git a/tools/private/release/create_rc.py b/tools/private/release/create_rc.py new file mode 100644 index 0000000000..2e61e7d80a --- /dev/null +++ b/tools/private/release/create_rc.py @@ -0,0 +1,213 @@ +"""Subcommand to tag and push the next release candidate.""" + +import traceback +from argparse import Namespace + +from tools.private.release.gh import GH_REACTION_THUMBS_DOWN, GitHub +from tools.private.release.git import Git +from tools.private.release.process_backports import ProcessBackports +from tools.private.release.release_issue import ( + RELEASE_TITLE_RE, + add_rc_task_to_body, + parse_backports, + parse_checklist_state, + update_task_in_body, +) +from tools.private.release.utils import ( + REPO_URL, + get_latest_rc_tag, +) + + +class CreateRc: + """Class to tag and push the next release candidate.""" + + def __init__(self, args, git: Git, gh: GitHub): + self.args = args + self.git = git + self.gh = gh + + def run(self) -> int: + """Executes the create-rc subcommand.""" + args = self.args + exit_code = 0 + try: + exit_code = self._run_internal() + except Exception as e: + print(f"Unexpected error: {e}") + traceback.print_exc() + exit_code = 1 + + if exit_code != 0 and args.triggering_comment: + print(f"Reacting with thumbs-down to comment {args.triggering_comment}...") + try: + self.gh.add_comment_reaction( + args.triggering_comment, GH_REACTION_THUMBS_DOWN + ) + except Exception as e: + print(f"Failed to add reaction to comment: {e}") + + return exit_code + + def _run_internal(self) -> int: + """Internal implementation of create-rc.""" + args = self.args + + # Try to process pending backports first + print("Processing pending backports before creating RC...") + backport_args = Namespace( + issue=args.issue, + remote=args.remote, + add=None, + triggering_comment=None, + dry_run=False, + ) + pb = ProcessBackports(backport_args, self.git, self.gh) + backports_exit_code = pb.run() + if backports_exit_code != 0: + print("Error: Processing backports failed. Aborting RC creation.") + return backports_exit_code + + body = self.gh.get_issue_body(args.issue) + state = parse_checklist_state(body) + + if ( + state["prepare_release"].status != "done" + or state["create_branch"].status != "done" + ): + print( + "Error: Preconditions not met (release must be prepared and" + " branch created)." + ) + return 1 + + # Gating: RC tagging is blocked if any backport is unchecked OR does not have status=done + backports = parse_backports(body) + conflicting_or_pending = [ + b + for b in backports + if (b.checked and b.status != "done") + or (not b.checked and b.status != "ignore") + ] + if conflicting_or_pending: + print( + f"Gating RC tagging: {len(conflicting_or_pending)} backports" + " are still unfinished, failed, or in conflict." + ) + return 1 + + # Resolve version and branch + issue_title = self.gh.get_issue_title(args.issue) + version_match = RELEASE_TITLE_RE.search(issue_title) + if not version_match: + print(f"Error: Could not parse version from issue title: {issue_title}") + return 1 + + version = version_match.group(1) + branch_version = ".".join(version.split(".")[:2]) + branch_name = f"release/{branch_version}" + + # Determine next RC tag + self.git.fetch(args.remote) + self.git.fetch(args.remote, tags=True, force=True) + latest_rc = get_latest_rc_tag(version, remote=args.remote) + + if not latest_rc: + next_rc_num = 0 + next_rc = f"{version}-rc0" + else: + rc_num = int(latest_rc.split("-rc")[-1]) + next_rc_num = rc_num + 1 + next_rc = f"{version}-rc{next_rc_num}" + + # Precheck: next RC number must exist and be unchecked in the checklist + rc_tags = state.get("rc_tags", {}) + if next_rc_num not in rc_tags: + print(f"Task 'Tag RC{next_rc_num}' not found in checklist. Adding it...") + body = add_rc_task_to_body(body, next_rc_num) + self.gh.update_issue_body(args.issue, body) + else: + target_rc_task = rc_tags[next_rc_num] + if target_rc_task.checked or target_rc_task.status == "done": + print( + f"Error: Task 'Tag RC{next_rc_num}' is already marked done in" + " the checklist." + ) + return 1 + + target_ref = f"{args.remote}/{branch_name}" + commit_sha = self.git.get_commit_sha(target_ref) + + print(f"Tagging and pushing next RC: {next_rc}...") + self.git.tag(next_rc, target_ref) + self.git.push(args.remote, next_rc) + + import os + + if "GITHUB_OUTPUT" in os.environ: + with open(os.environ["GITHUB_OUTPUT"], "a") as f: + f.write(f"tag_name={next_rc}\n") + + # Check off the appropriate "Tag RC{N}" task in the checklist + print(f"Checking off Tag RC{next_rc_num} task...") + metadata = {"status": "done", "tag": next_rc, "commit": commit_sha[:8]} + task_name = f"Tag RC{next_rc_num}" + updated_body = update_task_in_body( + body, task_name, checked=True, metadata=metadata + ) + self.gh.update_issue_body(args.issue, updated_body) + + tag_url = f"{REPO_URL}/releases/tag/{next_rc}" + bcr_entry_url = f"https://registry.bazel.build/modules/rules_python/{next_rc}" + bcr_search_url = f"https://github.com/bazelbuild/bazel-central-registry/pulls?q=is%3Apr+rules_python+{next_rc}" + if run_id := os.environ.get("GITHUB_RUN_ID"): + release_workflow_url = f"{REPO_URL}/actions/runs/{run_id}" + else: + release_workflow_url = ( + f"{REPO_URL}/actions/workflows/release_create_rc.yaml" + ) + branch_url = f"{REPO_URL}/tree/{branch_name}" + comment_body = f"""**New Release Candidate Tagged!** šŸšŸŒæ + +Release Candidate **{next_rc}** has been successfully generated and tagged on branch [`{branch_name}`]({branch_url}). + +- [Github Release {next_rc}]({tag_url}) +- [BCR Entry {next_rc}]({bcr_entry_url}) +- [BCR PRs]({bcr_search_url}) +- [Release workflow status]({release_workflow_url})""" + self.gh.post_issue_comment(args.issue, comment_body) + print("RC creation completed successfully!") + return 0 + + @classmethod + def add_parser(cls, subparsers): + """Adds parser for create-rc subcommand.""" + parser = subparsers.add_parser( + "create-rc", + help="Tags the next RC on the release branch if no backports remain.", + ) + parser.add_argument( + "--issue", + type=int, + required=True, + help="The tracking issue number (required).", + ) + parser.add_argument( + "--remote", + type=str, + required=True, + help="The git remote to push the RC tag to (required).", + ) + parser.add_argument( + "--triggering-comment", + type=int, + help="The ID of the comment that triggered this run (optional).", + ) + parser.set_defaults(command=cls.run_from_args) + + @classmethod + def run_from_args(cls, args): + """Instantiates and runs the command from parsed args.""" + git = Git(".") + gh = GitHub() + return cls(args, git, gh).run() diff --git a/tools/private/release/create_release_branch.py b/tools/private/release/create_release_branch.py new file mode 100644 index 0000000000..7a9d7eec37 --- /dev/null +++ b/tools/private/release/create_release_branch.py @@ -0,0 +1,131 @@ +"""Subcommand to create a release branch from a merged PR commit.""" + +from tools.private.release.gh import GitHub +from tools.private.release.git import Git +from tools.private.release.release_issue import ( + RELEASE_TITLE_RE, + parse_checklist_state, + update_task_in_body, +) +from tools.private.release.utils import REPO_URL + + +class CreateReleaseBranch: + """Class to create a release branch from a merged PR commit.""" + + def __init__(self, args, git: Git, gh: GitHub): + self.args = args + self.git = git + self.gh = gh + + def run(self) -> int: + """Executes the create-release-branch subcommand.""" + args = self.args + print(f"Evaluating branch creation for tracking issue #{args.issue}...") + body = self.gh.get_issue_body(args.issue) + state = parse_checklist_state(body) + + if ( + state["prepare_release"].status != "done" + or not state["prepare_release"].commit + ): + print( + "Error: Prepare Release task is not marked 'done' with a valid" + " commit SHA." + ) + return 1 + + if state["create_branch"].checked: + print("Release branch has already been created and checked. Skipping.") + return 0 + + # Extract version from issue title + issue_title = self.gh.get_issue_title(args.issue) + version_match = RELEASE_TITLE_RE.search(issue_title) + if not version_match: + print(f"Error: Could not parse version from issue title: {issue_title}") + return 1 + + version = version_match.group(1) + branch_version = ".".join(version.split(".")[:2]) + branch_name = f"release/{branch_version}" + + commit_sha = state["prepare_release"].commit + print(f"Cutting branch {branch_name} from commit {commit_sha}...") + + # Create and push branch without affecting local checkout + self.git.fetch(args.remote) + + if self.git.remote_branch_exists(args.remote, branch_name): + remote_ref = f"{args.remote}/{branch_name}" + remote_sha = self.git.get_commit_sha(remote_ref) + if remote_sha == commit_sha: + print( + f"Branch {branch_name} already exists on {args.remote} and" + f" points to {commit_sha}. Skipping push." + ) + elif self.git.is_ancestor(remote_ref, commit_sha): + print( + f"Branch {branch_name} exists on {args.remote} but can be" + f" fast-forwarded to {commit_sha}. Pushing..." + ) + ref_spec = f"{commit_sha}:refs/heads/{branch_name}" + self.git.push(args.remote, ref_spec) + else: + print( + f"Error: Branch {branch_name} already exists on" + f" {args.remote} at {remote_sha[:8]}, which is not an" + f" ancestor of {commit_sha[:8]}. Cannot fast-forward." + ) + return 1 + else: + print(f"Branch {branch_name} does not exist on {args.remote}. Pushing...") + ref_spec = f"{commit_sha}:refs/heads/{branch_name}" + self.git.push(args.remote, ref_spec) + print( + f"Successfully pushed branch {branch_name} pointing to" + f" {commit_sha} to {args.remote}" + ) + + # Update tracking issue checklist + print("Updating tracking issue checklist...") + branch_url = f"{REPO_URL}/tree/{branch_name}" + metadata = { + "status": "done", + "branch_url": branch_url, + "commit": commit_sha[:8], + } + updated_body = update_task_in_body( + body, "Create Release branch", checked=True, metadata=metadata + ) + self.gh.update_issue_body(args.issue, updated_body) + print("Create Release branch task marked complete successfully!") + return 0 + + @classmethod + def add_parser(cls, subparsers): + """Adds parser for create-release-branch subcommand.""" + parser = subparsers.add_parser( + "create-release-branch", + help="Create the release branch pointing to the merged PR commit.", + ) + parser.add_argument( + "--issue", + type=int, + required=True, + help="The tracking issue number (required).", + ) + parser.add_argument( + "--remote", + type=str, + required=True, + help="The git remote to create the branch on (required).", + ) + parser.set_defaults(command=cls.run_from_args) + + @classmethod + def run_from_args(cls, args): + """Instantiates and runs the command from parsed args.""" + git = Git(".") + gh = GitHub() + return cls(args, git, gh).run() diff --git a/tools/private/release/create_release_issue.py b/tools/private/release/create_release_issue.py new file mode 100644 index 0000000000..2d00d7369a --- /dev/null +++ b/tools/private/release/create_release_issue.py @@ -0,0 +1,59 @@ +"""Subcommand to create a release tracking issue.""" + +import pathlib + +from tools.private.release.gh import GitHub +from tools.private.release.utils import determine_next_version, semver_type + + +class CreateReleaseIssue: + """Class to create a release tracking issue.""" + + def __init__(self, args, gh: GitHub): + self.args = args + self.gh = gh + + def run(self) -> int: + """Executes the create-release-issue subcommand.""" + version = self.args.version + if version is None: + version = determine_next_version() + + # Concurrency check + open_issues = self.gh.get_open_tracking_issues() + if open_issues: + print("Error: A release is already in progress. Active tracking issues:") + for issue in open_issues: + print(f"- {issue['title']}: {issue['url']}") + return 1 + + template_path = pathlib.Path( + ".github/ISSUE_TEMPLATE/release_tracking_template.md" + ) + if not template_path.exists(): + raise FileNotFoundError(f"Template file not found at {template_path}") + template_content = template_path.read_text(encoding="utf-8") + + issue_num = self.gh.create_tracking_issue(version, template_content) + print(f"Created tracking issue #{issue_num} for v{version}") + return 0 + + @classmethod + def add_parser(cls, subparsers): + """Adds parser for create-release-issue subcommand.""" + parser = subparsers.add_parser( + "create-release-issue", + help="Search for open releases and create a new tracking issue.", + ) + parser.add_argument( + "--version", + type=semver_type, + help="The release version (e.g., 0.38.0). If not provided, determined automatically.", + ) + parser.set_defaults(command=cls.run_from_args) + + @classmethod + def run_from_args(cls, args): + """Instantiates and runs the command from parsed args.""" + gh = GitHub() + return cls(args, gh).run() diff --git a/tools/private/release/determine_next_version.py b/tools/private/release/determine_next_version.py new file mode 100644 index 0000000000..541daddfe7 --- /dev/null +++ b/tools/private/release/determine_next_version.py @@ -0,0 +1,30 @@ +"""Subcommand to determine the next version.""" + +from tools.private.release.utils import determine_next_version + + +class DetermineNextVersion: + """Class to determine the next version.""" + + def __init__(self, args, git=None, gh=None): + self.args = args + + def run(self) -> int: + """Executes the determine-next-version subcommand.""" + version = determine_next_version() + print(version) + return 0 + + @classmethod + def add_parser(cls, subparsers): + """Adds parser for determine-next-version subcommand.""" + parser = subparsers.add_parser( + "determine-next-version", + help="Determine the next version and print it, without making any changes.", + ) + parser.set_defaults(command=cls.run_from_args) + + @classmethod + def run_from_args(cls, args): + """Instantiates and runs the command from parsed args.""" + return cls(args).run() diff --git a/tools/private/release/gh.py b/tools/private/release/gh.py new file mode 100644 index 0000000000..92cc3ca5c4 --- /dev/null +++ b/tools/private/release/gh.py @@ -0,0 +1,480 @@ +"""GitHub CLI helper functions for the release tool.""" + +import json +import os +import re +import tempfile + +from tools.private.release.release_issue import BackportTask +from tools.private.release.shell import run_cmd + +# GitHub reaction types +# See: https://docs.github.com/en/rest/reactions/reactions?apiVersion=2022-11-28#about-reactions +GH_REACTION_THUMBS_UP = "+1" +GH_REACTION_THUMBS_DOWN = "-1" +GH_REACTION_LAUGH = "laugh" +GH_REACTION_CONFUSED = "confused" +GH_REACTION_HEART = "heart" +GH_REACTION_HOORAY = "hooray" +GH_REACTION_ROCKET = "rocket" +GH_REACTION_EYES = "eyes" + + +class MultipleTrackingIssuesError(ValueError): + """Raised when multiple open tracking issues are found for a version.""" + + pass + + +class NoTrackingIssueError(ValueError): + """Raised when no open tracking issue is found for a version.""" + + pass + + +class GitHub: + """GitHub CLI helper class for the release tool.""" + + def __init__(self, repo: str = "bazel-contrib/rules_python"): + """Initializes the GitHub helper. + + Args: + repo: The GitHub repository to operate on. + """ + self.repo = repo + self.label = "type: release" + + def _run_gh( + self, *args: str, check: bool = True, capture_output: bool = True + ) -> str | None: + """Runs a 'gh' command. + + Args: + *args: Arguments for 'gh' (excluding 'gh'). + check: If True, raises CalledProcessError on failure. + capture_output: If True, captures and returns stdout. + + Returns: + The stdout of the command, stripped, or None. + """ + return run_cmd("gh", *args, check=check, capture_output=capture_output) + + def _gh_issue( + self, *args: str, check: bool = True, capture_output: bool = True + ) -> str | None: + """Runs a 'gh issue' command.""" + return self._run_gh( + "issue", + *args, + f"--repo={self.repo}", + check=check, + capture_output=capture_output, + ) + + def _gh_pr( + self, *args: str, check: bool = True, capture_output: bool = True + ) -> str | None: + """Runs a 'gh pr' command.""" + return self._run_gh( + "pr", + *args, + f"--repo={self.repo}", + check=check, + capture_output=capture_output, + ) + + def list_issues( + self, + *, + fields: str, + label: str | None = None, + state: str | None = None, + search: str | None = None, + ) -> list[dict]: + """Helper to list issues using gh CLI. + + Args: + fields: Comma-separated list of fields to return. + label: Filter by label. + state: Filter by state (open, closed, all). + search: Search query. + + Returns: + A list of dictionaries representing the issues. + """ + cmd = ["list"] + if label: + cmd.append(f"--label={label}") + if state: + cmd.append(f"--state={state}") + if search: + cmd.append(f"--search={search}") + cmd.append(f"--json={fields}") + + output = self._gh_issue(*cmd) + return json.loads(output) if output else [] + + def get_open_tracking_issues(self, version: str | None = None) -> list[dict]: + """Returns a list of open tracking issues with the 'type: release' label. + + Args: + version: Optional version to filter by. + + Returns: + A list of open tracking issues. + """ + search = f'"Release {version}" in:title' if version else None + return self.list_issues( + label=self.label, + state="open", + search=search, + fields="number,title,url", + ) + + def get_release_tracking_issue(self, version: str) -> int: + """Resolves the tracking issue number for a given version. + + Searches for an open issue with label 'type: release' and 'Release + ' in the title. + + Args: + version: The version to find the tracking issue for. + + Returns: + The tracking issue number. + + Raises: + NoTrackingIssueError: If no open tracking issue is found. + MultipleTrackingIssuesError: If multiple open tracking issues are + found. + """ + matching_issues = self.get_open_tracking_issues(version) + + exact_matches = [] + for issue in matching_issues: + if issue["title"] == f"Release {version}": + exact_matches.append(issue) + + if not exact_matches: + raise NoTrackingIssueError( + f"No open tracking issue found matching 'Release {version}' " + f"in repo {self.repo} with label '{self.label}'" + ) + if len(exact_matches) > 1: + urls = [issue["url"] for issue in exact_matches] + raise MultipleTrackingIssuesError( + f"Multiple open tracking issues found for version {version} " + f"in repo {self.repo} with label '{self.label}':\n" + "\n".join(urls) + ) + + return exact_matches[0]["number"] + + def create_tracking_issue(self, version: str, template_content: str) -> int: + """Creates a new release tracking issue from template content. + + Strips YAML frontmatter if present. + + Args: + version: The version to create the tracking issue for. + template_content: The markdown template content for the issue body. + + Returns: + The created issue number. + """ + # Strip YAML frontmatter if present + issue_body = template_content + if template_content.startswith("---"): + parts = template_content.split("---", 2) + if len(parts) >= 3: + issue_body = parts[2].strip() + + # Write body to a secure temporary file to pass to the CLI + with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) as f: + f.write(issue_body) + temp_path = f.name + + try: + output = self._gh_issue( + "create", + f"--title=Release {version}", + f"--label={self.label}", + f"--body-file={temp_path}", + ) + if not output: + raise RuntimeError("Failed to get issue URL from gh issue create") + issue_url = output.strip() + issue_num = int(issue_url.split("/")[-1]) + return issue_num + finally: + if os.path.exists(temp_path): + os.unlink(temp_path) + + def get_issue_body(self, issue_num: int) -> str: + """Fetches the body of a specific issue. + + Args: + issue_num: The issue number. + + Returns: + The issue body markdown. + """ + output = self._gh_issue( + "view", + str(issue_num), + "--json=body", + "--jq=.body", + ) + return output if output else "" + + def get_issue_title(self, issue_num: int) -> str: + """Fetches the title of a specific issue. + + Args: + issue_num: The issue number. + + Returns: + The issue title. + """ + output = self._gh_issue( + "view", + str(issue_num), + "--json=title", + ) + return json.loads(output)["title"] if output else "" + + def update_issue_body(self, issue_num: int, body: str) -> None: + """Updates the body of a specific issue. + + Args: + issue_num: The issue number. + body: The new issue body markdown. + """ + with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) as f: + f.write(body) + temp_path = f.name + try: + self._gh_issue( + "edit", + str(issue_num), + f"--body-file={temp_path}", + capture_output=False, + ) + finally: + if os.path.exists(temp_path): + os.unlink(temp_path) + + def create_pr( + self, + title: str, + body: str, + base: str = "main", + labels: list[str] | None = None, + ) -> str: + """Creates a pull request. + + Args: + title: The title of the PR. + body: The body of the PR. + base: The base branch to merge into (default: 'main'). + labels: Optional list of labels to add to the PR. + + Returns: + The URL of the created PR. + """ + cmd = [ + "create", + f"--title={title}", + f"--body={body}", + f"--base={base}", + ] + if labels: + for label in labels: + cmd.append(f"--label={label}") + output = self._gh_pr(*cmd) + return output if output else "" + + def enable_auto_merge(self, pr_num: int, method: str = "squash") -> None: + """Enables auto-merge for a PR. + + Args: + pr_num: The PR number. + method: The merge method ('squash', 'rebase', or 'merge'). + """ + cmd = ["merge", str(pr_num), "--auto"] + if method == "squash": + cmd.append("--squash") + elif method == "rebase": + cmd.append("--rebase") + elif method == "merge": + cmd.append("--merge") + self._gh_pr(*cmd, capture_output=False) + + def get_open_pr(self, branch_name: str) -> dict | None: + """Returns PR info if an open PR exists for the given branch. + + Args: + branch_name: The head branch name of the PR. + + Returns: + A dictionary with 'number' and 'url' of the PR, or None. + """ + output = self._gh_pr( + "list", + f"--head={branch_name}", + "--state=open", + "--json=number,url", + ) + prs = json.loads(output) if output else [] + return prs[0] if prs else None + + def get_pr_info(self, pr_num: int) -> dict: + """Gets information about a PR. + + Includes state, merge commit, body, and draft status. + + Args: + pr_num: The PR number. + + Returns: + A dictionary containing the PR info. + """ + output = self._gh_pr( + "view", + str(pr_num), + "--json=state,mergeCommit,body,isDraft", + ) + return json.loads(output) if output else {} + + def get_pr_comments(self, pr_num: int) -> list[dict]: + """Gets comments for a PR. + + Args: + pr_num: The PR number. + + Returns: + A list of comments. + """ + output = self._gh_pr( + "view", + str(pr_num), + "--json=comments", + ) + return json.loads(output).get("comments") or [] + + def resolve_pr_number(self, pr_ref: str) -> int: + """Resolves a PR reference (number, #number, URL) to a PR number. + + Args: + pr_ref: The PR reference string. + + Returns: + The resolved PR number. + + Raises: + ValueError: If the reference cannot be resolved. + """ + # 1. Try number (e.g. "123" or "#123") + clean_ref = pr_ref.lstrip("#") + if clean_ref.isdigit(): + return int(clean_ref) + + # 2. Try URL (starts with http) + if pr_ref.startswith("http"): + # Try to extract PR number from URL using regex + # Pattern matches: github.com//pull/ followed by /, ?, or EOF + pattern = rf"github\.com/{re.escape(self.repo)}/pull/(\d+)(/|\?|\Z)" + match = re.search(pattern, pr_ref, re.IGNORECASE) + if match: + return int(match.group(1)) + raise ValueError( + f"URL is not for the configured repository ({self.repo}): {pr_ref}" + ) + + raise ValueError(f"Could not resolve PR reference: {pr_ref}") + + def post_issue_comment(self, issue_num: int, comment_body: str) -> None: + """Posts a comment to a specific issue. + + Args: + issue_num: The issue number. + comment_body: The comment body markdown. + """ + self._gh_issue( + "comment", + str(issue_num), + f"--body={comment_body}", + capture_output=False, + ) + + def add_comment_reaction(self, comment_id: int, reaction: str) -> None: + """Adds a reaction to a comment. + + Args: + comment_id: The ID of the comment. + reaction: The reaction type (e.g. '+1', '-1', 'eyes', etc). + """ + path = f"/repos/{self.repo}/issues/comments/{comment_id}/reactions" + self._run_gh( + "api", + "--method", + "POST", + "-H", + "Accept: application/vnd.github+json", + "-H", + "X-GitHub-Api-Version: 2022-11-28", + path, + "-f", + f"content={reaction}", + capture_output=False, + ) + + def get_merge_commits_for_prs( + self, pending_items: list[BackportTask] + ) -> list[BackportTask]: + """Resolves PR references in pending backports to their merge commit SHAs. + + Updates item.status based on PR state if it cannot be resolved. + + Args: + pending_items: A list of BackportTask items to resolve. + + Returns: + The list of resolved BackportTask items. + """ + resolved_items = [] + for item in pending_items: + pr_num = int(item.pr_ref.lstrip("#")) + print(f"Resolving PR #{pr_num} to merge commit...") + try: + pr_info = self.get_pr_info(pr_num) + if not pr_info: + print(f"PR #{pr_num} not found. Gating.") + item.status = "error-not-found" + else: + state = pr_info.get("state") + is_draft = pr_info.get("isDraft", False) + if state == "OPEN" or is_draft: + print( + f"PR #{pr_num} is open or draft (state: {state}," + f" draft: {is_draft}). Ignoring." + ) + item.status = "open-pr" if not is_draft else "draft-pr" + elif state == "CLOSED": + print(f"PR #{pr_num} is closed but not merged. Gating.") + item.status = "error-closed-pr" + elif state == "MERGED": + merge_commit = pr_info.get("mergeCommit") + if merge_commit and "oid" in merge_commit: + item.commit = merge_commit["oid"] + item.status = "resolved" + else: + print(f"PR #{pr_num} has no merge commit SHA. Gating.") + item.status = "error-no-merge-commit" + else: + print(f"PR #{pr_num} has unknown state: {state}. Gating.") + item.status = "error-unknown" + except Exception as e: + print(f"Error resolving PR #{pr_num}: {e}. Gating.") + item.status = "error-resolution-failed" + resolved_items.append(item) + return resolved_items diff --git a/tools/private/release/git.py b/tools/private/release/git.py new file mode 100644 index 0000000000..4305de855f --- /dev/null +++ b/tools/private/release/git.py @@ -0,0 +1,399 @@ +"""Git helper functions for the release tool.""" + +import subprocess + +from tools.private.release.shell import run_cmd + + +class Git: + """Git helper class for the release tool. + + Operates on a specific git repository path. + """ + + def __init__(self, repo: str): + """Initializes the Git helper. + + Args: + repo: The path to the git repository. + """ + self._repo = repo + + def _run_git( + self, *args: str, check: bool = True, capture_output: bool = True + ) -> str | None: + """Runs a git command in the repository directory. + + Args: + *args: Arguments passed to the git command. + check: If True, raises CalledProcessError on failure. + capture_output: If True, captures and returns stdout. + + Returns: + The stdout of the command, stripped, or None if capture_output is + False. + """ + return run_cmd( + "git", + *args, + check=check, + capture_output=capture_output, + cwd=self._repo, + ) + + def get_tags(self) -> list[str]: + """Returns a list of all git tags in the repository. + + Returns: + A list of tag names (strings). + """ + output = self._run_git("tag") + return output.splitlines() if output else [] + + def checkout( + self, + ref: str, + create_branch: bool = False, + track_remote: str | None = None, + ) -> None: + """Checks out a git reference (tag, branch, or commit). + + Args: + ref: The git reference (tag, branch, or commit) to checkout. + create_branch: If True, creates the branch before checking it out. + track_remote: If specified, checks out the branch tracking this + remote's corresponding branch. + """ + cmd = ["checkout"] + if create_branch: + cmd.append("-b") + + should_reset_hard = False + if track_remote: + if self.branch_exists(ref): + cmd.append(ref) + should_reset_hard = True + else: + cmd.extend(["--track", f"{track_remote}/{ref}"]) + else: + cmd.append(ref) + self._run_git(*cmd, capture_output=False) + + if should_reset_hard: + self.reset_hard(reset_to=f"{track_remote}/{ref}") + + def add(self, *files: str) -> None: + """Stages files for commit. + + Args: + *files: Paths to files to stage. + """ + self._run_git("add", *files, capture_output=False) + + def add_modified_and_deleted(self) -> None: + """Stages all modified and deleted tracked files.""" + self._run_git("add", "--update", capture_output=False) + + def commit(self, message: str, amend: bool = False, no_edit: bool = False) -> None: + """Commits staged changes, optionally amending the previous commit. + + Args: + message: The commit message. + amend: If True, amends the previous commit. + no_edit: If True, uses the existing commit message without editing. + """ + cmd = ["commit"] + if amend: + cmd.append("--amend") + if no_edit: + cmd.append("--no-edit") + if message: + cmd.extend(["-m", message]) + self._run_git(*cmd, capture_output=False) + + def push( + self, + remote: str, + ref: str, + set_upstream: bool = False, + force: bool = False, + ) -> None: + """Pushes a reference to a remote repository. + + Args: + remote: The remote repository name (e.g., 'origin'). + ref: The reference to push (e.g., a branch name). + set_upstream: If True, sets the upstream tracking branch. + force: If True, force pushes the changes. + """ + cmd = ["push"] + if set_upstream: + cmd.append("--set-upstream") + if force: + cmd.append("--force") + cmd.extend([remote, ref]) + self._run_git(*cmd, capture_output=False) + + def fetch( + self, + remote: str = "origin", + refspec: str | None = None, + tags: bool = False, + force: bool = False, + ) -> None: + """Fetches updates from a remote repository. + + Args: + remote: The remote repository name. Defaults to 'origin'. + refspec: The refspec to fetch. + tags: If True, fetches all tags. + force: If True, force fetches updates. + """ + cmd = ["fetch", remote] + if refspec: + cmd.append(refspec) + if tags: + cmd.append("--tags") + if force: + cmd.append("--force") + self._run_git(*cmd, capture_output=False) + + def merge(self, commit_ref: str, ff_only: bool = True) -> None: + """Merges a commit into the current branch. + + Args: + commit_ref: The commit reference to merge. + ff_only: If True, only allows fast-forward merges. + """ + cmd = ["merge", commit_ref] + if ff_only: + cmd.append("--ff-only") + self._run_git(*cmd, capture_output=False) + + def tag(self, tag_name: str, commit_ref: str) -> None: + """Creates a local tag pointing to a specific commit. + + Args: + tag_name: The name of the tag to create. + commit_ref: The commit reference the tag should point to. + """ + self._run_git("tag", tag_name, commit_ref, capture_output=False) + + def cherry_pick(self, sha: str) -> None: + """Cherry-picks a commit. + + Args: + sha: The commit SHA to cherry-pick. + """ + self._run_git("cherry-pick", "-x", sha, capture_output=False) + + def cherry_pick_abort(self) -> None: + """Aborts an in-progress cherry-pick operation.""" + self._run_git("cherry-pick", "--abort", capture_output=False) + + def reset_hard(self, *, reset_to: str = "HEAD") -> None: + """Resets the index and working tree to a specific reference. + + Args: + reset_to: The git reference to reset to. Defaults to 'HEAD'. + """ + self._run_git("reset", "--hard", reset_to, capture_output=False) + + def status(self) -> str: + """Returns the output of git status --porcelain. + + Returns: + The porcelain status output. + """ + output = self._run_git("status", "--porcelain") + return output if output else "" + + def get_commit_sha(self, ref: str = "HEAD", short: bool = False) -> str: + """Returns the commit SHA of a given reference. + + Args: + ref: The git reference. Defaults to 'HEAD'. + short: If True, returns a short SHA. + + Returns: + The commit SHA. + """ + cmd = ["rev-parse"] + if short: + cmd.append("--short") + cmd.append(ref) + output = self._run_git(*cmd) + return output if output else "" + + def get_commit_message(self, ref: str = "HEAD") -> str: + """Returns the commit message of a given reference. + + Args: + ref: The git reference. Defaults to 'HEAD'. + + Returns: + The commit message. + """ + output = self._run_git("log", "-1", "--format=%B", ref) + return output if output else "" + + def branch_exists(self, branch_name: str) -> bool: + """Returns True if a local branch exists. + + Args: + branch_name: The name of the branch to check. + + Returns: + True if the branch exists, False otherwise. + """ + try: + self._run_git("show-ref", "--verify", f"refs/heads/{branch_name}") + return True + except subprocess.CalledProcessError: + return False + + def tag_exists(self, tag_name: str) -> bool: + """Returns True if a local tag exists. + + Args: + tag_name: The name of the tag to check. + + Returns: + True if the tag exists, False otherwise. + """ + try: + self._run_git("show-ref", "--verify", f"refs/tags/{tag_name}") + return True + except subprocess.CalledProcessError: + return False + + def sort_commits_chronologically(self, shas: list[str]) -> list[str]: + """Sorts a list of commit SHAs chronologically (oldest first). + + Args: + shas: A list of commit SHAs to sort. + + Returns: + The sorted list of commit SHAs. + """ + output = self._run_git("log", "--no-walk", "--reverse", "--format=%H", *shas) + return output.splitlines() if output else [] + + def get_current_branch(self) -> str: + """Returns the current git branch name. + + Returns: + The current branch name. + """ + output = self._run_git("rev-parse", "--abbrev-ref", "HEAD") + return output if output else "" + + def remote_branch_exists(self, remote: str, branch_name: str) -> bool: + """Returns True if a remote branch exists. + + Args: + remote: The name of the remote. + branch_name: The name of the branch. + + Returns: + True if the remote branch exists, False otherwise. + """ + try: + self._run_git( + "show-ref", + "--verify", + f"refs/remotes/{remote}/{branch_name}", + ) + return True + except subprocess.CalledProcessError: + return False + + def is_ancestor(self, ancestor: str, descendant: str) -> bool: + """Returns True if ancestor is an ancestor of descendant. + + Args: + ancestor: The commit reference that might be an ancestor. + descendant: The commit reference that might be a descendant. + + Returns: + True if ancestor is an ancestor of descendant, False otherwise. + """ + try: + self._run_git("merge-base", "--is-ancestor", ancestor, descendant) + return True + except subprocess.CalledProcessError: + return False + + def get_remote_tags(self, remote: str) -> list[str]: + """Returns a list of tags present on the specified remote repository. + + Args: + remote: The name of the git remote to query (e.g., 'origin', + 'upstream'). + + Returns: + A list of tag names (strings) found on the remote, excluding peeled + tags. + """ + output = self._run_git("ls-remote", "--tags", remote) + tags = [] + if not output: + return tags + for line in output.splitlines(): + if not line: + continue + parts = line.split() + if len(parts) < 2: + continue + ref = parts[1] + if ref.startswith("refs/tags/"): + tag = ref[len("refs/tags/") :] + # Skip peeled tags (e.g. tag^{}) to avoid + # duplicate tag names in the output. + if not tag.endswith("^{}"): + tags.append(tag) + return tags + + def get_modified_files(self, ref: str) -> list[str]: + """Returns a list of files modified in a given reference. + + Args: + ref: The git reference. + + Returns: + A list of file paths. + """ + output = self._run_git("show", "--name-only", "--format=", ref) + return [line for line in output.splitlines() if line.strip()] if output else [] + + def diff(self) -> str: + """Returns the diff of unstaged changes. + + Returns: + The diff output as a string. + """ + output = self._run_git("diff") + return output if output else "" + + def apply(self, patch_file: str) -> None: + """Applies a patch file. + + Args: + patch_file: The path to the patch file. + """ + self._run_git("apply", patch_file, capture_output=False) + + def apply_check(self, patch_file: str) -> bool: + """Verifies if a patch can be applied cleanly. + + Args: + patch_file: The path to the patch file. + + Returns: + True if the patch can be applied cleanly, False otherwise. + """ + try: + self._run_git("apply", "--check", patch_file, capture_output=False) + return True + except subprocess.CalledProcessError: + return False diff --git a/tools/private/release/on_pr_merged.py b/tools/private/release/on_pr_merged.py new file mode 100644 index 0000000000..8d451580fb --- /dev/null +++ b/tools/private/release/on_pr_merged.py @@ -0,0 +1,113 @@ +"""Subcommand to handle PR merge event by processing backports.""" + +import argparse +import re + +from tools.private.release.gh import GitHub +from tools.private.release.git import Git +from tools.private.release.process_backports import ProcessBackports +from tools.private.release.release_issue import parse_backports + + +class OnPrMerged: + """Class to handle PR merge event.""" + + def __init__(self, args, git: Git, gh: GitHub): + self.args = args + self.git = git + self.gh = gh + + def run(self) -> int: + """Executes the on-pr-merged subcommand.""" + args = self.args + pr_num = args.pr + pr_ref = f"#{pr_num}" + + print(f"Verifying PR {pr_ref} has backport comment...") + try: + comments = self.gh.get_pr_comments(pr_num) + has_comment = any( + re.match( + r"^\s*/backport(\s|$)", + comment.get("body") or "", + re.IGNORECASE, + ) + for comment in comments + ) + if not has_comment: + print(f"PR {pr_ref} does not have a /backport comment. Skipping.") + return 1 + except Exception as e: + print(f"Error checking PR comments: {e}") + return 1 + + print(f"Searching for active release tracking issue containing PR {pr_ref}...") + open_issues = self.gh.get_open_tracking_issues() + if not open_issues: + print("No open release tracking issues found.") + return 1 + + found_issue = None + for issue in open_issues: + issue_num = issue["number"] + body = self.gh.get_issue_body(issue_num) + backports = parse_backports(body) + + if any(item.pr_ref == pr_ref for item in backports): + if found_issue: + print( + f"Error: PR {pr_ref} found in multiple open release" + f" tracking issues: #{found_issue} and #{issue_num}" + ) + return 1 + found_issue = issue_num + + if not found_issue: + print(f"PR {pr_ref} not found in any active release tracking issue.") + return 1 + + print(f"Found PR {pr_ref} in tracking issue #{found_issue}") + + # Now run ProcessBackports for this issue + process_args = argparse.Namespace( + issue=found_issue, + remote=args.remote, + add=None, + triggering_comment=None, + dry_run=args.dry_run, + ) + print(f"Processing backports for issue #{found_issue}...") + return ProcessBackports(process_args, self.git, self.gh).run() + + @classmethod + def add_parser(cls, subparsers): + """Adds parser for on-pr-merged subcommand.""" + parser = subparsers.add_parser( + "on-pr-merged", + help="Handle PR merge event by processing backports.", + ) + parser.add_argument( + "pr", + type=int, + help="PR number that was merged.", + ) + parser.add_argument( + "--remote", + type=str, + required=True, + help="The git remote to push changes to (required).", + ) + parser.add_argument( + "--dry-run", + action=argparse.BooleanOptionalAction, + default=True, + help="Perform a dry run (default: True). Use --no-dry-run to actually execute.", + ) + parser.set_defaults(command=cls.run_from_args) + + @classmethod + def run_from_args(cls, args): + """Instantiates and runs the command from parsed args.""" + git = Git(".") + gh = GitHub() + return cls(args, git, gh).run() diff --git a/tools/private/release/prepare.py b/tools/private/release/prepare.py new file mode 100644 index 0000000000..fd19362bec --- /dev/null +++ b/tools/private/release/prepare.py @@ -0,0 +1,236 @@ +"""Subcommand to prepare the release (updates changelog, placeholders).""" + +import argparse +import datetime +import pathlib + +from tools.private.release import changelog_news +from tools.private.release.gh import ( + GitHub, + MultipleTrackingIssuesError, + NoTrackingIssueError, +) +from tools.private.release.git import Git +from tools.private.release.release_issue import ( + parse_checklist_state, + update_task_in_body, +) +from tools.private.release.utils import ( + determine_next_version, + replace_version_next, + semver_type, +) + + +class Prepare: + """Class to prepare the release.""" + + def __init__(self, args, git: Git, gh: GitHub): + self.args = args + self.git = git + self.gh = gh + + def run(self) -> int: + """Executes the prepare subcommand.""" + args = self.args + print("Fetching upstream to verify fresh release history...") + self.git.fetch(tags=True, force=True) + + # Run pre-check: verify there are no local edits + status = self.git.status() + if status: + print( + "Error: Local edits detected. Workspace must be completely clean" + " before running release preparation." + ) + for line in status.splitlines(): + print(f" {line}") + return 1 + print("Pre-check passed: Workspace is clean.") + + version = args.version + if version is None: + version = determine_next_version() + + print(f"Running preparation pipeline for {version}...") + + # 1. Find or create tracking issue (EARLY) + # We do this before any write operations (branch creation, commit, push) + issue_num = args.issue + + if not issue_num: + try: + issue_num = self.gh.get_release_tracking_issue(version) + print(f"Tracking issue: #{issue_num}") + except MultipleTrackingIssuesError as e: + print(f"Error: {e}") + return 1 + except NoTrackingIssueError: + # Not found, we need the template + template_path = pathlib.Path( + ".github/ISSUE_TEMPLATE/release_tracking_template.md" + ) + if not template_path.exists(): + raise FileNotFoundError( + f"Template file not found at {template_path}" + ) + template_content = template_path.read_text(encoding="utf-8") + + if args.dry_run: + print( + f"[DRY RUN] No active tracking issue found for" + f" {version}. Would create a new one." + ) + print(f"[DRY RUN] Title: Release {version}\n{template_content}") + issue_num = None # Keep it None for dry-run prints later + else: + print( + f"No active tracking issue found for {version}." + " Creating a new one..." + ) + issue_num = self.gh.create_tracking_issue(version, template_content) + print(f"Tracking issue: #{issue_num}") + else: + print(f"Tracking issue: #{issue_num}") + + branch_name = f"prepare-{version}" + + # 2. Interleaved git and write operations + + # --- Branch selection/creation --- + if self.git.branch_exists(branch_name): + if args.dry_run: + print( + f"[DRY RUN] Branch {branch_name} already exists. Would" + " checkout existing branch." + ) + else: + print(f"Branch {branch_name} already exists. Checking it out...") + self.git.checkout(branch_name) + else: + if args.dry_run: + print(f"[DRY RUN] Would create and checkout branch {branch_name}") + else: + self.git.checkout(branch_name, create_branch=True) + + # --- Update files --- + if args.dry_run: + print( + f"[DRY RUN] Would update CHANGELOG.md and version placeholders" + f" for {version}" + ) + else: + print("Updating changelog and placeholders...") + release_date = datetime.date.today().strftime("%Y-%m-%d") + changelog_news.update_changelog(version, release_date) + replace_version_next(version) + + # --- Commit and Push --- + if args.dry_run: + print(f"[DRY RUN] Would push branch {branch_name} to origin") + else: + modified_files = self.git.status() + if modified_files: + # Stage all modified and deleted tracked files + self.git.add_modified_and_deleted() + self.git.commit(f"Prepare release {version}") + else: + print("No files modified by the release tool. Nothing to commit.") + + print(f"Pushing branch {branch_name} to origin...") + # Force push to overwrite the remote branch if it already exists (e.g. from a previous run) + self.git.push("origin", branch_name, set_upstream=True, force=True) + + # --- Create PR --- + # Determine if we need to create a PR or reuse an existing one + open_pr = self.gh.get_open_pr(branch_name) + associated_pr = None + + if not open_pr and issue_num: + body = self.gh.get_issue_body(issue_num) + state = parse_checklist_state(body) + associated_pr = state["prepare_release"].pr + + if open_pr: + pr_num = open_pr["number"] + pr_url = open_pr["url"] + print(f"Open Pull Request already exists: {pr_url} (PR #{pr_num})") + elif associated_pr: + pr_num = associated_pr.lstrip("#") + pr_url = f"https://github.com/bazel-contrib/rules_python/pull/{pr_num}" + print( + f"PR #{pr_num} is already associated in tracking issue" + f" #{issue_num}. Using it." + ) + else: + if args.dry_run: + target_issue = f"#{issue_num}" if issue_num else "" + print( + f"[DRY RUN] Would create Pull Request for branch" + f" {branch_name} targeting issue {target_issue}" + ) + pr_num = "" + else: + pr_url = self.gh.create_pr( + title=f"Prepare release v{version}", + body=f"Work towards #{issue_num}", + base="main", + ) + pr_num = pr_url.split("/")[-1] + print(f"Created Pull Request: {pr_url} (PR #{pr_num})") + + # --- Update checklist --- + if args.dry_run: + target_issue = f"#{issue_num}" if issue_num else "" + print( + f"[DRY RUN] Would update tracking issue {target_issue} checklist" + " 'Prepare Release' task status to PENDING" + ) + else: + print( + f"Updating tracking issue #{issue_num} checklist 'Prepare" + " Release' task status to PENDING..." + ) + body = self.gh.get_issue_body(issue_num) + metadata = {"status": "pending", "pr": f"#{pr_num}"} + updated_body = update_task_in_body( + body, "Prepare Release", checked=False, metadata=metadata + ) + self.gh.update_issue_body(issue_num, updated_body) + print("Preparation pipeline completed successfully!") + + return 0 + + @classmethod + def add_parser(cls, subparsers): + """Adds parser for prepare subcommand.""" + parser = subparsers.add_parser( + "prepare", + help="Prepare the release (updates changelog, placeholders).", + ) + parser.add_argument( + "version", + nargs="?", + type=semver_type, + help="The new release version (e.g., 0.28.0). If not provided, " + "it will be determined automatically.", + ) + parser.add_argument( + "--issue", + type=int, + help="The tracking issue number (optional, triggers automated branch/PR pipeline).", + ) + parser.add_argument( + "--dry-run", + action=argparse.BooleanOptionalAction, + default=True, + help="Perform a dry run (default: True). Use --no-dry-run to actually execute.", + ) + parser.set_defaults(command=cls.run_from_args) + + @classmethod + def run_from_args(cls, args): + """Instantiates and runs the command from parsed args.""" + git = Git(".") + gh = GitHub() + return cls(args, git, gh).run() diff --git a/tools/private/release/process_backports.py b/tools/private/release/process_backports.py new file mode 100644 index 0000000000..27eb3226b1 --- /dev/null +++ b/tools/private/release/process_backports.py @@ -0,0 +1,631 @@ +"""Subcommand to process pending backports.""" + +import argparse +import datetime +import hashlib +import os +import tempfile +from dataclasses import dataclass +from typing import Any + +from tools.private.release import changelog_news +from tools.private.release.gh import GH_REACTION_THUMBS_DOWN, GitHub +from tools.private.release.git import Git +from tools.private.release.release_issue import ( + RELEASE_TITLE_RE, + add_backports_to_body, + add_rc_task_to_body, + add_sync_changelog_task_to_body, + parse_backports, + parse_checklist_state, + update_task_in_body, +) +from tools.private.release.utils import ( + get_latest_rc_tag, + parse_pr_list, + replace_version_next, +) + + +@dataclass +class CherryPickAndUpdatePrsResult: + # List of PR references that failed to cherry-pick. + failed_prs: list[str] + # List of news files collected from the successful cherry-picks. + collected_news_files: list[str] + # List of PR numbers that were successfully cherry-picked. + successful_pr_nums: list[int] + # List of tuples mapping successful PR numbers to their version marker diffs. + collected_diffs: list[tuple[int, str]] + # The updated checklist body for the release tracking issue. + body: str + + +class ProcessBackports: + """Class to process pending backports.""" + + def __init__(self, args, git: Git, gh: GitHub): + self.args = args + self.git = git + self.gh = gh + + def _process_pr_commit_infos( + self, pr_commit_infos, body, issue, dry_run + ) -> tuple[list[str], dict[str, Any], list[str], list[str], str]: + shas = [] + sha_to_item = {} + failed_prs = [] + ignored_prs = [] + for item in pr_commit_infos: + if item.commit: + sha = item.commit + sha_to_item[sha] = item + shas.append(sha) + elif item.status in ("open-pr", "draft-pr"): + print(f"PR {item.pr_ref} is open or draft. Ignoring.") + ignored_prs.append(item.pr_ref) + else: + failed_prs.append(item.pr_ref) + status_to_set = item.status or "error-unmerged-pr" + if dry_run: + print( + f"[DRY RUN] Would update tracking issue checklist for" + f" unresolved PR {item.pr_ref} to status={status_to_set}" + ) + else: + print( + f"Updating tracking issue checklist for unresolved PR" + f" {item.pr_ref}..." + ) + try: + body = update_task_in_body( + body, + item.pr_ref, + checked=False, + metadata={"status": status_to_set}, + ) + self.gh.update_issue_body(issue, body) + except Exception as e: + print( + f"ERROR: Failed to update tracking issue for" + f" unresolved PR {item.pr_ref}: {e}" + ) + return shas, sha_to_item, failed_prs, ignored_prs, body + + def _cherry_pick_and_update_prs( + self, + sorted_shas, + sha_to_item, + body, + issue, + remote, + dry_run, + version, + branch_name, + next_rc_suffix, + ) -> CherryPickAndUpdatePrsResult: + failed_prs = [] + collected_news_files = [] + successful_pr_nums = [] + collected_diffs = [] + for sha in sorted_shas: + item = sha_to_item[sha] + print(f"Cherry-picking {item.pr_ref} / {sha}...") + try: + self.git.cherry_pick(sha) + + # Collect news files before they are deleted by update_changelog + modified_files = self.git.get_modified_files("HEAD") + for f in modified_files: + if changelog_news.is_news_file(f): + collected_news_files.append(f) + + # Replace version markers FIRST to isolate diff + print(f"Replacing version markers for PR {item.pr_ref}...") + replace_version_next(version) + + # Get diff of unstaged changes (version marker replacement) + diff_content = self.git.diff() + + # Perform news processing (merging news/ files into the changelog) + print(f"Merging news fragments into changelog for PR {item.pr_ref}...") + release_date = datetime.date.today().strftime("%Y-%m-%d") + changelog_news.update_changelog(version, release_date) + + # Stage changelog changes, news/ deletions, and version placeholder updates + self.git.add_modified_and_deleted() + + # Amend cherry-pick commit to include news merging and deletions, + # and reference the release tracking issue. + print(f"Amending cherry-pick commit for PR {item.pr_ref}...") + current_msg = self.git.get_commit_message("HEAD") + new_msg = f"{current_msg.strip()}\n\nWork towards #{issue}" + self.git.commit(new_msg, amend=True) + + try: + pr_num = self.gh.resolve_pr_number(item.pr_ref) + if diff_content: + collected_diffs.append((pr_num, diff_content)) + successful_pr_nums.append(pr_num) + except Exception as e: + print( + f"Warning: Failed to resolve PR number for {item.pr_ref}: {e}" + ) + + if not dry_run: + # Push amended commit + self.git.push(remote, branch_name) + + new_sha = self.git.get_commit_sha("HEAD", short=True) + metadata = { + "status": "done", + "rc": next_rc_suffix, + "commit": new_sha, + } + print(f"Updating tracking issue checklist for PR {item.pr_ref}...") + try: + body = update_task_in_body( + body, item.pr_ref, checked=True, metadata=metadata + ) + self.gh.update_issue_body(issue, body) + except Exception as e: + print( + f"ERROR: Failed to update tracking issue for PR" + f" {item.pr_ref}: {e}" + ) + print(f"Success: backported {item.pr_ref} / {sha} to {branch_name}") + else: + print( + f"[DRY RUN] Success: {item.pr_ref} / {sha} can be" + f" backported without error." + ) + print( + f"[DRY RUN] Would update tracking issue checklist for" + f" PR {item.pr_ref} to status=done" + ) + except Exception as e: + print(f"ERROR: Conflict or error on {sha}: {e}. Aborting.") + try: + self.git.cherry_pick_abort() + except Exception: + pass + failed_prs.append(item.pr_ref) + + if dry_run: + print( + f"[DRY RUN] Would update tracking issue checklist for" + f" failed PR {item.pr_ref} to status=error-merge-conflict" + ) + else: + print( + f"Updating tracking issue checklist for failed PR" + f" {item.pr_ref}..." + ) + try: + body = update_task_in_body( + body, + item.pr_ref, + checked=False, + metadata={"status": "error-merge-conflict"}, + ) + self.gh.update_issue_body(issue, body) + print( + f"Updated back port of {item.pr_ref} to" + f" status=error-merge-conflict (unchecked)" + ) + except Exception as e: + print( + f"ERROR: Failed to update tracking issue for" + f" failed PR {item.pr_ref}: {e}" + ) + return CherryPickAndUpdatePrsResult( + failed_prs=failed_prs, + collected_news_files=collected_news_files, + successful_pr_nums=successful_pr_nums, + collected_diffs=collected_diffs, + body=body, + ) + + def _sync_changelog_to_main( + self, + version: str, + collected_news_files: list[str], + successful_pr_nums: list[int], + collected_diffs: list[tuple[int, str]], + release_branch: str, + ) -> None: + args = self.args + sorted_prs = sorted(successful_pr_nums) + prs_str = ",".join(str(n) for n in sorted_prs) + prs_hash = hashlib.sha256(prs_str.encode()).hexdigest()[:7] + + main_branch = "main" + backport_branch = f"prepare-{version}-backports-{prs_hash}" + + print(f"Syncing changelog to {main_branch} via branch {backport_branch}...") + + self.git.fetch(args.remote, refspec=main_branch) + self.git.checkout(main_branch, track_remote=args.remote) + main_start_sha = self.git.get_commit_sha("HEAD") + + failed_version_sync_prs = [] + try: + if args.dry_run: + print( + f"[DRY RUN] Would create and checkout branch {backport_branch} from {main_branch}" + ) + else: + if self.git.branch_exists(backport_branch): + self.git.checkout(backport_branch) + self.git.reset_hard(reset_to=main_branch) + else: + self.git.checkout(backport_branch, create_branch=True) + + print( + f"Updating CHANGELOG.md and removing news files on {backport_branch}..." + ) + release_date = datetime.date.today().strftime("%Y-%m-%d") + changelog_news.update_changelog( + version, + release_date, + news_files=collected_news_files, + delete_news=True, + ) + + # Apply version marker diffs + failed_version_sync_prs = self._apply_version_marker_diffs(collected_diffs) + + if args.dry_run: + print( + f"[DRY RUN] Would commit: 'chore(release): sync changelog for v{version} backports'" + ) + print(f"[DRY RUN] Would push {backport_branch} to {args.remote}") + print( + f"[DRY RUN] Would create PR to {main_branch} with label 'type: sync-changelog'" + ) + print( + f"[DRY RUN] Would update tracking issue #{args.issue} checklist tasks 'Sync Changelog #' to PENDING" + ) + print("[DRY RUN] Diff of changes:") + print(self.git.status()) + else: + self.git.add_modified_and_deleted() + self.git.commit( + f"chore(release): sync changelog for v{version} backports" + ) + self.git.push( + args.remote, backport_branch, set_upstream=True, force=True + ) + + pr_title = f"chore(release): sync changelog for v{version} backports" + pr_body_lines = [ + "Updates CHANGELOG.md and removes news files for backports:", + ] + for pr_num in sorted_prs: + pr_body_lines.append(f"- #{pr_num}") + + if failed_version_sync_prs: + pr_body_lines.append("") + pr_body_lines.append( + "Warning: These PRs failed to update their version markers:" + ) + for pr_num in sorted(failed_version_sync_prs): + pr_body_lines.append(f"- #{pr_num}") + + pr_body_lines.append("") + pr_body_lines.append(f"Work towards #{args.issue}") + pr_body_lines.append(f"Release-Tracking-Issue: #{args.issue}") + pr_body = "\n".join(pr_body_lines) + + print(f"Creating PR to {main_branch}...") + pr_url = self.gh.create_pr( + title=pr_title, + body=pr_body, + base=main_branch, + labels=["type: sync-changelog"], + ) + print(f"Created PR: {pr_url}") + + try: + pr_num = int(pr_url.split("/")[-1]) + print(f"Enabling auto-merge for PR #{pr_num}...") + self.gh.enable_auto_merge(pr_num) + + print( + f"Updating tracking issue #{args.issue} checklist with" + " Sync Changelog tasks..." + ) + issue_body = self.gh.get_issue_body(args.issue) + for pr in successful_pr_nums: + task_name = f"Sync Changelog #{pr}" + metadata = {"status": "pending", "pr": f"#{pr_num}"} + issue_body = update_task_in_body( + issue_body, + task_name, + checked=False, + metadata=metadata, + ) + self.gh.update_issue_body(args.issue, issue_body) + except Exception as e: + print( + f"Warning: Failed to update tracking issue or enable" + f" auto-merge: {e}" + ) + finally: + if args.dry_run: + self.git.reset_hard(reset_to=main_start_sha) + self.git.checkout(release_branch) + + def _apply_version_marker_diffs( + self, + collected_diffs: list[tuple[int, str]], + ) -> list[int]: + """Applies version marker diffs on main branch and returns failed PR numbers.""" + args = self.args + failed_version_sync_prs = [] + if not collected_diffs: + return failed_version_sync_prs + + with tempfile.TemporaryDirectory() as temp_dir: + print(f"Applying {len(collected_diffs)} version marker patches...") + for pr_num, diff_content in collected_diffs: + if args.dry_run: + print( + f"[DRY RUN] Would check and apply version marker patch for PR #{pr_num}" + ) + + patch_filepath = os.path.join(temp_dir, f"{pr_num}.patch") + with open(patch_filepath, "w", encoding="utf-8") as f: + f.write(diff_content) + + if self.git.apply_check(patch_filepath): + if args.dry_run: + print( + f"[DRY RUN] Version marker patch for PR #{pr_num} applies cleanly." + ) + else: + print(f"Applying version marker patch for PR #{pr_num}...") + self.git.apply(patch_filepath) + else: + print( + f"Warning: Version marker patch for PR #{pr_num} could not be applied cleanly to main. Skipping." + ) + failed_version_sync_prs.append(pr_num) + return failed_version_sync_prs + + def run(self) -> int: + """Executes the process-backports subcommand.""" + args = self.args + exit_code = 0 + try: + exit_code = self._run_internal() + except Exception as e: + print(f"Unexpected error: {e}") + exit_code = 1 + + if exit_code != 0 and args.triggering_comment: + print(f"Reacting with thumbs-down to comment {args.triggering_comment}...") + try: + self.gh.add_comment_reaction( + args.triggering_comment, GH_REACTION_THUMBS_DOWN + ) + except Exception as e: + print(f"Failed to add reaction to comment: {e}") + + return exit_code + + def _run_internal(self) -> int: + """Internal implementation of process-backports.""" + args = self.args + body = self.gh.get_issue_body(args.issue) + + if args.add: + items_to_add = [] + for pr_ref in args.add: + try: + pr_num = self.gh.resolve_pr_number(pr_ref) + items_to_add.append({"ref": f"#{pr_num}"}) + except Exception as e: + print(f"Warning: PR ref '{pr_ref}' is invalid: {e}") + items_to_add.append( + { + "ref": pr_ref, + "metadata": {"status": "error-invalid-pr"}, + } + ) + + print(f"Adding backports {items_to_add} to tracking issue #{args.issue}...") + try: + body = add_backports_to_body(body, items_to_add) + for item in items_to_add: + if ( + "metadata" in item + and item["metadata"].get("status") == "error-invalid-pr" + ): + continue + pr_num = int(item["ref"].lstrip("#")) + body = add_sync_changelog_task_to_body(body, pr_num) + state = parse_checklist_state(body) + rc_tags = state.get("rc_tags", {}) + has_pending_rc = any( + not task.checked and task.status != "done" + for task in rc_tags.values() + ) + next_rc_num = max(rc_tags.keys()) + 1 if rc_tags else 0 + if not has_pending_rc: + print( + f"No pending RC task found. Adding 'Tag" + f" RC{next_rc_num}' to checklist..." + ) + body = add_rc_task_to_body(body, next_rc_num) + except ValueError as e: + print(f"Error: {e}") + return 1 + + if not args.dry_run: + self.gh.update_issue_body(args.issue, body) + print("Successfully updated tracking issue checklist.") + else: + print( + "[DRY RUN] Would update tracking issue checklist with new" + " backports." + ) + if not has_pending_rc: + print(f"[DRY RUN] Would add 'Tag RC{next_rc_num}' to checklist.") + + items = parse_backports(body) + + pending_items = [ + item + for item in items + if not item.checked and not item.status.startswith("error-") + ] + + if not pending_items: + print("No pending backports found.") + return 0 + + print(f"Found {len(pending_items)} pending backports to process.") + + # Determine branch name from issue title + issue_title = self.gh.get_issue_title(args.issue) + version_match = RELEASE_TITLE_RE.search(issue_title) + if not version_match: + print(f"Error: Could not parse version from issue title: {issue_title}") + return 1 + + version = version_match.group(1) + branch_version = ".".join(version.split(".")[:2]) + branch_name = f"release/{branch_version}" + + # Determine next RC tag to write to backport metadata + self.git.fetch(args.remote, tags=True, force=True) + latest_rc = get_latest_rc_tag(version, remote=args.remote) + if not latest_rc: + next_rc_suffix = "rc0" + else: + rc_num = int(latest_rc.split("-rc")[-1]) + next_rc_suffix = f"rc{rc_num + 1}" + + # Resolve PRs to merge commits using gh helper. + pr_commit_infos = self.gh.get_merge_commits_for_prs(pending_items) + + shas, sha_to_item, failed_prs, ignored_prs, body = ( + self._process_pr_commit_infos( + pr_commit_infos, body, args.issue, args.dry_run + ) + ) + + if not shas: + print("No valid merge commits to process.") + if failed_prs: + print("Failed PRs:") + for pr in failed_prs: + print(f"- {pr}") + return 1 + return 0 + + # Verify workspace is clean before proceeding + if self.git.status(): + print( + "ERROR: Git workspace is dirty. Please commit or stash changes" + " before running backports." + ) + return 1 + + # Sort chronologically using git helper + sorted_shas = self.git.sort_commits_chronologically(shas) + + self.git.fetch(args.remote) + self.git.checkout(branch_name, track_remote=args.remote) + start_sha = self.git.get_commit_sha("HEAD") + + collected_news_files = [] + successful_pr_nums = [] + collected_diffs = [] + try: + result = self._cherry_pick_and_update_prs( + sorted_shas, + sha_to_item, + body, + args.issue, + args.remote, + args.dry_run, + version, + branch_name, + next_rc_suffix, + ) + failed_prs.extend(result.failed_prs) + collected_news_files.extend(result.collected_news_files) + successful_pr_nums.extend(result.successful_pr_nums) + collected_diffs.extend(result.collected_diffs) + body = result.body + finally: + if args.dry_run: + print(f"[DRY RUN] Resetting branch {branch_name} to {start_sha}") + self.git.reset_hard(reset_to=start_sha) + + if successful_pr_nums: + self._sync_changelog_to_main( + version, + collected_news_files, + successful_pr_nums, + collected_diffs, + branch_name, + ) + + if failed_prs: + print("ERROR: One or more cherry-picks/resolutions failed:") + for pr in failed_prs: + print(f"- {pr}") + return 1 + + if args.dry_run: + print("Dry run completed successfully. No errors found.") + else: + print("All backports successfully processed!") + return 0 + + @classmethod + def add_parser(cls, subparsers): + """Adds parser for process-backports subcommand.""" + parser = subparsers.add_parser( + "process-backports", + help="Cherry-pick pending backports listed in the tracking issue.", + ) + parser.add_argument( + "--issue", + type=int, + required=True, + help="The tracking issue number (required).", + ) + parser.add_argument( + "--remote", + type=str, + required=True, + help="The git remote to push changes to (required).", + ) + parser.add_argument( + "--add", + type=parse_pr_list, + help="PR references (numbers, #numbers, or URLs, comma/space separated) to add before processing.", + ) + parser.add_argument( + "--triggering-comment", + type=int, + help="The ID of the comment that triggered this run (optional).", + ) + parser.add_argument( + "--dry-run", + action=argparse.BooleanOptionalAction, + default=True, + help="Perform a dry run (default: True). Use --no-dry-run to actually execute.", + ) + parser.set_defaults(command=cls.run_from_args) + + @classmethod + def run_from_args(cls, args): + """Instantiates and runs the command from parsed args.""" + git = Git(".") + gh = GitHub() + return cls(args, git, gh).run() diff --git a/tools/private/release/promote_rc.py b/tools/private/release/promote_rc.py new file mode 100644 index 0000000000..34c8107af4 --- /dev/null +++ b/tools/private/release/promote_rc.py @@ -0,0 +1,233 @@ +"""Subcommand to promote a release candidate to final release.""" + +import argparse +import os +import urllib.parse + +from tools.private.release.gh import GitHub +from tools.private.release.git import Git +from tools.private.release.release_issue import ( + RELEASE_TITLE_RE, + update_task_in_body, +) +from tools.private.release.utils import ( + REPO_URL, + determine_next_version, + get_latest_rc_tag, + semver_type, +) + + +class PromoteRc: + """Class to promote a release candidate to final release.""" + + def __init__(self, args, git: Git, gh: GitHub): + self.args = args + self.git = git + self.gh = gh + + def run(self) -> int: + """Executes the promote-rc subcommand (Phase 3).""" + args = self.args + # Fetch from remote to ensure we have the latest tags + self.git.fetch(args.remote, tags=True, force=True) + + version = args.version + if version is None: + if args.issue: + issue_title = self.gh.get_issue_title(args.issue) + version_match = RELEASE_TITLE_RE.search(issue_title) + if version_match: + version = version_match.group(1) + print( + f"Resolved version {version} from tracking issue" + f" #{args.issue} title." + ) + else: + print( + f"Error: Could not parse version from issue title:" + f" {issue_title}" + ) + return 1 + else: + version = determine_next_version() + + latest_rc = get_latest_rc_tag(version, remote=args.remote) + if not latest_rc: + print(f"Error: No release candidate tags found matching {version}-rc*") + return 1 + + # Verify final tag doesn't already exist + if self.git.tag_exists(version): + print(f"Error: Final tag {version} already exists.") + return 1 + + # Verify issue can be found + issue_num = args.issue + if not issue_num: + try: + issue_num = self.gh.get_release_tracking_issue(version) + except ValueError as e: + print(f"Error: {e}") + return 1 + except Exception as e: + print(f"Error: Unexpected error finding tracking issue: {e}") + return 1 + + # Get commit SHA of the RC tag (which will be the same for the final tag) + commit_sha = self.git.get_commit_sha(latest_rc) + + # Verify issue can be found and read it early + print(f"Verifying tracking issue #{issue_num} format...") + body = self.gh.get_issue_body(issue_num) + + # Determine release branch name and verify it matches the RC tag commit + branch_version = ".".join(version.split(".")[:2]) + branch_name = f"release/{branch_version}" + remote_branch = f"{args.remote}/{branch_name}" + + print(f"Fetching remote branch {remote_branch}...") + self.git.fetch(args.remote, refspec=branch_name) + try: + branch_sha = self.git.get_commit_sha(remote_branch) + except Exception as e: + print( + f"Error: Could not get commit SHA for remote branch" + f" {remote_branch}: {e}" + ) + return 1 + + if commit_sha != branch_sha: + print( + f"Error: The latest RC tag {latest_rc} ({commit_sha[:8]}) is not at" + f" the head of release branch {remote_branch} ({branch_sha[:8]})." + ) + metadata = { + "status": "error-rc-tag-not-branch-head", + "rc": latest_rc, + "branch_commit": branch_sha[:8], + "tag_commit": commit_sha[:8], + } + try: + updated_body = update_task_in_body( + body, "Tag Final", checked=False, metadata=metadata + ) + except ValueError as e: + print(f"Error: Tracking issue #{issue_num} is malformed: {e}") + return 1 + + if not args.dry_run: + self.gh.update_issue_body(issue_num, updated_body) + print(f"Updated tracking issue #{issue_num} with error status.") + else: + print( + f"[DRY RUN] Would update tracking issue #{issue_num} with" + f" error status." + ) + return 1 + + # Verify issue is in the right format by trying to prepare the update (for success case) + metadata = {"status": "done", "tag": version, "commit": commit_sha[:8]} + try: + updated_body = update_task_in_body( + body, "Tag Final", checked=True, metadata=metadata + ) + except ValueError as e: + print(f"Error: Tracking issue #{issue_num} is malformed: {e}") + return 1 + + # All pre-conditions met, perform modifications + if args.dry_run: + print( + f"[DRY RUN] Pre-conditions passed successfully for promoting" + f" {latest_rc} to {version}." + ) + print(f"[DRY RUN] Would tag commit {commit_sha[:8]} as {version}") + print(f"[DRY RUN] Would push tag {version} to {args.remote}") + print(f"[DRY RUN] Would update tracking issue #{issue_num} checklist") + print(f"[DRY RUN] Would post comment to tracking issue #{issue_num}") + return 0 + + print( + f"Promoting {latest_rc} to final release {version} (commit" + f" {commit_sha[:8]}) using tracking issue #{issue_num}..." + ) + + # Tag the specific commit without checkout, and push to remote + self.git.tag(version, commit_sha) + self.git.push(args.remote, version) + + if github_output := os.environ.get("GITHUB_OUTPUT"): + with open(github_output, "a", encoding="utf-8") as f: + f.write(f"version={version}\n") + + print(f"Updating tracking issue #{issue_num} checklist...") + self.gh.update_issue_body(issue_num, updated_body) + + print(f"Posting comment to tracking issue #{issue_num}...") + + branch_url = f"{REPO_URL}/tree/{branch_name}" + release_url = f"{REPO_URL}/releases/tag/{version}" + bcr_entry_url = f"https://registry.bazel.build/modules/rules_python/{version}" + bcr_query = ( + f'is:pr ("bazel-contrib/rules_python" in:title) ("@{version}" in:title)' + ) + bcr_search_url = f"https://github.com/bazelbuild/bazel-central-registry/pulls?q={urllib.parse.quote(bcr_query)}" + + if run_id := os.environ.get("GITHUB_RUN_ID"): + release_workflow_url = f"{REPO_URL}/actions/runs/{run_id}" + else: + release_workflow_url = ( + f"{REPO_URL}/actions/workflows/release_promote_rc.yaml" + ) + + comment_body = f"""**New Release Tagged!** šŸšŸŒæ + +Version **{version}** has been successfully generated and tagged on branch [`{branch_name}`]({branch_url}). + +- [Github Release {version}]({release_url}) +- [BCR Entry {version}]({bcr_entry_url}) +- [BCR PRs]({bcr_search_url}) +- [Release workflow status]({release_workflow_url})""" + self.gh.post_issue_comment(issue_num, comment_body) + + return 0 + + @classmethod + def add_parser(cls, subparsers): + """Adds parser for promote-rc subcommand.""" + parser = subparsers.add_parser( + "promote-rc", + help="Promote the latest RC to final release.", + ) + parser.add_argument( + "version", + nargs="?", + type=semver_type, + help="The final version to release (e.g., 0.38.0).", + ) + parser.add_argument( + "--issue", + type=int, + help="The tracking issue number (optional).", + ) + parser.add_argument( + "--remote", + type=str, + required=True, + help="The git remote to push the final tag to (required).", + ) + parser.add_argument( + "--dry-run", + action=argparse.BooleanOptionalAction, + default=True, + help="Perform a dry run (default: True). Use --no-dry-run to actually execute.", + ) + parser.set_defaults(command=cls.run_from_args) + + @classmethod + def run_from_args(cls, args): + """Instantiates and runs the command from parsed args.""" + git = Git(".") + gh = GitHub() + return cls(args, git, gh).run() diff --git a/tools/private/release/release.py b/tools/private/release/release.py index def6754347..f008c093b1 100644 --- a/tools/private/release/release.py +++ b/tools/private/release/release.py @@ -1,196 +1,72 @@ """A tool to perform release steps.""" import argparse -import datetime -import fnmatch import os -import pathlib -import re -import subprocess - -from packaging.version import parse as parse_version - -_EXCLUDE_PATTERNS = [ - "./.git/*", - "./.github/*", - "./.bazelci/*", - "./.bcr/*", - "./bazel-*/*", - "./CONTRIBUTING.md", - "./RELEASING.md", - "./tools/private/release/*", - "./tests/tools/private/release/*", +import sys + +from tools.private.release.add_backports import AddBackports +from tools.private.release.complete_prepare import CompletePrepare +from tools.private.release.complete_sync_changelog import CompleteSyncChangelog +from tools.private.release.create_rc import CreateRc +from tools.private.release.create_release_branch import CreateReleaseBranch +from tools.private.release.create_release_issue import CreateReleaseIssue +from tools.private.release.determine_next_version import DetermineNextVersion +from tools.private.release.on_pr_merged import OnPrMerged +from tools.private.release.prepare import Prepare +from tools.private.release.process_backports import ProcessBackports +from tools.private.release.promote_rc import PromoteRc + +cmds = [ + DetermineNextVersion, + CreateReleaseIssue, + Prepare, + CompletePrepare, + CompleteSyncChangelog, + CreateReleaseBranch, + AddBackports, + ProcessBackports, + OnPrMerged, + CreateRc, + PromoteRc, ] -def _iter_version_placeholder_files(): - for root, dirs, files in os.walk(".", topdown=True): - # Filter directories - dirs[:] = [ - d - for d in dirs - if not any( - fnmatch.fnmatch(os.path.join(root, d), pattern) - for pattern in _EXCLUDE_PATTERNS - ) - ] - - for filename in files: - filepath = os.path.join(root, filename) - if any(fnmatch.fnmatch(filepath, pattern) for pattern in _EXCLUDE_PATTERNS): - continue - - yield filepath - - -def _get_git_tags(): - """Runs a git command and returns the output.""" - return subprocess.check_output(["git", "tag"]).decode("utf-8").splitlines() - - -def get_latest_version(): - """Gets the latest version from git tags.""" - tags = _get_git_tags() - # The packaging module can parse PEP440 versions, including RCs. - # It has a good understanding of version precedence. - versions = [ - (tag, parse_version(tag)) - for tag in tags - if re.match(r"^\d+\.\d+\.\d+(rc\d+)?$", tag.strip()) - ] - if not versions: - raise RuntimeError("No git tags found matching X.Y.Z or X.Y.ZrcN format.") - - versions.sort(key=lambda v: v[1]) - latest_tag, latest_version = versions[-1] - - if latest_version.is_prerelease: - raise ValueError(f"The latest version is a pre-release version: {latest_tag}") - - # After all that, we only want to consider stable versions for the release. - stable_versions = [tag for tag, version in versions if not version.is_prerelease] - if not stable_versions: - raise ValueError("No stable git tags found matching X.Y.Z format.") - - # The versions are already sorted, so the last one is the latest. - return stable_versions[-1] - - -def should_increment_minor(): - """Checks if the minor version should be incremented.""" - for filepath in _iter_version_placeholder_files(): - try: - with open(filepath, "r") as f: - content = f.read() - except (IOError, UnicodeDecodeError): - # Ignore binary files or files with read errors - continue - - if "VERSION_NEXT_FEATURE" in content: - return True - return False - - -def determine_next_version(): - """Determines the next version based on git tags and placeholders.""" - latest_version = get_latest_version() - major, minor, patch = [int(n) for n in latest_version.split(".")] - - if should_increment_minor(): - return f"{major}.{minor + 1}.0" - else: - return f"{major}.{minor}.{patch + 1}" - - -def update_changelog(version, release_date, changelog_path="CHANGELOG.md"): - """Performs the version replacements in CHANGELOG.md.""" - - header_version = version.replace(".", "-") - - changelog_path_obj = pathlib.Path(changelog_path) - lines = changelog_path_obj.read_text().splitlines() - - new_lines = [] - after_template = False - before_already_released = True - for line in lines: - if "END_UNRELEASED_TEMPLATE" in line: - after_template = True - if re.match("#v[1-9]-", line): - before_already_released = False - - if after_template and before_already_released: - line = line.replace("## Unreleased", f"## [{version}] - {release_date}") - line = line.replace("v0-0-0", f"v{header_version}") - line = line.replace("0.0.0", version) - - new_lines.append(line) - - changelog_path_obj.write_text("\n".join(new_lines)) - - -def replace_version_next(version): - """Replaces all VERSION_NEXT_* placeholders with the new version.""" - for filepath in _iter_version_placeholder_files(): - try: - with open(filepath, "r") as f: - content = f.read() - except (IOError, UnicodeDecodeError): - # Ignore binary files or files with read errors - continue - - if "VERSION_NEXT_FEATURE" in content or "VERSION_NEXT_PATCH" in content: - new_content = content.replace("VERSION_NEXT_FEATURE", version) - new_content = new_content.replace("VERSION_NEXT_PATCH", version) - with open(filepath, "w") as f: - f.write(new_content) - - -def _semver_type(value): - if not re.match(r"^\d+\.\d+\.\d+(rc\d+)?$", value): - raise argparse.ArgumentTypeError( - f"'{value}' is not a valid semantic version (X.Y.Z or X.Y.ZrcN)" - ) - return value - - def create_parser(): - """Creates the argument parser.""" + """Creates the argument parser with subcommands.""" parser = argparse.ArgumentParser( description="Automate release steps for rules_python." ) - parser.add_argument( - "version", - nargs="?", - type=_semver_type, - help="The new release version (e.g., 0.28.0). If not provided, " - "it will be determined automatically.", + + subparsers = parser.add_subparsers( + dest="command", required=True, help="Subcommands" ) - return parser + for cmd in cmds: + cmd.add_parser(subparsers) -def main(): - parser = create_parser() - args = parser.parse_args() + return parser - version = args.version - if version is None: - print("No version provided, determining next version automatically...") - version = determine_next_version() - print(f"Determined next version: {version}") - # Change to the workspace root so the script can be run using `bazel run` +def main(): + print(f"sys.argv: {sys.argv}") if "BUILD_WORKSPACE_DIRECTORY" in os.environ: os.chdir(os.environ["BUILD_WORKSPACE_DIRECTORY"]) - print("Updating changelog ...") - release_date = datetime.date.today().strftime("%Y-%m-%d") - update_changelog(version, release_date) - - print("Replacing VERSION_NEXT placeholders ...") - replace_version_next(version) + parser = create_parser() + args = parser.parse_args() - print("Done") + exit_code = 1 + try: + # args.command is the run_from_args classmethod of the selected command + exit_code = args.command(args) + except Exception as e: + print(f"Fatal error: {e}", file=sys.stderr) + if hasattr(e, "__notes__"): + for note in e.__notes__: + print(note, file=sys.stderr) + sys.exit(1) + + sys.exit(exit_code if exit_code is not None else 0) if __name__ == "__main__": diff --git a/tools/private/release/release_issue.py b/tools/private/release/release_issue.py new file mode 100644 index 0000000000..220348bd47 --- /dev/null +++ b/tools/private/release/release_issue.py @@ -0,0 +1,388 @@ +import re + + +class BackportTask: + """Represents a backport task from the tracking issue checklist.""" + + def __init__( + self, + pr_ref: str, + checked: bool, + status: str, + rc: str | None = None, + commit: str | None = None, + metadata: dict[str, str] | None = None, + ): + """Initializes a BackportTask. + + Args: + pr_ref: The PR reference (e.g. '#123'). + checked: Whether the checklist item is checked. + status: The status of the backport (e.g. 'pending', 'done', + 'error-merge-conflict'). + rc: The release candidate version this PR was backported to. + commit: The cherry-pick commit SHA. + metadata: Raw metadata parsed from the checklist line. + """ + self.pr_ref = pr_ref + self.checked = checked + self.status = status + self.rc = rc + self.commit = commit + self.metadata = metadata or {} + + def __repr__(self): + return ( + f"BackportTask(pr_ref={self.pr_ref!r}, checked={self.checked!r}, " + f"status={self.status!r}, rc={self.rc!r}, commit={self.commit!r})" + ) + + +class ReleaseTask: + """Represents a release task from the tracking issue checklist.""" + + def __init__( + self, + name: str, + checked: bool, + status: str | None = None, + pr: str | None = None, + commit: str | None = None, + branch: str | None = None, + tag: str | None = None, + metadata: dict[str, str] | None = None, + ): + """Initializes a ReleaseTask. + + Args: + name: The name of the task (e.g. 'Prepare Release'). + checked: Whether the checklist item is checked. + status: The status of the task (e.g. 'pending', 'done'). + pr: The associated PR reference (e.g. '#123'). + commit: The associated commit SHA. + branch: The associated branch name. + tag: The associated tag name. + metadata: Raw metadata parsed from the checklist line. + """ + self.name = name + self.checked = checked + self.status = status + self.pr = pr + self.commit = commit + self.branch = branch + self.tag = tag + self.metadata = metadata or {} + + def __repr__(self): + return ( + f"ReleaseTask(name={self.name!r}, checked={self.checked!r}, " + f"status={self.status!r}, pr={self.pr!r}, commit={self.commit!r}, " + f"branch={self.branch!r}, tag={self.tag!r})" + ) + + +RELEASE_TITLE_RE = re.compile(r"Release (\d+\.\d+\.\d+)", re.IGNORECASE) + + +def parse_metadata_line(line): + """Parses a checklist line with optional | key=value metadata.""" + match = re.match(r"^\s*-\s*\[([ xX])\]\s+([^|]+)(?:\s*\|\s*(.*))?$", line) + if not match: + return None + + checked = match.group(1).lower() == "x" + name = match.group(2).strip() + metadata_str = match.group(3) + + metadata = {} + if metadata_str: + for k, v in re.findall(r"(\w+)\s*=\s*(\S+)", metadata_str): + metadata[k] = v + + return { + "checked": checked, + "name": name, + "metadata": metadata, + "original_line": line, + } + + +def format_metadata_line(checked, name, metadata): + """Formats a checklist line with space-separated key=value metadata.""" + check_str = "x" if checked else " " + if not metadata: + return f"- [{check_str}] {name}" + + metadata_pairs = [] + for k, v in metadata.items(): + if k == "commit" or k.endswith("_commit"): + # The 'commit' key (and keys ending with '_commit') is special-cased with + # a space after '=' so that GitHub autolinks the commit SHA. Autolinking + # requires certain characters to precede the value. + metadata_pairs.append(f"{k}= {v}") + else: + metadata_pairs.append(f"{k}={v}") + metadata_str = " ".join(metadata_pairs) + return f"- [{check_str}] {name} | {metadata_str}" + + +def update_task_in_body(body, task_name, checked, metadata): + """Updates a specific task's checked state and metadata in the issue body.""" + lines = body.splitlines() + updated_lines = [] + found = False + + for line in lines: + parsed = parse_metadata_line(line) + if parsed and parsed["name"].lower() == task_name.lower(): + updated_lines.append(format_metadata_line(checked, task_name, metadata)) + found = True + else: + updated_lines.append(line) + + if not found: + raise ValueError( + f"Task '{task_name}' not found in issue body. " + f"Expected format: '- [ ] {task_name}' or '- [x] {task_name}' (optionally followed by '| key=value')" + ) + + return "\n".join(updated_lines) + + +def parse_checklist_state(body): + """Parses the main checklist tasks and their metadata. + + Returns: + A dict containing ReleaseTask objects for 'prepare_release', + 'create_branch', 'tag_final', and a dict of RC tags. + """ + state = { + "prepare_release": ReleaseTask("Prepare Release", False), + "create_branch": ReleaseTask("Create Release branch", False), + "tag_final": ReleaseTask("Tag Final", False), + "rc_tags": {}, # Dynamically mapped: int -> ReleaseTask + "sync_changelogs": {}, # Dynamically mapped: int -> ReleaseTask + } + + lines = body.splitlines() + for line in lines: + parsed = parse_metadata_line(line) + if not parsed: + continue + + name = parsed["name"].strip() + meta = parsed["metadata"] + checked = parsed["checked"] + name_lower = name.lower() + + if "prepare release" in name_lower: + state["prepare_release"] = ReleaseTask( + name=name, + checked=checked, + status=meta.get("status"), + pr=meta.get("pr"), + commit=meta.get("commit"), + metadata=meta, + ) + elif "create release branch" in name_lower: + state["create_branch"] = ReleaseTask( + name=name, + checked=checked, + status=meta.get("status"), + branch=meta.get("branch"), + commit=meta.get("commit"), + metadata=meta, + ) + elif "tag final" in name_lower: + state["tag_final"] = ReleaseTask( + name=name, + checked=checked, + status=meta.get("status"), + tag=meta.get("tag"), + commit=meta.get("commit"), + metadata=meta, + ) + else: + # Match Tag RC + rc_match = re.match(r"Tag RC(\d+)", name, re.IGNORECASE) + if rc_match: + rc_num = int(rc_match.group(1)) + state["rc_tags"][rc_num] = ReleaseTask( + name=name, + checked=checked, + status=meta.get("status"), + tag=meta.get("tag"), + commit=meta.get("commit"), + metadata=meta, + ) + else: + # Match Sync Changelog # + sync_match = re.match(r"Sync Changelog #(\d+)", name, re.IGNORECASE) + if sync_match: + pr_num = int(sync_match.group(1)) + state["sync_changelogs"][pr_num] = ReleaseTask( + name=name, + checked=checked, + status=meta.get("status"), + pr=meta.get("pr"), + commit=meta.get("commit"), + metadata=meta, + ) + + return state + + +def parse_backports(body): + """Parses the ## Backports checklist section.""" + body = body.replace("\r\n", "\n") + match = re.search( + r"## Backports\n(.*?)(?=\n##|\n---|\Z)", body, re.DOTALL | re.IGNORECASE + ) + if not match: + return [] + + section_content = match.group(1) + items = [] + lines = section_content.splitlines() + + for line in lines: + parsed = parse_metadata_line(line) + if parsed: + items.append( + BackportTask( + pr_ref=parsed["name"], + checked=parsed["checked"], + status=parsed["metadata"].get("status", "pending"), + rc=parsed["metadata"].get("rc"), + commit=parsed["metadata"].get("commit"), + metadata=parsed["metadata"], + ) + ) + return items + + +def add_backports_to_body(body: str, items: list[dict]) -> str: + """Adds new backport checklist items to the ## Backports section. + + Args: + body: The issue body. + items: A list of dicts, where each dict has a 'ref' key (str) and + optional 'metadata' key (dict). + """ + body = body.replace("\r\n", "\n") + # Find the Backports section + pattern = r"(## Backports\n)(.*?)(?=\n##|\n---|\Z)" + match = re.search(pattern, body, re.DOTALL | re.IGNORECASE) + if not match: + raise ValueError("Could not find '## Backports' section in issue body.") + + section_content = match.group(2) + + # Parse existing backports to avoid duplicates + existing_items = parse_backports(body) + existing_refs = {item.pr_ref for item in existing_items} + + new_lines = [] + for item in items: + ref = item["ref"] + # Normalize numeric refs to #numeric + if not ref.startswith("#") and ref.isdigit(): + ref = f"#{ref}" + + if ref in existing_refs: + print(f"PR {ref} is already in the backports list. Skipping.") + continue + existing_refs.add(ref) + + metadata = item.get("metadata", {}) + new_lines.append( + format_metadata_line(checked=False, name=ref, metadata=metadata) + ) + + if not new_lines: + return body + + # Append new lines to the section content. + section_content_clean = section_content.rstrip("\n") + separator = "\n" if section_content_clean else "" + updated_section = section_content_clean + separator + "\n".join(new_lines) + "\n\n" + + # Replace the old section with the updated one + start, end = match.span(2) + return body[:start] + updated_section + body[end:] + + +def add_rc_task_to_body(body: str, rc_num: int) -> str: + """Adds a new 'Tag RC' task to the checklist in the issue body.""" + body = body.replace("\r\n", "\n") + lines = body.splitlines() + + # Find the index of the last "Tag RC" line + last_rc_idx = -1 + for i, line in enumerate(lines): + parsed = parse_metadata_line(line) + if parsed and re.match(r"Tag RC\d+", parsed["name"], re.IGNORECASE): + last_rc_idx = i + + if last_rc_idx == -1: + # If no RC task found (unexpected, but fallback to before "Tag Final") + for i, line in enumerate(lines): + parsed = parse_metadata_line(line) + if parsed and parsed["name"].lower() == "tag final": + last_rc_idx = i - 1 + break + + if last_rc_idx == -1: + raise ValueError("Could not find a place to insert the new RC task.") + + new_task_line = f"- [ ] Tag RC{rc_num}" + lines.insert(last_rc_idx + 1, new_task_line) + + return "\n".join(lines) + + +def add_sync_changelog_task_to_body(body: str, pr_num: int) -> str: + """Adds a new 'Sync Changelog #' task to the checklist in the issue body.""" + body = body.replace("\r\n", "\n") + + # Check if already exists + task_name = f"Sync Changelog #{pr_num}" + lines = body.splitlines() + for line in lines: + parsed = parse_metadata_line(line) + if parsed and parsed["name"].lower() == task_name.lower(): + print(f"Task '{task_name}' already exists. Skipping.") + return body + + # Find the index of the last "Sync Changelog #" line + last_sync_idx = -1 + for i, line in enumerate(lines): + parsed = parse_metadata_line(line) + if parsed and re.match(r"Sync Changelog #\d+", parsed["name"], re.IGNORECASE): + last_sync_idx = i + + if last_sync_idx == -1: + # If no Sync Changelog task found, insert before "Tag Final" + for i, line in enumerate(lines): + parsed = parse_metadata_line(line) + if parsed and parsed["name"].lower() == "tag final": + last_sync_idx = i - 1 + break + + if last_sync_idx == -1: + # If "Tag Final" not found, insert after "Create Release branch" + for i, line in enumerate(lines): + parsed = parse_metadata_line(line) + if parsed and parsed["name"].lower() == "create release branch": + last_sync_idx = i + break + + if last_sync_idx == -1: + raise ValueError( + "Could not find a place to insert the new Sync Changelog task." + ) + + new_task_line = f"- [ ] Sync Changelog #{pr_num}" + lines.insert(last_sync_idx + 1, new_task_line) + + return "\n".join(lines) diff --git a/tools/private/release/shell.py b/tools/private/release/shell.py new file mode 100644 index 0000000000..0093a28574 --- /dev/null +++ b/tools/private/release/shell.py @@ -0,0 +1,31 @@ +"""Shell utility functions for the release tool.""" + +import shlex +import subprocess + + +def run_cmd(*args, check=True, capture_output=True, cwd=None): + """Runs a command as a subprocess with separate arguments (prints command). + + If the command fails, it raises the CalledProcessError after attaching + a detailed note explaining the failure to preserve the stack trace. + """ + cmd = [str(arg) for arg in args] + cwd_suffix = f" (cwd: {cwd})" if cwd else "" + print(f"> {shlex.join(cmd)}{cwd_suffix}") + try: + result = subprocess.run( + cmd, + check=check, + stdout=subprocess.PIPE if capture_output else None, + stderr=subprocess.PIPE if capture_output else None, + universal_newlines=True, + cwd=cwd, + ) + return result.stdout.strip() if capture_output else None + except subprocess.CalledProcessError as e: + note = f"Error running command: {shlex.join(cmd)}{cwd_suffix}" + if capture_output: + note += f"\nStdout: {e.stdout}\nStderr: {e.stderr}" + e.add_note(note) + raise diff --git a/tools/private/release/utils.py b/tools/private/release/utils.py new file mode 100644 index 0000000000..cdd1b7724d --- /dev/null +++ b/tools/private/release/utils.py @@ -0,0 +1,185 @@ +"""Utility functions for the release tool.""" + +import argparse +import fnmatch +import os +import re + +from packaging.version import parse as parse_version + +from tools.private.release.git import Git + +REPO_URL = "https://github.com/bazel-contrib/rules_python" + + +def semver_type(value): + """Argparse type validator for semantic versions.""" + if not re.match(r"^\d+\.\d+\.\d+(rc\d+)?$", value): + raise argparse.ArgumentTypeError( + f"'{value}' is not a valid semantic version (X.Y.Z or X.Y.ZrcN)" + ) + return value + + +_EXCLUDE_PATTERNS = [ + "./.git/*", + "./.github/*", + "./.bazelci/*", + "./.bcr/*", + "./bazel-*/*", + "./CONTRIBUTING.md", + "./RELEASING.md", + "./tools/private/release/*", + "./tests/tools/private/release/*", +] + + +def _iter_version_placeholder_files(): + for root, dirs, files in os.walk(".", topdown=True): + # Filter directories + dirs[:] = [ + d + for d in dirs + if not any( + fnmatch.fnmatch(os.path.join(root, d), pattern) + for pattern in _EXCLUDE_PATTERNS + ) + ] + + for filename in files: + filepath = os.path.join(root, filename) + if any(fnmatch.fnmatch(filepath, pattern) for pattern in _EXCLUDE_PATTERNS): + continue + + yield filepath + + +def get_latest_version(): + """Gets the latest version from git tags.""" + git = Git(".") + tags = git.get_tags() + versions = [ + (tag, parse_version(tag)) + for tag in tags + if re.match(r"^\d+\.\d+\.\d+(rc\d+)?$", tag.strip()) + ] + if not versions: + raise RuntimeError("No git tags found matching X.Y.Z or X.Y.ZrcN format.") + + versions.sort(key=lambda v: v[1]) + latest_tag, latest_version = versions[-1] + + if latest_version.is_prerelease: + raise ValueError(f"The latest version is a pre-release version: {latest_tag}") + + stable_versions = [tag for tag, version in versions if not version.is_prerelease] + if not stable_versions: + raise ValueError("No stable git tags found matching X.Y.Z format.") + + return stable_versions[-1] + + +def get_latest_rc_tag(version, remote=None): + """Queries git tags and returns the highest RC tag for the version.""" + git = Git(".") + if remote: + tags = git.get_remote_tags(remote) + else: + tags = git.get_tags() + pattern = rf"^{re.escape(version)}-rc\d+$" + rc_tags = [tag.strip() for tag in tags if re.match(pattern, tag.strip())] + if not rc_tags: + return None + rc_tags.sort(key=parse_version) + return rc_tags[-1] + + +def should_increment_minor(): + """Checks if the minor version should be incremented.""" + for filepath in _iter_version_placeholder_files(): + try: + with open(filepath, "r") as f: + content = f.read() + except (IOError, UnicodeDecodeError): + continue + + if "VERSION_NEXT_FEATURE" in content: + return True + return False + + +def determine_next_version(branch_name=None): + """Determines the next version based on git tags and the current branch.""" + git = Git(".") + if branch_name is None: + branch_name = git.get_current_branch() + + if branch_name: + release_match = re.match(r"^release/(\d+)\.(\d+)$", branch_name) + if release_match: + branch_major = int(release_match.group(1)) + branch_minor = int(release_match.group(2)) + print( + f"Detected release branch: {branch_name} (targeting" + f" {branch_major}.{branch_minor}.x)" + ) + + tags = git.get_tags() + matching_patches = [] + for tag in tags: + tag = tag.strip() + m = re.match(rf"^{branch_major}\.{branch_minor}\.(\d+)$", tag) + if m: + matching_patches.append(int(m.group(1))) + + if matching_patches: + latest_patch = max(matching_patches) + next_version = f"{branch_major}.{branch_minor}.{latest_patch + 1}" + print( + f"Latest tag on this branch is" + f" {branch_major}.{branch_minor}.{latest_patch}. Next" + f" version: {next_version}" + ) + return next_version + else: + next_version = f"{branch_major}.{branch_minor}.0" + print( + f"No stable tags found for {branch_major}.{branch_minor}.x." + f" Next version: {next_version}" + ) + return next_version + + latest_version = get_latest_version() + major, minor, patch = [int(n) for n in latest_version.split(".")] + + if should_increment_minor(): + return f"{major}.{minor + 1}.0" + else: + return f"{major}.{minor}.{patch + 1}" + + +def replace_version_next(version): + """Replaces all VERSION_NEXT_* placeholders with the new version.""" + for filepath in _iter_version_placeholder_files(): + try: + with open(filepath, "r") as f: + content = f.read() + except (IOError, UnicodeDecodeError): + continue + + if "VERSION_NEXT_FEATURE" in content or "VERSION_NEXT_PATCH" in content: + new_content = content.replace("VERSION_NEXT_FEATURE", version) + new_content = new_content.replace("VERSION_NEXT_PATCH", version) + with open(filepath, "w") as f: + f.write(new_content) + + +def parse_pr_list(value: str) -> list[str]: + """Parses a comma or space separated list of PR references. + + PR references can be numbers (optionally prefixed with '#') or URLs. + """ + if not value: + return [] + # Split by space and/or comma + return [p for p in re.split(r"[\s,]+", value.strip()) if p] diff --git a/tools/private/reviewbot/antigravity_review.py b/tools/private/reviewbot/antigravity_review.py new file mode 100644 index 0000000000..3aef480f3b --- /dev/null +++ b/tools/private/reviewbot/antigravity_review.py @@ -0,0 +1,85 @@ +# /// script +# requires-python = ">=3.11" +# dependencies = [ +# "google-antigravity", +# ] +# /// +import argparse +import asyncio +import subprocess +from pathlib import Path + +from google.antigravity import Agent, CapabilitiesConfig, LocalAgentConfig +from google.antigravity.models import GeminiAPIEndpoint, ModelTarget +from google.antigravity.types import BuiltinTools + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument("--prompt", required=True, help="Path to prompt file") + return parser.parse_args() + + +def get_pr_diff() -> str: + """Fetches the git diff for the current pull request against origin/main.""" + try: + return subprocess.check_output( + ["git", "diff", "origin/main...HEAD"], text=True, stderr=subprocess.DEVNULL + ) + except Exception: + return "No diff could be automatically extracted via git commands." + + +async def main(): + args = parse_args() + + # Read prompt file and pre-hydrate with the exact PR code diff + base_prompt = Path(args.prompt).read_text() + diff_text = get_pr_diff() + prompt = ( + f"{base_prompt}\n\n## Pull Request Git Diff\n" + f"Here is the exact code diff for this pull request:\n```diff\n{diff_text}\n```" + ) + + # General coordinator instructions for the reviewer agent. + system_instructions = ( + "You are a code review assistant. Use your available skills to perform " + "reviews on pull requests." + ) + + # Create the default endpoint picking up GEMINI_API_KEY from the environment. + endpoint = GeminiAPIEndpoint() + + # Initialize the Antigravity Agent in read-only mode for security. + # Register the review-pr skill from the local reviewbot folder. + # Provide a comprehensive prioritized cascade across Gemini 3 models + # to automatically fall back if any model hits free-tier quota limits (429) + # or temporary unavailability. + config = LocalAgentConfig( + models=[ + ModelTarget(name="gemini-3.5-flash", endpoint=endpoint), + ModelTarget(name="gemini-3.1-pro-preview", endpoint=endpoint), + ModelTarget(name="gemini-3.1-flash-lite", endpoint=endpoint), + ModelTarget(name="gemini-3-pro-preview", endpoint=endpoint), + ModelTarget(name="gemini-flash-latest", endpoint=endpoint), + ModelTarget(name="gemini-pro-latest", endpoint=endpoint), + ], + system_instructions=system_instructions, + skills_paths=[str(Path(__file__).parent / "skills" / "review-pr" / "SKILL.md")], + capabilities=CapabilitiesConfig( + enabled_tools=BuiltinTools.read_only(), + ), + ) + + async with Agent(config) as agent: + response = await agent.chat(prompt) + report = await response.text() + + print("--- REVIEW REPORT GENERATED ---") + print(report) + + # TODO: Use GITHUB_TOKEN to post the report back to the PR comments. + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/tools/private/reviewbot/prompt.txt b/tools/private/reviewbot/prompt.txt new file mode 100644 index 0000000000..e406164af1 --- /dev/null +++ b/tools/private/reviewbot/prompt.txt @@ -0,0 +1,3 @@ +Use the review-pr skill to review the files modified in this pull request. +Summarize your findings and suggest specific, actionable improvements. +Group your findings into clear, descriptive nits or suggestions. diff --git a/tools/private/reviewbot/skills/review-pr/SKILL.md b/tools/private/reviewbot/skills/review-pr/SKILL.md new file mode 100644 index 0000000000..915a526686 --- /dev/null +++ b/tools/private/reviewbot/skills/review-pr/SKILL.md @@ -0,0 +1,54 @@ +--- +name: review-pr +description: Perform a read-only code review on a pull request. +--- + +# review-pr + +IMPORTANT: The exact git diff for the pull request is pre-provided right in your prompt. Analyze this provided diff directly in a single pass without calling exploratory directory listing or file reading tools unless you specifically need surrounding lines of context from a modified file. + +You are an expert Starlark, Python, and Bazel code reviewer. Analyze the changed files for +correctness, edge cases, and performance. Focus strictly on logical +correctness, concurrency safety, system architecture, performance bottlenecks, +and resource management. Do not comment on style nits or formatting issues +that an automated formatter can handle. Be constructive and concise. + +For every issue or improvement you identify, you MUST output the finding in the +GitHub Actions workflow command warning format. Specify the exact file path +and line numbers that the comment applies to. + +Format each finding exactly as a single line to stdout matching this template: +`::warning file={file_path},line={line_number},endLine={end_line},title={category}::{comment_body}` + +Where: +* `file_path` is the relative file path from the repository root. +* `line_number` is the starting line number in the file where the comment applies. +* `end_line` is the ending line number in the file where the comment applies (equal to line_number if the issue is on a single line). +* `category` is a short tag for the type of issue (e.g., "Error Handling", "Correctness", "Performance"). +* `comment_body` is your constructive and concise feedback. + +Do not write any markdown commentary outside of these GHA command formatted lines. + +Follow these checklists during your review: + +### General Quality & Architecture Checklist +* **PR Description Audit**: Verify the description contains the Why + (business/technical reason), a brief high level overview of changes, + Issue/Bug Link, and explicit Testing Evidence. +* **Separation of Concerns**: Suggest extracting large hardcoded data structures + (e.g., massive templates, complex regexes) to resource files. +* **Logic Correctness**: Verify calculations, negative values, division-by-zero, + and null safety before member access. +* **Error Handling**: Flag silent failures (e.g., empty except blocks) and + unconditional defaults that override configs. +* **Deterministic Operations**: Sort collections/keys to guarantee + reproducible/deterministic execution. + +### Skeptical Critic (Adversarial Specialist Review) +* **Dynamic Filtering**: Filter the PR diff and run only the specialist checks + that have relevant files changed (e.g. skip the C++ checks if only Python + files are modified). +* **Specialist Review Pillars**: Run parallel audits focusing on: + 1. Crash Regression: Null safety and resource lifecycle. + 2. Performance & Latency: Thread bottlenecks, locks, and network calls. + 3. Test Integrity: Coverage validity, change detectors defense. diff --git a/tools/private/sync_downloader_configs.py b/tools/private/sync_downloader_configs.py new file mode 100755 index 0000000000..7e8527b989 --- /dev/null +++ b/tools/private/sync_downloader_configs.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 + +"""Synchronizes downloader_config.cfg across subworkspaces.""" + +import sys +from pathlib import Path + + +def main(): + repo_root = Path(__file__).resolve().parent.parent.parent + canonical = repo_root / "downloader_config.cfg" + + subworkspaces = [ + repo_root / "gazelle" / "downloader_config.cfg", + repo_root / "sphinxdocs" / "downloader_config.cfg", + ] + + with open(canonical, "r", encoding="utf-8") as f: + canonical_content = f.read() + + changed = False + for sub in subworkspaces: + if sub.exists(): + with open(sub, "r", encoding="utf-8") as f: + old_content = f.read() + if old_content != canonical_content: + with open(sub, "w", encoding="utf-8") as f: + f.write(canonical_content) + print(f"Updated {sub.relative_to(repo_root)}") + changed = True + + if changed: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/tools/private/update_deps/BUILD.bazel b/tools/private/update_deps/BUILD.bazel index beecf82189..c3f94c9814 100644 --- a/tools/private/update_deps/BUILD.bazel +++ b/tools/private/update_deps/BUILD.bazel @@ -34,10 +34,10 @@ py_binary( name = "update_coverage_deps", srcs = ["update_coverage_deps.py"], data = [ - "//python/private:coverage_deps", + "//python/private:coverage_deps_filegroup", ], env = { - "UPDATE_FILE": "$(rlocationpath //python/private:coverage_deps)", + "UPDATE_FILE": "$(rlocationpath //python/private:coverage_deps_filegroup)", }, imports = ["../../.."], deps = [ diff --git a/tools/private/update_deps/update_coverage_deps.py b/tools/private/update_deps/update_coverage_deps.py index bbff67e927..fcb44fcc7c 100755 --- a/tools/private/update_deps/update_coverage_deps.py +++ b/tools/private/update_deps/update_coverage_deps.py @@ -20,11 +20,8 @@ # NOTE @aignas 2023-01-09: We should only depend on core Python 3 packages. import argparse -import difflib import json import os -import pathlib -import sys import textwrap from collections import defaultdict from dataclasses import dataclass @@ -38,14 +35,18 @@ _supported_platforms = { # Windows is unsupported right now # "win_amd64": "x86_64-pc-windows-msvc", + "manylinux1_x86_64": "x86_64-unknown-linux-gnu", "manylinux2014_x86_64": "x86_64-unknown-linux-gnu", "manylinux2014_aarch64": "aarch64-unknown-linux-gnu", "macosx_11_0_arm64": "aarch64-apple-darwin", "macosx_10_9_x86_64": "x86_64-apple-darwin", + "macosx_10_13_x86_64": "x86_64-apple-darwin", + ("t", "manylinux1_x86_64"): "x86_64-unknown-linux-gnu-freethreaded", ("t", "manylinux2014_x86_64"): "x86_64-unknown-linux-gnu-freethreaded", ("t", "manylinux2014_aarch64"): "aarch64-unknown-linux-gnu-freethreaded", ("t", "macosx_11_0_arm64"): "aarch64-apple-darwin-freethreaded", ("t", "macosx_10_9_x86_64"): "x86_64-apple-darwin-freethreaded", + ("t", "macosx_10_13_x86_64"): "x86_64-apple-darwin-freethreaded", } @@ -143,7 +144,7 @@ def _parse_args() -> argparse.Namespace: "--py", nargs="+", type=str, - default=["cp38", "cp39", "cp310", "cp311", "cp312", "cp313"], + default=["cp39", "cp310", "cp311", "cp312", "cp313", "cp314"], help="Supported python versions", ) parser.add_argument( @@ -179,7 +180,7 @@ def main(): if u["python_version"] not in args.py: continue - if f'_{u["python_version"]}m_' in u["filename"]: + if f"_{u['python_version']}m_" in u["filename"]: continue platforms = _get_platforms( diff --git a/tools/private/update_deps/update_file.py b/tools/private/update_deps/update_file.py index ab3e8a817e..cbf4c32bd1 100644 --- a/tools/private/update_deps/update_file.py +++ b/tools/private/update_deps/update_file.py @@ -17,10 +17,8 @@ This is reused in other files updating coverage deps and pip deps. """ -import argparse import difflib import pathlib -import sys def _writelines(path: pathlib.Path, out: str): diff --git a/tools/private/update_deps/update_file_test.py b/tools/private/update_deps/update_file_test.py index 01c6ec74b0..a3cb1c0a6d 100644 --- a/tools/private/update_deps/update_file_test.py +++ b/tools/private/update_deps/update_file_test.py @@ -30,7 +30,7 @@ def test_replace_simple(self): After the snippet """ - snippet = "Replaced" + snippet = "Replaced" # noqa: F841 got = replace_snippet( current=current, snippet="Replaced", diff --git a/tools/private/update_deps/update_pip_deps.py b/tools/private/update_deps/update_pip_deps.py index 1034382f0d..406697bc4d 100755 --- a/tools/private/update_deps/update_pip_deps.py +++ b/tools/private/update_deps/update_pip_deps.py @@ -96,7 +96,7 @@ def _get_deps(report: dict) -> list[Dep]: url=dep["download_info"]["url"], sha256=dep["download_info"]["archive_info"]["hash"][len("sha256=") :], ) - except: + except Exception: debug_dep = textwrap.indent(json.dumps(dep, indent=4), " " * 4) print(f"Could not parse the response from 'pip':\n{debug_dep}") raise diff --git a/tools/private/zipapp/BUILD.bazel b/tools/private/zipapp/BUILD.bazel new file mode 100644 index 0000000000..45bd126c6e --- /dev/null +++ b/tools/private/zipapp/BUILD.bazel @@ -0,0 +1,51 @@ +load("//python:py_library.bzl", "py_library") +load("//python/private:py_interpreter_program.bzl", "py_interpreter_program") # buildifier: disable=bzl-visibility +load("//python/private:visibility.bzl", "NOT_ACTUALLY_PUBLIC") # buildifier: disable=bzl-visibility + +package( + default_visibility = ["//:__subpackages__"], +) + +py_interpreter_program( + name = "zipper", + main = "zipper.py", + # Not actually public. Only public so rules_python-generated toolchains + # are able to reference it. + visibility = NOT_ACTUALLY_PUBLIC, +) + +py_library( + name = "zipper_lib", + srcs = ["zipper.py"], +) + +py_interpreter_program( + name = "exe_zip_maker", + main = "exe_zip_maker.py", + # Not actually public. Only public so rules_python-generated toolchains + # are able to reference it. + visibility = NOT_ACTUALLY_PUBLIC, +) + +py_library( + name = "exe_zip_maker_lib", + srcs = ["exe_zip_maker.py"], +) + +py_interpreter_program( + name = "zip_main_maker", + main = "zip_main_maker.py", + # Not actually public. Only public so rules_python-generated toolchains + # are able to reference it. + visibility = NOT_ACTUALLY_PUBLIC, +) + +py_library( + name = "zip_main_maker_lib", + srcs = ["zip_main_maker.py"], +) + +filegroup( + name = "distribution", + srcs = glob(["**"]), +) diff --git a/tools/private/zipapp/exe_zip_maker.py b/tools/private/zipapp/exe_zip_maker.py new file mode 100644 index 0000000000..29391c86da --- /dev/null +++ b/tools/private/zipapp/exe_zip_maker.py @@ -0,0 +1,40 @@ +import hashlib +import os +import shutil +import stat +import sys + +BLOCK_SIZE = 256 * 1024 + + +def create_exe_zip(preamble_path, zip_path, output_path): + sha256_hash = hashlib.sha256() + with open(zip_path, "rb", buffering=BLOCK_SIZE) as f: + for byte_block in iter(lambda: f.read(BLOCK_SIZE), b""): + sha256_hash.update(byte_block) + zip_hash = sha256_hash.hexdigest() + + with open(preamble_path, "rb") as f: + preamble_content = f.read() + + preamble_content = preamble_content.replace(b"%ZIP_HASH%", zip_hash.encode("utf-8")) + + with open(output_path, "wb") as out_f: + out_f.write(preamble_content) + with open(zip_path, "rb") as zip_f: + shutil.copyfileobj(zip_f, out_f, length=BLOCK_SIZE) + + st = os.stat(output_path) + os.chmod(output_path, st.st_mode | stat.S_IEXEC) + + +def main(): + if len(sys.argv) != 4: + print(f"Usage: {sys.argv[0]} ", file=sys.stderr) + sys.exit(1) + + create_exe_zip(sys.argv[1], sys.argv[2], sys.argv[3]) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/private/zipapp/zip_main_maker.py b/tools/private/zipapp/zip_main_maker.py new file mode 100644 index 0000000000..ae112ffc66 --- /dev/null +++ b/tools/private/zipapp/zip_main_maker.py @@ -0,0 +1,94 @@ +"""Creates the __main__.py for a zipapp by populating a template. + +This program also calculates a hash of the application files to include in +the template, which allows making the extraction directory unique to the +content of the zipapp. +""" + +import argparse +import hashlib +import os + +BLOCK_SIZE = 256 * 1024 + + +def create_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(fromfile_prefix_chars="@") + parser.add_argument("--template", required=True) + parser.add_argument("--output", required=True) + parser.add_argument("--substitution", action="append", default=[]) + parser.add_argument( + "--hash_files_manifest", + required=True, + help="A file containing lines in rf-XXX formats (rf-empty, rf-file, rf-symlink, etc.)", + ) + return parser + + +def compute_inputs_hash(manifest_path: str) -> str: + h = hashlib.sha256() + with open(manifest_path, "r", encoding="utf-8") as f: + manifest_lines = f.read().splitlines() + + # Sort lines for determinism. Hash the paths (to capture structure) and the + # content. + for line in sorted(manifest_lines): + type_, _, rest = line.partition("|") + h.update(rest.encode("utf-8")) + parts = rest.split("|") + + if type_ == "rf-empty": + continue + if type_ == "symlink": + # The symlink path and the target it points to + # are captured by hashing the entire line above. + continue + + is_symlink_str = parts[0] + path = parts[-1] + + if is_symlink_str == "-1": + is_symlink = not os.path.exists(path) + else: + is_symlink = is_symlink_str == "1" + + if is_symlink: + h.update(os.readlink(path).encode("utf-8")) + else: + with open(path, "rb") as f: + while True: + chunk = f.read(BLOCK_SIZE) + if not chunk: + break + h.update(chunk) + + return h.hexdigest() + + +def expand_template(template_path: str, output_path: str, substitutions: dict) -> None: + with open(template_path, "r", encoding="utf-8") as f: + content = f.read() + + for key, val in substitutions.items(): + content = content.replace(key, val) + + with open(output_path, "w", encoding="utf-8") as f: + f.write(content) + + +def main(): + parser = create_parser() + args = parser.parse_args() + + app_hash = compute_inputs_hash(args.hash_files_manifest) + + substitutions = {"%APP_HASH%": app_hash} + for s in args.substitution: + key, val = s.split("=", 1) + substitutions[key] = val + + expand_template(args.template, args.output, substitutions) + + +if __name__ == "__main__": + main() diff --git a/tools/private/zipapp/zipper.py b/tools/private/zipapp/zipper.py new file mode 100644 index 0000000000..5a8eb8c6fd --- /dev/null +++ b/tools/private/zipapp/zipper.py @@ -0,0 +1,286 @@ +import argparse +import os +import shutil +import sys +import zipfile +from os.path import dirname + +# Unix permission bit for symlink (S_IFLNK) +# S_IFLNK is usually 0o120000 +S_IFLNK = 0o120000 + + +def unix_join(*parts): + return "/".join(parts) + + +def _get_zip_runfiles_path( + path, workspace_name, legacy_external_runfiles, runfiles_dir +): + if legacy_external_runfiles and path.startswith("external/"): + path = path[len("external/") :] + elif path.startswith("../"): + path = path[3:] + else: + path = unix_join(workspace_name, path) + return unix_join(runfiles_dir, path) + + +def _parse_entry( + line, + line_idx, + workspace_name, + legacy_external_runfiles, + runfiles_dir, +): + line = line.strip() + if not line: + return None + + parts = line.split("|") + type_ = parts[0] + + if type_ == "regular": + _, is_symlink_str, zip_path, content_path = parts + elif type_ == "rf-empty": + _, runfile_path = parts + zip_path = _get_zip_runfiles_path( + runfile_path, workspace_name, legacy_external_runfiles, runfiles_dir + ) + content_path = None # Empty file + is_symlink_str = "0" + elif type_ == "rf-file": + _, is_symlink_str, runfile_path, content_path = parts + zip_path = _get_zip_runfiles_path( + runfile_path, workspace_name, legacy_external_runfiles, runfiles_dir + ) + elif type_ == "rf-symlink": + _, is_symlink_str, runfile_path, content_path = parts + zip_path = unix_join(runfiles_dir, workspace_name, runfile_path) + elif type_ == "rf-root-symlink": + _, is_symlink_str, runfile_path, content_path = parts + zip_path = unix_join(runfiles_dir, runfile_path) + elif type_ == "symlink": + _, runfile_path, link_to_rf_path = parts + zip_path = unix_join(runfiles_dir, runfile_path) + link_to_rf_path = unix_join(runfiles_dir, link_to_rf_path) + content_path = os.path.relpath(link_to_rf_path, start=dirname(zip_path)) + is_symlink_str = "2" + else: + raise ValueError( + f"Error: Unknown entry type or invalid format at line {line_idx + 1}: {line}" + ) + + return type_, is_symlink_str, zip_path, content_path + + +def read_manifest( + manifest_path, workspace_name, legacy_external_runfiles, runfiles_dir +): + with open(manifest_path, "r") as f: + entries = [] + for line_idx, line in enumerate(f): + try: + entry = _parse_entry( + line, + line_idx, + workspace_name, + legacy_external_runfiles, + runfiles_dir, + ) + if entry: + entries.append(entry) + except ValueError as e: + e.add_note(f"Error processing line {line_idx + 1}: {line.strip()}") + raise + + # Sort symlink entries first so they have precedence. + # Then sort by zip path + entries.sort(key=lambda x: (x[2], 0 if x[0] == "symlink" else 1)) + return entries + + +def convert_symlink_target(path, platform_pathsep): + """Converts the path a symlink points to the target-platform format. + + On Windows, relative symlinks must use backslashes. + """ + if platform_pathsep == "/": + # Convert Windows to Unix + return path.replace("\\", platform_pathsep) + else: + # Convert Unix to Windows + return path.replace("/", "\\") + + +# Zip files use forward slash for the entries, even on Windows +def normalize_zip_path(path): + return path.replace("\\", "/") + + +def _write_entry(zf, entry, compress_type, seen, platform_pathsep): + type_, is_symlink_str, zip_path, content_path = entry + # Normalize slashes, otherwise the `seen` logic doesn't + # work correctly. + zip_path = normalize_zip_path(zip_path) + if zip_path in seen: + # This can occur because symlink entries have precedence + # over non-symlink entries. + return + seen.add(zip_path) + + if type_ == "rf-empty": + zi = zipfile.ZipInfo(zip_path) + zi.date_time = (1980, 1, 1, 0, 0, 0) + zi.create_system = 3 # Unix + zi.compress_type = compress_type + # Create empty file + zi.external_attr = (0o644 & 0xFFFF) << 16 + zf.writestr(zi, "") + return + if type_ == "symlink": + zi = zipfile.ZipInfo(zip_path) + zi.date_time = (1980, 1, 1, 0, 0, 0) + zi.create_system = 3 # Unix + zi.compress_type = compress_type + target = convert_symlink_target(content_path, platform_pathsep) + # Set permissions to 777 for symlink (standard) + zi.external_attr = (S_IFLNK | 0o777) << 16 + zf.writestr(zi, target) + return + + if is_symlink_str == "-1": + if not os.path.exists(content_path): + is_symlink_str = "1" + else: + is_symlink_str = "0" + + is_symlink = is_symlink_str == "1" + + if is_symlink: + zi = zipfile.ZipInfo(zip_path) + zi.date_time = (1980, 1, 1, 0, 0, 0) + zi.create_system = 3 # Unix + zi.compress_type = compress_type + target = convert_symlink_target(os.readlink(content_path), platform_pathsep) + # Set permissions to 777 for symlink (standard) + zi.external_attr = (S_IFLNK | 0o777) << 16 + zf.writestr(zi, target) + else: + st = os.stat(content_path) + zi = zipfile.ZipInfo(zip_path) + zi.date_time = (1980, 1, 1, 0, 0, 0) + zi.create_system = 3 # Unix + zi.compress_type = compress_type + # Preserve permissions, otherwise execute is dropped. + zi.external_attr = (st.st_mode & 0xFFFF) << 16 + with open(content_path, "rb") as src, zf.open(zi, "w") as dst: + shutil.copyfileobj(src, dst) + + +def create_zip( + *, + manifest_path, + output_zip, + compress_level, + workspace_name, + legacy_external_runfiles, + runfiles_dir, + platform_pathsep, +): + compress_type = zipfile.ZIP_STORED if compress_level == 0 else zipfile.ZIP_DEFLATED + zf_level = compress_level if compress_level != 0 else None + + entries = read_manifest( + manifest_path, workspace_name, legacy_external_runfiles, runfiles_dir + ) + + seen = set() + with zipfile.ZipFile( + output_zip, "w", compress_type, allowZip64=True, compresslevel=zf_level + ) as zf: + for entry in entries: + _write_entry(zf, entry, compress_type, seen, platform_pathsep) + + +def main(): + parser = argparse.ArgumentParser(description="Create a zip file from a manifest.") + parser.add_argument( + "manifest", + help=""" +Path to the manifest file. Lines have one of the following formats: + +1. `regular|is_symlink|zip_path|content_path`: This form stores the `zip_path` + in the zip, whose content is taken from `content_path` + +2. `rf-empty|runfile_path`: A `runfiles.empty_filenames` value. The stored + zip path is computed from `runfile_path` + +3. `rf-file|is_symlink|runfile_path|content_path`: Store a file in + the zip. The zip path is computed from `runfile_path`. + +4. `rf-symlink|is_symlink|runfile_symlink_path|content_path`: Store a + main-repo-relative path in the zip. + +5. `rf-root-symlink|is_symlink|runfile_root_path|content_path`: Store a + runfiles-root-relative path in the zip. + +6. `symlink|runfile_root_path|link_to_path_rf_path`: Store a symlink that + stores a relative path from `runfile_root_path` to `link_to_rf_path` + +In all cases, `is_symlink` has the following values: +* `1` means it should be stored as a symlink whose value is read + (using `readlink()`) from `content_path`. +* `0` means to store it as a regular file, read from `content_path` +* `-1` occurs with Bazel 7 (because it lacks `File.is_symlink`), which means + to infer whether it's a symlink (files to be stored as symlinks can be + determined by looking for symlinks that point to non-existent files). + +For runfiles entries, they have `--runfiles-dir` prepended to their computed +zip path. + +Compute `zip_path` from `runfile_path`: Computing the final zip path for +runfiles entries is a bit complicated, but boils down to computing what the +runfiles-root-relative path would be, with `--legacy-external-runfiles` taken +into account. +""", + ) + parser.add_argument("output", help="Path to the output zip file.") + parser.add_argument( + "--compression", + type=int, + default=0, + help="Compression level (0 for stored, others for deflated)", + ) + parser.add_argument("--workspace-name", default="", help="Name of the workspace") + parser.add_argument( + "--legacy-external-runfiles", + default="0", + choices=["0", "1"], + help="Whether to use legacy external runfiles behavior", + ) + parser.add_argument( + "--runfiles-dir", default="runfiles", help="Name of the runfiles directory" + ) + parser.add_argument( + "--target-platform-pathsep", help="The path separator for the target platform" + ) + args = parser.parse_args() + + try: + create_zip( + manifest_path=args.manifest, + output_zip=args.output, + compress_level=args.compression, + workspace_name=args.workspace_name, + legacy_external_runfiles=args.legacy_external_runfiles == "1", + runfiles_dir=args.runfiles_dir, + platform_pathsep=args.target_platform_pathsep, + ) + except Exception as e: + e.add_note(f"Error creating zip {args.output}") + raise + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/publish/requirements_darwin.txt b/tools/publish/requirements_darwin.txt index 39248d4675..6d8de8b4c9 100644 --- a/tools/publish/requirements_darwin.txt +++ b/tools/publish/requirements_darwin.txt @@ -6,9 +6,9 @@ backports-tarfile==1.2.0 \ --hash=sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34 \ --hash=sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991 # via jaraco-context -certifi==2025.8.3 \ - --hash=sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407 \ - --hash=sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5 +certifi==2025.10.5 \ + --hash=sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de \ + --hash=sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43 # via requests charset-normalizer==3.4.3 \ --hash=sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91 \ @@ -91,9 +91,9 @@ charset-normalizer==3.4.3 \ --hash=sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c \ --hash=sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9 # via requests -docutils==0.22 \ - --hash=sha256:4ed966a0e96a0477d852f7af31bdcb3adc049fbb35ccba358c2ea8a03287615e \ - --hash=sha256:ba9d57750e92331ebe7c08a1bbf7a7f8143b86c476acd51528b042216a6aad0f +docutils==0.22.2 \ + --hash=sha256:9fdb771707c8784c8f2728b67cb2c691305933d68137ef95a75db5f4dfbc213d \ + --hash=sha256:b0e98d679283fc3bb0ead8a5da7f501baa632654e7056e9c5846842213d674d8 # via readme-renderer idna==3.10 \ --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ @@ -121,9 +121,9 @@ keyring==25.6.0 \ --hash=sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66 \ --hash=sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd # via twine -markdown-it-py==3.0.0 \ - --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ - --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb +markdown-it-py==4.0.0 \ + --hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \ + --hash=sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3 # via rich mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ @@ -177,9 +177,9 @@ readme-renderer==44.0 \ --hash=sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151 \ --hash=sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1 # via twine -requests==2.32.5 \ - --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ - --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf +requests==2.33.0 \ + --hash=sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b \ + --hash=sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652 # via # requests-toolbelt # twine @@ -199,9 +199,9 @@ twine==5.1.1 \ --hash=sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997 \ --hash=sha256:9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db # via -r tools/publish/requirements.in -urllib3==2.5.0 \ - --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ - --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc +urllib3==2.6.3 \ + --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ + --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 # via # requests # twine diff --git a/tools/publish/requirements_linux.txt b/tools/publish/requirements_linux.txt index c078a0ed61..8865eb3ecb 100644 --- a/tools/publish/requirements_linux.txt +++ b/tools/publish/requirements_linux.txt @@ -6,9 +6,9 @@ backports-tarfile==1.2.0 \ --hash=sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34 \ --hash=sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991 # via jaraco-context -certifi==2025.8.3 \ - --hash=sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407 \ - --hash=sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5 +certifi==2025.10.5 \ + --hash=sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de \ + --hash=sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43 # via requests cffi==2.0.0 \ --hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \ @@ -177,48 +177,60 @@ charset-normalizer==3.4.3 \ --hash=sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c \ --hash=sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9 # via requests -cryptography==45.0.7 \ - --hash=sha256:06ce84dc14df0bf6ea84666f958e6080cdb6fe1231be2a51f3fc1267d9f3fb34 \ - --hash=sha256:16ede8a4f7929b4b7ff3642eba2bf79aa1d71f24ab6ee443935c0d269b6bc513 \ - --hash=sha256:18fcf70f243fe07252dcb1b268a687f2358025ce32f9f88028ca5c364b123ef5 \ - --hash=sha256:1993a1bb7e4eccfb922b6cd414f072e08ff5816702a0bdb8941c247a6b1b287c \ - --hash=sha256:1f3d56f73595376f4244646dd5c5870c14c196949807be39e79e7bd9bac3da63 \ - --hash=sha256:258e0dff86d1d891169b5af222d362468a9570e2532923088658aa866eb11130 \ - --hash=sha256:2f641b64acc00811da98df63df7d59fd4706c0df449da71cb7ac39a0732b40ae \ - --hash=sha256:3808e6b2e5f0b46d981c24d79648e5c25c35e59902ea4391a0dcb3e667bf7443 \ - --hash=sha256:3994c809c17fc570c2af12c9b840d7cea85a9fd3e5c0e0491f4fa3c029216d59 \ - --hash=sha256:3be4f21c6245930688bd9e162829480de027f8bf962ede33d4f8ba7d67a00cee \ - --hash=sha256:465ccac9d70115cd4de7186e60cfe989de73f7bb23e8a7aa45af18f7412e75bf \ - --hash=sha256:48c41a44ef8b8c2e80ca4527ee81daa4c527df3ecbc9423c41a420a9559d0e27 \ - --hash=sha256:4a862753b36620af6fc54209264f92c716367f2f0ff4624952276a6bbd18cbde \ - --hash=sha256:4b1654dfc64ea479c242508eb8c724044f1e964a47d1d1cacc5132292d851971 \ - --hash=sha256:4bd3e5c4b9682bc112d634f2c6ccc6736ed3635fc3319ac2bb11d768cc5a00d8 \ - --hash=sha256:577470e39e60a6cd7780793202e63536026d9b8641de011ed9d8174da9ca5339 \ - --hash=sha256:67285f8a611b0ebc0857ced2081e30302909f571a46bfa7a3cc0ad303fe015c6 \ - --hash=sha256:7285a89df4900ed3bfaad5679b1e668cb4b38a8de1ccbfc84b05f34512da0a90 \ - --hash=sha256:81823935e2f8d476707e85a78a405953a03ef7b7b4f55f93f7c2d9680e5e0691 \ - --hash=sha256:8978132287a9d3ad6b54fcd1e08548033cc09dc6aacacb6c004c73c3eb5d3ac3 \ - --hash=sha256:a20e442e917889d1a6b3c570c9e3fa2fdc398c20868abcea268ea33c024c4083 \ - --hash=sha256:a24ee598d10befaec178efdff6054bc4d7e883f615bfbcd08126a0f4931c83a6 \ - --hash=sha256:b04f85ac3a90c227b6e5890acb0edbaf3140938dbecf07bff618bf3638578cf1 \ - --hash=sha256:b6a0e535baec27b528cb07a119f321ac024592388c5681a5ced167ae98e9fff3 \ - --hash=sha256:bef32a5e327bd8e5af915d3416ffefdbe65ed975b646b3805be81b23580b57b8 \ - --hash=sha256:bfb4c801f65dd61cedfc61a83732327fafbac55a47282e6f26f073ca7a41c3b2 \ - --hash=sha256:c13b1e3afd29a5b3b2656257f14669ca8fa8d7956d509926f0b130b600b50ab7 \ - --hash=sha256:c987dad82e8c65ebc985f5dae5e74a3beda9d0a2a4daf8a1115f3772b59e5141 \ - --hash=sha256:ce7a453385e4c4693985b4a4a3533e041558851eae061a58a5405363b098fcd3 \ - --hash=sha256:d0c5c6bac22b177bf8da7435d9d27a6834ee130309749d162b26c3105c0795a9 \ - --hash=sha256:d97cf502abe2ab9eff8bd5e4aca274da8d06dd3ef08b759a8d6143f4ad65d4b4 \ - --hash=sha256:dad43797959a74103cb59c5dac71409f9c27d34c8a05921341fb64ea8ccb1dd4 \ - --hash=sha256:dd342f085542f6eb894ca00ef70236ea46070c8a13824c6bde0dfdcd36065b9b \ - --hash=sha256:de58755d723e86175756f463f2f0bddd45cc36fbd62601228a3f8761c9f58252 \ - --hash=sha256:f3df7b3d0f91b88b2106031fd995802a2e9ae13e02c36c1fc075b43f420f3a17 \ - --hash=sha256:f5414a788ecc6ee6bc58560e85ca624258a55ca434884445440a810796ea0e0b \ - --hash=sha256:fa26fa54c0a9384c27fcdc905a2fb7d60ac6e47d14bc2692145f2b3b1e2cfdbd +cryptography==46.0.7 \ + --hash=sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65 \ + --hash=sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832 \ + --hash=sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067 \ + --hash=sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de \ + --hash=sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4 \ + --hash=sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0 \ + --hash=sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b \ + --hash=sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968 \ + --hash=sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef \ + --hash=sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b \ + --hash=sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4 \ + --hash=sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3 \ + --hash=sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308 \ + --hash=sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e \ + --hash=sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163 \ + --hash=sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f \ + --hash=sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee \ + --hash=sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77 \ + --hash=sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85 \ + --hash=sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99 \ + --hash=sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7 \ + --hash=sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83 \ + --hash=sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85 \ + --hash=sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006 \ + --hash=sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb \ + --hash=sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e \ + --hash=sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba \ + --hash=sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325 \ + --hash=sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d \ + --hash=sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1 \ + --hash=sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1 \ + --hash=sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2 \ + --hash=sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0 \ + --hash=sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455 \ + --hash=sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842 \ + --hash=sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457 \ + --hash=sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15 \ + --hash=sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2 \ + --hash=sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c \ + --hash=sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb \ + --hash=sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5 \ + --hash=sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4 \ + --hash=sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902 \ + --hash=sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246 \ + --hash=sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022 \ + --hash=sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f \ + --hash=sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e \ + --hash=sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298 \ + --hash=sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce # via secretstorage -docutils==0.22 \ - --hash=sha256:4ed966a0e96a0477d852f7af31bdcb3adc049fbb35ccba358c2ea8a03287615e \ - --hash=sha256:ba9d57750e92331ebe7c08a1bbf7a7f8143b86c476acd51528b042216a6aad0f +docutils==0.22.2 \ + --hash=sha256:9fdb771707c8784c8f2728b67cb2c691305933d68137ef95a75db5f4dfbc213d \ + --hash=sha256:b0e98d679283fc3bb0ead8a5da7f501baa632654e7056e9c5846842213d674d8 # via readme-renderer idna==3.10 \ --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ @@ -252,9 +264,9 @@ keyring==25.6.0 \ --hash=sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66 \ --hash=sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd # via twine -markdown-it-py==3.0.0 \ - --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ - --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb +markdown-it-py==4.0.0 \ + --hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \ + --hash=sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3 # via rich mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ @@ -312,9 +324,9 @@ readme-renderer==44.0 \ --hash=sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151 \ --hash=sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1 # via twine -requests==2.32.5 \ - --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ - --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf +requests==2.33.0 \ + --hash=sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b \ + --hash=sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652 # via # requests-toolbelt # twine @@ -338,9 +350,9 @@ twine==5.1.1 \ --hash=sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997 \ --hash=sha256:9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db # via -r tools/publish/requirements.in -urllib3==2.5.0 \ - --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ - --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc +urllib3==2.6.3 \ + --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ + --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 # via # requests # twine diff --git a/tools/publish/requirements_universal.txt b/tools/publish/requirements_universal.txt index a3a3f23a51..4dbcc92abc 100644 --- a/tools/publish/requirements_universal.txt +++ b/tools/publish/requirements_universal.txt @@ -6,9 +6,9 @@ backports-tarfile==1.2.0 ; python_full_version < '3.12' \ --hash=sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34 \ --hash=sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991 # via jaraco-context -certifi==2025.8.3 \ - --hash=sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407 \ - --hash=sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5 +certifi==2025.10.5 \ + --hash=sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de \ + --hash=sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43 # via requests cffi==2.0.0 ; platform_python_implementation != 'PyPy' and sys_platform == 'linux' \ --hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \ @@ -160,48 +160,60 @@ charset-normalizer==3.4.3 \ --hash=sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c \ --hash=sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9 # via requests -cryptography==45.0.7 ; sys_platform == 'linux' \ - --hash=sha256:06ce84dc14df0bf6ea84666f958e6080cdb6fe1231be2a51f3fc1267d9f3fb34 \ - --hash=sha256:16ede8a4f7929b4b7ff3642eba2bf79aa1d71f24ab6ee443935c0d269b6bc513 \ - --hash=sha256:18fcf70f243fe07252dcb1b268a687f2358025ce32f9f88028ca5c364b123ef5 \ - --hash=sha256:1993a1bb7e4eccfb922b6cd414f072e08ff5816702a0bdb8941c247a6b1b287c \ - --hash=sha256:1f3d56f73595376f4244646dd5c5870c14c196949807be39e79e7bd9bac3da63 \ - --hash=sha256:258e0dff86d1d891169b5af222d362468a9570e2532923088658aa866eb11130 \ - --hash=sha256:2f641b64acc00811da98df63df7d59fd4706c0df449da71cb7ac39a0732b40ae \ - --hash=sha256:3808e6b2e5f0b46d981c24d79648e5c25c35e59902ea4391a0dcb3e667bf7443 \ - --hash=sha256:3994c809c17fc570c2af12c9b840d7cea85a9fd3e5c0e0491f4fa3c029216d59 \ - --hash=sha256:3be4f21c6245930688bd9e162829480de027f8bf962ede33d4f8ba7d67a00cee \ - --hash=sha256:465ccac9d70115cd4de7186e60cfe989de73f7bb23e8a7aa45af18f7412e75bf \ - --hash=sha256:48c41a44ef8b8c2e80ca4527ee81daa4c527df3ecbc9423c41a420a9559d0e27 \ - --hash=sha256:4a862753b36620af6fc54209264f92c716367f2f0ff4624952276a6bbd18cbde \ - --hash=sha256:4b1654dfc64ea479c242508eb8c724044f1e964a47d1d1cacc5132292d851971 \ - --hash=sha256:4bd3e5c4b9682bc112d634f2c6ccc6736ed3635fc3319ac2bb11d768cc5a00d8 \ - --hash=sha256:577470e39e60a6cd7780793202e63536026d9b8641de011ed9d8174da9ca5339 \ - --hash=sha256:67285f8a611b0ebc0857ced2081e30302909f571a46bfa7a3cc0ad303fe015c6 \ - --hash=sha256:7285a89df4900ed3bfaad5679b1e668cb4b38a8de1ccbfc84b05f34512da0a90 \ - --hash=sha256:81823935e2f8d476707e85a78a405953a03ef7b7b4f55f93f7c2d9680e5e0691 \ - --hash=sha256:8978132287a9d3ad6b54fcd1e08548033cc09dc6aacacb6c004c73c3eb5d3ac3 \ - --hash=sha256:a20e442e917889d1a6b3c570c9e3fa2fdc398c20868abcea268ea33c024c4083 \ - --hash=sha256:a24ee598d10befaec178efdff6054bc4d7e883f615bfbcd08126a0f4931c83a6 \ - --hash=sha256:b04f85ac3a90c227b6e5890acb0edbaf3140938dbecf07bff618bf3638578cf1 \ - --hash=sha256:b6a0e535baec27b528cb07a119f321ac024592388c5681a5ced167ae98e9fff3 \ - --hash=sha256:bef32a5e327bd8e5af915d3416ffefdbe65ed975b646b3805be81b23580b57b8 \ - --hash=sha256:bfb4c801f65dd61cedfc61a83732327fafbac55a47282e6f26f073ca7a41c3b2 \ - --hash=sha256:c13b1e3afd29a5b3b2656257f14669ca8fa8d7956d509926f0b130b600b50ab7 \ - --hash=sha256:c987dad82e8c65ebc985f5dae5e74a3beda9d0a2a4daf8a1115f3772b59e5141 \ - --hash=sha256:ce7a453385e4c4693985b4a4a3533e041558851eae061a58a5405363b098fcd3 \ - --hash=sha256:d0c5c6bac22b177bf8da7435d9d27a6834ee130309749d162b26c3105c0795a9 \ - --hash=sha256:d97cf502abe2ab9eff8bd5e4aca274da8d06dd3ef08b759a8d6143f4ad65d4b4 \ - --hash=sha256:dad43797959a74103cb59c5dac71409f9c27d34c8a05921341fb64ea8ccb1dd4 \ - --hash=sha256:dd342f085542f6eb894ca00ef70236ea46070c8a13824c6bde0dfdcd36065b9b \ - --hash=sha256:de58755d723e86175756f463f2f0bddd45cc36fbd62601228a3f8761c9f58252 \ - --hash=sha256:f3df7b3d0f91b88b2106031fd995802a2e9ae13e02c36c1fc075b43f420f3a17 \ - --hash=sha256:f5414a788ecc6ee6bc58560e85ca624258a55ca434884445440a810796ea0e0b \ - --hash=sha256:fa26fa54c0a9384c27fcdc905a2fb7d60ac6e47d14bc2692145f2b3b1e2cfdbd +cryptography==46.0.7 ; sys_platform == 'linux' \ + --hash=sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65 \ + --hash=sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832 \ + --hash=sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067 \ + --hash=sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de \ + --hash=sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4 \ + --hash=sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0 \ + --hash=sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b \ + --hash=sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968 \ + --hash=sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef \ + --hash=sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b \ + --hash=sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4 \ + --hash=sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3 \ + --hash=sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308 \ + --hash=sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e \ + --hash=sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163 \ + --hash=sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f \ + --hash=sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee \ + --hash=sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77 \ + --hash=sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85 \ + --hash=sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99 \ + --hash=sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7 \ + --hash=sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83 \ + --hash=sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85 \ + --hash=sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006 \ + --hash=sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb \ + --hash=sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e \ + --hash=sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba \ + --hash=sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325 \ + --hash=sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d \ + --hash=sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1 \ + --hash=sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1 \ + --hash=sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2 \ + --hash=sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0 \ + --hash=sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455 \ + --hash=sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842 \ + --hash=sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457 \ + --hash=sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15 \ + --hash=sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2 \ + --hash=sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c \ + --hash=sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb \ + --hash=sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5 \ + --hash=sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4 \ + --hash=sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902 \ + --hash=sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246 \ + --hash=sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022 \ + --hash=sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f \ + --hash=sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e \ + --hash=sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298 \ + --hash=sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce # via secretstorage -docutils==0.22 \ - --hash=sha256:4ed966a0e96a0477d852f7af31bdcb3adc049fbb35ccba358c2ea8a03287615e \ - --hash=sha256:ba9d57750e92331ebe7c08a1bbf7a7f8143b86c476acd51528b042216a6aad0f +docutils==0.22.2 \ + --hash=sha256:9fdb771707c8784c8f2728b67cb2c691305933d68137ef95a75db5f4dfbc213d \ + --hash=sha256:b0e98d679283fc3bb0ead8a5da7f501baa632654e7056e9c5846842213d674d8 # via readme-renderer idna==3.10 \ --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ @@ -235,9 +247,9 @@ keyring==25.6.0 \ --hash=sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66 \ --hash=sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd # via twine -markdown-it-py==3.0.0 \ - --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ - --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb +markdown-it-py==4.0.0 \ + --hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \ + --hash=sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3 # via rich mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ @@ -299,9 +311,9 @@ readme-renderer==44.0 \ --hash=sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151 \ --hash=sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1 # via twine -requests==2.32.5 \ - --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ - --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf +requests==2.33.0 \ + --hash=sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b \ + --hash=sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652 # via # requests-toolbelt # twine @@ -325,9 +337,9 @@ twine==5.1.1 \ --hash=sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997 \ --hash=sha256:9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db # via -r tools/publish/requirements.in -urllib3==2.5.0 \ - --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ - --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc +urllib3==2.6.3 \ + --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ + --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 # via # requests # twine diff --git a/tools/publish/requirements_windows.txt b/tools/publish/requirements_windows.txt index b4eb83d1f1..029c6bef4d 100644 --- a/tools/publish/requirements_windows.txt +++ b/tools/publish/requirements_windows.txt @@ -6,9 +6,9 @@ backports-tarfile==1.2.0 \ --hash=sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34 \ --hash=sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991 # via jaraco-context -certifi==2025.8.3 \ - --hash=sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407 \ - --hash=sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5 +certifi==2025.10.5 \ + --hash=sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de \ + --hash=sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43 # via requests charset-normalizer==3.4.3 \ --hash=sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91 \ @@ -91,9 +91,9 @@ charset-normalizer==3.4.3 \ --hash=sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c \ --hash=sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9 # via requests -docutils==0.22 \ - --hash=sha256:4ed966a0e96a0477d852f7af31bdcb3adc049fbb35ccba358c2ea8a03287615e \ - --hash=sha256:ba9d57750e92331ebe7c08a1bbf7a7f8143b86c476acd51528b042216a6aad0f +docutils==0.22.2 \ + --hash=sha256:9fdb771707c8784c8f2728b67cb2c691305933d68137ef95a75db5f4dfbc213d \ + --hash=sha256:b0e98d679283fc3bb0ead8a5da7f501baa632654e7056e9c5846842213d674d8 # via readme-renderer idna==3.10 \ --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ @@ -121,9 +121,9 @@ keyring==25.6.0 \ --hash=sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66 \ --hash=sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd # via twine -markdown-it-py==3.0.0 \ - --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ - --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb +markdown-it-py==4.0.0 \ + --hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \ + --hash=sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3 # via rich mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ @@ -181,9 +181,9 @@ readme-renderer==44.0 \ --hash=sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151 \ --hash=sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1 # via twine -requests==2.32.5 \ - --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ - --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf +requests==2.33.0 \ + --hash=sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b \ + --hash=sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652 # via # requests-toolbelt # twine @@ -203,9 +203,9 @@ twine==5.1.1 \ --hash=sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997 \ --hash=sha256:9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db # via -r tools/publish/requirements.in -urllib3==2.5.0 \ - --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ - --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc +urllib3==2.6.3 \ + --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ + --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 # via # requests # twine diff --git a/tools/update_deleted_packages.sh b/tools/update_deleted_packages.sh index 17e33d182a..83bed91d16 100755 --- a/tools/update_deleted_packages.sh +++ b/tools/update_deleted_packages.sh @@ -23,17 +23,19 @@ # 2. For each of the directories, get all directories that contains a BUILD.bazel file. # 3. Sort and remove duplicates. -set -euxo pipefail +set -euo pipefail DIR="$(dirname $0)/.." cd $DIR # The sed -i.bak pattern is compatible between macos and linux -sed -i.bak "/^[^#].*--deleted_packages/s#=.*#=$(\ - find examples/*/* tests/*/* \( -name WORKSPACE -or -name MODULE.bazel \) | +{ + echo "# Generated via './tools/update_deleted_packages.sh'" + find examples tests gazelle \( -name WORKSPACE -or -name MODULE.bazel \) | xargs -n 1 dirname | - xargs -n 1 -I{} find {} \( -name BUILD -or -name BUILD.bazel \) | + xargs -I{} find {} \( -name BUILD -or -name BUILD.bazel \) | xargs -n 1 dirname | + grep -v "gazelle/docs" | sort -u | - paste -sd, -\ -)#" $DIR/.bazelrc && rm .bazelrc.bak + sed 's/^/common --deleted_packages=/g' +} | tee "$DIR"/.bazelrc.deleted_packages diff --git a/tools/wheelmaker.py b/tools/wheelmaker.py index de6b8f48af..70e375b4ee 100644 --- a/tools/wheelmaker.py +++ b/tools/wheelmaker.py @@ -24,7 +24,6 @@ import stat import sys import zipfile -from collections.abc import Iterable from pathlib import Path _ZIP_EPOCH = (1980, 1, 1, 0, 0, 0) @@ -100,7 +99,10 @@ def normalize_pep440(version): def arcname_from( - name: str, distribution_prefix: str, strip_path_prefixes: Sequence[str] = () + name: str, + distribution_prefix: str, + strip_path_prefixes: Sequence[str] = (), # noqa: F821 + add_path_prefix: str = "", ) -> str: """Return the within-archive name for a given file path name. @@ -110,6 +112,7 @@ def arcname_from( name: The file path eg 'mylib/a/b/c/file.py' distribution_prefix: The strip_path_prefixes: Remove these prefixes from names. + add_path_prefix: Add prefix after stripping the path from names. """ # Always use unix path separators. normalized_arcname = name.replace(os.path.sep, "/") @@ -118,9 +121,9 @@ def arcname_from( return normalized_arcname for prefix in strip_path_prefixes: if normalized_arcname.startswith(prefix): - return normalized_arcname[len(prefix) :] + return add_path_prefix + normalized_arcname[len(prefix) :] - return normalized_arcname + return add_path_prefix + normalized_arcname class _WhlFile(zipfile.ZipFile): @@ -131,14 +134,20 @@ def __init__( mode, distribution_prefix: str, strip_path_prefixes=None, + add_path_prefix=None, compression=zipfile.ZIP_DEFLATED, + quote_all_filenames: bool = False, **kwargs, ): self._distribution_prefix = distribution_prefix self._strip_path_prefixes = strip_path_prefixes or [] - # Entries for the RECORD file as (filename, hash, size) tuples. - self._record = [] + self._add_path_prefix = add_path_prefix or "" + # Entries for the RECORD file as (filename, digest, size) tuples. + self._record: list[tuple[str, str, str]] = [] + # Whether to quote filenames in the RECORD file (for compatibility with + # some wheels like torch that have quoted filenames in their RECORD). + self.quote_all_filenames = quote_all_filenames super().__init__(filename, mode=mode, compression=compression, **kwargs) @@ -164,6 +173,7 @@ def add_file(self, package_filename, real_filename): package_filename, distribution_prefix=self._distribution_prefix, strip_path_prefixes=self._strip_path_prefixes, + add_path_prefix=self._add_path_prefix, ) zinfo = self._zipinfo(arcname) @@ -192,16 +202,15 @@ def add_string(self, filename, contents): hash.update(contents) self._add_to_record(filename, self._serialize_digest(hash), len(contents)) - def _serialize_digest(self, hash): + def _serialize_digest(self, hash) -> str: # https://www.python.org/dev/peps/pep-0376/#record # "base64.urlsafe_b64encode(digest) with trailing = removed" digest = base64.urlsafe_b64encode(hash.digest()) digest = b"sha256=" + digest.rstrip(b"=") - return digest + return digest.decode("utf-8", "surrogateescape") - def _add_to_record(self, filename, hash, size): - size = str(size).encode("ascii") - self._record.append((filename, hash, size)) + def _add_to_record(self, filename: str, hash: str, size: int) -> None: + self._record.append((filename, hash, str(size))) def _zipinfo(self, filename): """Construct deterministic ZipInfo entry for a file named filename""" @@ -223,29 +232,27 @@ def _zipinfo(self, filename): zinfo.compress_type = self.compression return zinfo - def add_recordfile(self): + def _quote_filename(self, filename: str) -> str: + """Return a possibly quoted filename for RECORD file.""" + filename = filename.lstrip("/") + # Some RECORDs like torch have *all* filenames quoted and we must minimize diff. + # Otherwise, we quote only when necessary (e.g. for filenames with commas). + quoting = csv.QUOTE_ALL if self.quote_all_filenames else csv.QUOTE_MINIMAL + with io.StringIO() as buf: + csv.writer(buf, quoting=quoting).writerow([filename]) + return buf.getvalue().strip() + + def add_recordfile(self) -> str: """Write RECORD file to the distribution.""" record_path = self.distinfo_path("RECORD") - entries = self._record + [(record_path, b"", b"")] - with io.StringIO() as contents_io: - writer = csv.writer(contents_io, lineterminator="\n") - for filename, digest, size in entries: - if isinstance(filename, str): - filename = filename.lstrip("/") - writer.writerow( - ( - ( - c - if isinstance(c, str) - else c.decode("utf-8", "surrogateescape") - ) - for c in (filename, digest, size) - ) - ) - - contents = contents_io.getvalue() - self.add_string(record_path, contents) - return contents.encode("utf-8", "surrogateescape") + entries = self._record + [(record_path, "", "")] + entries = [ + (self._quote_filename(fname), digest, size) + for fname, digest, size in entries + ] + contents = "\n".join(",".join(entry) for entry in entries) + "\n" + self.add_string(record_path, contents) + return contents class WheelMaker(object): @@ -260,6 +267,7 @@ def __init__( compress, outfile=None, strip_path_prefixes=None, + add_path_prefix=None, ): self._name = name self._version = normalize_pep440(version) @@ -269,6 +277,7 @@ def __init__( self._platform = platform self._outfile = outfile self._strip_path_prefixes = strip_path_prefixes + self._add_path_prefix = add_path_prefix self._compress = compress self._wheelname_fragment_distribution_name = escape_filename_distribution_name( self._name @@ -286,7 +295,10 @@ def __enter__(self): mode="w", distribution_prefix=self._distribution_prefix, strip_path_prefixes=self._strip_path_prefixes, - compression=zipfile.ZIP_DEFLATED if self._compress else zipfile.ZIP_STORED, + add_path_prefix=self._add_path_prefix, + compression=( + zipfile.ZIP_DEFLATED if self._compress else zipfile.ZIP_STORED + ), ) return self @@ -329,9 +341,7 @@ def add_wheelfile(self): Wheel-Version: 1.0 Generator: bazel-wheelmaker 1.0 Root-Is-Purelib: {} -""".format( - "true" if self._platform == "any" else "false" - ) +""".format("true" if self._platform == "any" else "false") for tag in self.disttags(): wheel_contents += "Tag: %s\n" % tag self._whlfile.add_string(self.distinfo_path("WHEEL"), wheel_contents) @@ -364,6 +374,35 @@ def get_files_to_package(input_files): return files +def get_new_requirement_line(reqs_text: str, extra: str) -> str: + """Formats a requirement text into a Requires-Dist metadata line.""" + # This is not imported at the top of the file due to the reliance + # on this file in the `whl_library` repository rule which does not + # provide `packaging` but does import symbols defined here. + from packaging.requirements import Requirement + + req = Requirement(reqs_text.strip()) + req_extra_deps = f"[{','.join(req.extras)}]" if req.extras else "" + + # Handle URL requirements (PEP 508) + if req.url: + req_spec = f" @ {req.url}" + else: + req_spec = str(req.specifier) + + base = f"Requires-Dist: {req.name}{req_extra_deps}{req_spec}" + + if req.marker: + if extra: + return f"{base}; ({req.marker}) and {extra}" + else: + return f"{base}; {req.marker}" + elif extra: + return f"{base}; {extra}" + else: + return base + + def resolve_argument_stamp( argument: str, volatile_status_stamp: Path, stable_status_stamp: Path ) -> str: @@ -429,7 +468,7 @@ def parse_args() -> argparse.Namespace: output_group.add_argument( "--name_file", type=Path, - help="A file where the canonical name of the " "wheel will be written", + help="A file where the canonical name of the wheel will be written", ) output_group.add_argument( @@ -440,6 +479,13 @@ def parse_args() -> argparse.Namespace: help="Path prefix to be stripped from input package files' path. " "Can be supplied multiple times. Evaluated in order.", ) + output_group.add_argument( + "--path_prefix", + type=str, + default="", + help="Path prefix to be prepended to input package files' path. " + "It is prepended after stripping any specified path prefixes first.", + ) wheel_group = parser.add_argument_group("Wheel metadata") wheel_group.add_argument( @@ -452,7 +498,8 @@ def parse_args() -> argparse.Namespace: "--description_file", help="Path to the file with package description" ) wheel_group.add_argument( - "--description_content_type", help="Content type of the package description" + "--description_content_type", + help="Content type of the package description", ) wheel_group.add_argument( "--entry_points_file", @@ -501,7 +548,7 @@ def parse_args() -> argparse.Namespace: return parser.parse_args(sys.argv[1:]) -def _parse_file_pairs(content: List[str]) -> List[List[str]]: +def _parse_file_pairs(content: List[str]) -> List[List[str]]: # noqa: F821 """ Parse ; delimited lists of files into a 2D list. """ @@ -554,6 +601,7 @@ def main() -> None: platform=arguments.platform, outfile=arguments.out, strip_path_prefixes=strip_prefixes, + add_path_prefix=arguments.path_prefix, compress=not arguments.no_compress, ) as maker: for package_filename, real_filename in all_files: @@ -569,27 +617,9 @@ def main() -> None: metadata = arguments.metadata_file.read_text(encoding="utf-8") - # This is not imported at the top of the file due to the reliance - # on this file in the `whl_library` repository rule which does not - # provide `packaging` but does import symbols defined here. - from packaging.requirements import Requirement - # Search for any `Requires-Dist` entries that refer to other files and # expand them. - def get_new_requirement_line(reqs_text, extra): - req = Requirement(reqs_text.strip()) - req_extra_deps = f"[{','.join(req.extras)}]" if req.extras else "" - if req.marker: - if extra: - return f"Requires-Dist: {req.name}{req_extra_deps}{req.specifier}; ({req.marker}) and {extra}" - else: - return f"Requires-Dist: {req.name}{req_extra_deps}{req.specifier}; {req.marker}" - else: - return f"Requires-Dist: {req.name}{req_extra_deps}{req.specifier}; {extra}".strip( - " ;" - ) - for meta_line in metadata.splitlines(): if not meta_line.startswith("Requires-Dist: "): continue @@ -638,7 +668,8 @@ def get_new_requirement_line(reqs_text, extra): if arguments.entry_points_file: maker.add_file( - maker.distinfo_path("entry_points.txt"), arguments.entry_points_file + maker.distinfo_path("entry_points.txt"), + arguments.entry_points_file, ) # Sort the files for reproducible order in the archive. diff --git a/workspace_bazel9.bzl b/workspace_bazel9.bzl new file mode 100644 index 0000000000..dad59378a9 --- /dev/null +++ b/workspace_bazel9.bzl @@ -0,0 +1,10 @@ +"""Workaround for Bazel 9 duplicate name issue in Gazelle.""" + +def bazel_9_workaround(name = "bazel_9_workaround"): + # Necessary so that Bazel 9 recognizes this as rules_python and doesn't try + # to load the version Bazel itself uses by default. + # We hide this from Gazelle's WORKSPACE parser by putting it in a macro. + native.local_repository( + name = "rules_python", + path = ".", + )