diff --git a/.bazelignore b/.bazelignore new file mode 100644 index 0000000..56bef8c --- /dev/null +++ b/.bazelignore @@ -0,0 +1 @@ +examples/ \ No newline at end of file diff --git a/.bazelrc b/.bazelrc new file mode 100644 index 0000000..694f59b --- /dev/null +++ b/.bazelrc @@ -0,0 +1,3 @@ +common --enable_bzlmod + +common --lockfile_mode=off diff --git a/.bazelversion b/.bazelversion new file mode 100644 index 0000000..66ce77b --- /dev/null +++ b/.bazelversion @@ -0,0 +1 @@ +7.0.0 diff --git a/.bcr/README.md b/.bcr/README.md new file mode 100644 index 0000000..44ae7fe --- /dev/null +++ b/.bcr/README.md @@ -0,0 +1,9 @@ +# Bazel Central Registry + +When the ruleset is released, we want it to be published to the +Bazel Central Registry automatically: + + +This folder contains configuration files to automate the publish step. +See +for authoritative documentation about these files. diff --git a/.bcr/config.yml b/.bcr/config.yml new file mode 100644 index 0000000..894ade6 --- /dev/null +++ b/.bcr/config.yml @@ -0,0 +1,3 @@ +fixedReleaser: + login: mark-thm + email: 123787712+mark-thm@users.noreply.github.com diff --git a/.bcr/metadata.template.json b/.bcr/metadata.template.json new file mode 100644 index 0000000..48e6bef --- /dev/null +++ b/.bcr/metadata.template.json @@ -0,0 +1,18 @@ +{ + "homepage": "https://github.com/theoremlp/rules_uv", + "maintainers": [ + { + "email": "123787712+mark-thm@users.noreply.github.com", + "github": "mark-thm", + "name": "Mark Elliot" + }, + { + "email": "bazel-maintainers@theoremlp.com", + "github": "theoremlp", + "name": "Theorem Bazel Maintainers" + } + ], + "repository": ["github:theoremlp/rules_uv"], + "versions": [], + "yanked_versions": {} +} diff --git a/.bcr/presubmit.yml b/.bcr/presubmit.yml new file mode 100644 index 0000000..3a2a60f --- /dev/null +++ b/.bcr/presubmit.yml @@ -0,0 +1,15 @@ +matrix: + platform: + - debian10 + - ubuntu2004 + - macos + - macos_arm64 + bazel: + - 7.x +tasks: + verify_targets: + name: Verify build targets + platform: ${{ platform }} + bazel: ${{ bazel }} + build_targets: + - "@rules_uv//..." diff --git a/.bcr/source.template.json b/.bcr/source.template.json new file mode 100644 index 0000000..d25b066 --- /dev/null +++ b/.bcr/source.template.json @@ -0,0 +1,5 @@ +{ + "integrity": "**leave this alone**", + "strip_prefix": "{REPO}-{VERSION}", + "url": "https://github.com/{OWNER}/{REPO}/releases/download/{TAG}/{REPO}-{VERSION}.tar.gz" +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e8f86e7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +# In code review, collapse generated files +docs/*.md linguist-generated=true + +################################# +# Configuration for 'git archive' +# See https://git-scm.com/docs/git-archive#ATTRIBUTES + +# Don't include examples in the distribution artifact, to reduce size. +# You may want to add additional exclusions for folders or files that users don't need. +examples export-ignore diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000..4039f18 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + ":dependencyDashboard", + ":semanticPrefixFixDepsChoreOthers", + "group:monorepos", + "group:recommended", + "replacements:all", + "workarounds:all" + ], + "packageRules": [ + { + "matchFiles": ["MODULE.bazel"], + "enabled": false + } + ] + } + \ No newline at end of file diff --git a/.github/workflows/ci.bazelrc b/.github/workflows/ci.bazelrc new file mode 100644 index 0000000..cb2a9b3 --- /dev/null +++ b/.github/workflows/ci.bazelrc @@ -0,0 +1,18 @@ +# This file contains Bazel settings to apply on CI only. +# It is referenced with a --bazelrc option in the call to bazel in ci.yaml + +# Debug where options came from +build --announce_rc + +# This directory is configured in GitHub actions to be persisted between runs. +# We do not enable the repository cache to cache downloaded external artifacts +# as these are generally faster to download again than to fetch them from the +# GitHub actions cache. +build --disk_cache=~/.cache/bazel + +# Don't rely on test logs being easily accessible from the test runner, +# though it makes the log noisier. +test --test_output=errors + +# Allows tests to run bazelisk-in-bazel, since this is the cache folder used +test --test_env=XDG_CACHE_HOME diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a04da1c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +name: CI +on: + push: + branches: [main] + pull_request: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true +jobs: + test: + uses: bazel-contrib/.github/.github/workflows/bazel.yaml@v5 + with: + folders: | + [ + ".", + "examples/typical", + ] + # we only support Bazel 7, and only with bzlmod enabled + exclude: | + [ + {"bzlmodEnabled": false}, + {"bazelversion": "5.4.0"}, + {"bazelversion": "6.4.0"}, + ] + # this ruleset only supports linux and macos + exclude_windows: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..2e586e7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,14 @@ +name: Release +on: + push: + tags: + - "v*.*.*" + +jobs: + release: + uses: bazel-contrib/.github/.github/workflows/release_ruleset.yaml@v5 + permissions: + contents: write + with: + release_files: rules_uv-*.tar.gz + prerelease: false diff --git a/.github/workflows/release_prep.sh b/.github/workflows/release_prep.sh new file mode 100755 index 0000000..1c01b98 --- /dev/null +++ b/.github/workflows/release_prep.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +# invoked by release workflow +# (via https://github.com/bazel-contrib/.github/blob/master/.github/workflows/release_ruleset.yaml) + +set -o errexit -o nounset -o pipefail + +RULES_NAME="rules_uv" +TAG="${GITHUB_REF_NAME}" +PREFIX="${RULES_NAME}-${TAG:1}" +ARCHIVE="${RULES_NAME}-${TAG:1}.tar.gz" + +# embed version in MODULE.bazel +perl -pi -e "s/version = \"0\.0\.0\",/version = \"${TAG:1}\",/g" MODULE.bazel + +stash_name=`git stash create`; +git archive --format=tar --prefix=${PREFIX}/ "${stash_name}" | gzip > $ARCHIVE + +SHA=$(shasum -a 256 $ARCHIVE | awk '{print $1}') + +cat << EOF +## Using Bzlmod with Bazel 7 + +1. Enable with \`common --enable_bzlmod\` in \`.bazelrc\`. +2. Add to your \`MODULE.bazel\` file: + +\`\`\`starlark +bazel_dep(name = "${RULES_NAME}", version = "${TAG:1}") +\`\`\` +EOF diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b288699 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +bazel-bin/ +bazel-out/ +bazel-testlogs/ +bazel-* + +venv/ diff --git a/BUILD.bazel b/BUILD.bazel new file mode 100644 index 0000000..f5bf95d --- /dev/null +++ b/BUILD.bazel @@ -0,0 +1,22 @@ +load("@buildifier_prebuilt//:rules.bzl", "buildifier", "buildifier_test") + +exports_files([ + "MODULE.bazel", +]) + +buildifier( + name = "buildifier.fix", + exclude_patterns = ["./.git/*"], + lint_mode = "fix", + mode = "fix", + visibility = ["//thm/buildtools/fix:__pkg__"], +) + +buildifier_test( + name = "buildifier.test", + exclude_patterns = ["./.git/*"], + lint_mode = "warn", + mode = "diff", + no_sandbox = True, + workspace = "//:MODULE.bazel", +) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/MODULE.bazel b/MODULE.bazel new file mode 100644 index 0000000..905d78b --- /dev/null +++ b/MODULE.bazel @@ -0,0 +1,19 @@ +"rules_uv" + +module( + name = "rules_uv", + version = "0.0.0", + compatibility_level = 1, +) + +bazel_dep(name = "bazel_skylib", version = "1.4.1") +bazel_dep(name = "buildifier_prebuilt", version = "6.1.2") +bazel_dep(name = "platforms", version = "0.0.8") +bazel_dep(name = "rules_multitool", version = "0.4.0") + +# required for venv +bazel_dep(name = "rules_python", version = "0.31.0") + +multitool = use_extension("@rules_multitool//multitool:extension.bzl", "multitool") +multitool.hub(lockfile = "//uv/private:uv.lock.json") +use_repo(multitool, "multitool") diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel new file mode 100644 index 0000000..e69de29 diff --git a/examples/typical/.bazelrc b/examples/typical/.bazelrc new file mode 100644 index 0000000..694f59b --- /dev/null +++ b/examples/typical/.bazelrc @@ -0,0 +1,3 @@ +common --enable_bzlmod + +common --lockfile_mode=off diff --git a/examples/typical/.bazelversion b/examples/typical/.bazelversion new file mode 100644 index 0000000..66ce77b --- /dev/null +++ b/examples/typical/.bazelversion @@ -0,0 +1 @@ +7.0.0 diff --git a/examples/typical/BUILD.bazel b/examples/typical/BUILD.bazel new file mode 100644 index 0000000..b3ebf69 --- /dev/null +++ b/examples/typical/BUILD.bazel @@ -0,0 +1,6 @@ +load("@rules_uv//uv:pip.bzl", "pip_compile") +load("@rules_uv//uv:venv.bzl", "create_venv") + +pip_compile(name = "generate_requirements_txt") + +create_venv(name = "create-venv") diff --git a/examples/typical/MODULE.bazel b/examples/typical/MODULE.bazel new file mode 100644 index 0000000..8e5230a --- /dev/null +++ b/examples/typical/MODULE.bazel @@ -0,0 +1,13 @@ +"typical usage of rules_uv" + +module( + name = "uv__examples__typical", + version = "0.0.0", +) + +bazel_dep(name = "platforms", version = "0.0.8") +bazel_dep(name = "rules_uv", version = "0.0.0") +local_path_override( + module_name = "rules_uv", + path = "../..", +) diff --git a/examples/typical/WORKSPACE.bazel b/examples/typical/WORKSPACE.bazel new file mode 100644 index 0000000..e69de29 diff --git a/examples/typical/integration_test.sh b/examples/typical/integration_test.sh new file mode 100755 index 0000000..7ac2187 --- /dev/null +++ b/examples/typical/integration_test.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -eu + +$1 -version diff --git a/examples/typical/requirements.in b/examples/typical/requirements.in new file mode 100644 index 0000000..dbcc677 --- /dev/null +++ b/examples/typical/requirements.in @@ -0,0 +1 @@ +click~=8.1.7 \ No newline at end of file diff --git a/examples/typical/requirements.txt b/examples/typical/requirements.txt new file mode 100644 index 0000000..f2aabcd --- /dev/null +++ b/examples/typical/requirements.txt @@ -0,0 +1,3 @@ +click==8.1.7 \ + --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \ + --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..35658e0 --- /dev/null +++ b/readme.md @@ -0,0 +1,46 @@ +# rules_uv + +Bazel rules to enable use of [uv](https://github.com/astral-sh/uv) to compile pip requirements and generate virtual envs. + +## Usage + +Installing with bzlmod, add to MODULE.bazel (adjust version as appropriate): + +```starlark +bazel_dep(name = "rules_uv", version = "") +``` + +### pip_compile + +Create a requirements.in or pyproject.toml -> requirements.txt compilation target and diff test: + +```starlark +load("@rules_uv//uv:pip.bzl", "pip_compile") + +pip_compile( + name = "generate_requirements_txt", + requirements_in = "//:requirements.in", # default + requirements_txt = "//:requirements.txt", # default +) +``` + +Run the compilation step with `bazel run //:generate_requirements_txt`. + +This will automatically register a diff test with name `[name]_diff_test`. + +### create_venv + +Create a virtual environment creation target: + +```starlark +load("@rules_uv//uv:venv.bzl", "create_venv") + +create_venv( + name = "create_venv", + requirements_txt = "//:requirements.txt", # default +) +``` + +Create a virtual environment with default path `venv` by running `bazel run //:create_venv`. The generated script accepts a single, optional argument to define the virtual environment path. + +The created venv will use the default Python 3 runtime defined in rules_python. diff --git a/uv/BUILD.bazel b/uv/BUILD.bazel new file mode 100644 index 0000000..6f0e386 --- /dev/null +++ b/uv/BUILD.bazel @@ -0,0 +1,13 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + +bzl_library( + name = "pip", + srcs = ["pip.bzl"], + visibility = ["//visibility:public"], +) + +bzl_library( + name = "venv", + srcs = ["venv.bzl"], + visibility = ["//visibility:public"], +) diff --git a/uv/pip.bzl b/uv/pip.bzl new file mode 100644 index 0000000..4010742 --- /dev/null +++ b/uv/pip.bzl @@ -0,0 +1,5 @@ +"uv based pip compile rules" + +load("//uv/private:pip.bzl", _pip_compile = "pip_compile") + +pip_compile = _pip_compile diff --git a/uv/private/BUILD.bazel b/uv/private/BUILD.bazel new file mode 100644 index 0000000..fdbd36c --- /dev/null +++ b/uv/private/BUILD.bazel @@ -0,0 +1,19 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + +exports_files([ + "create_venv.sh", + "pip_compile_test.sh", + "pip_compile.sh", +]) + +bzl_library( + name = "pip", + srcs = ["pip.bzl"], + visibility = ["//multitool:__subpackages__"], +) + +bzl_library( + name = "venv", + srcs = ["venv.bzl"], + visibility = ["//multitool:__subpackages__"], +) diff --git a/uv/private/create_venv.sh b/uv/private/create_venv.sh new file mode 100644 index 0000000..f16893b --- /dev/null +++ b/uv/private/create_venv.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +set -euo pipefail + +UV="{{uv}}" +RESOLVED_PYTHON="{{resolved_python}}" +REQUIREMENTS_TXT="{{requirements_txt}}" + +PYTHON="$(realpath "$RESOLVED_PYTHON")" + +bold="$(tput bold)" +normal="$(tput sgr0)" + +if [ $# -gt 1 ]; then + echo "create-venv takes one optional argument, the path to the virtual environment." + exit -1 +elif [ $# == 0 ] || [ -z "$1" ]; then + target="venv" +else + target="$1" +fi + +if [ "${target}" == "/" ] || [ "${target}" == "." ] +then + echo "${bold}Invalid venv target '${target}'${normal}" + exit -1 +fi + +"$UV" venv "$BUILD_WORKSPACE_DIRECTORY/$target" --python "$PYTHON" +source "$BUILD_WORKSPACE_DIRECTORY/$target/bin/activate" +"$UV" pip install -r "$REQUIREMENTS_TXT" + +echo "${bold}Created '${target}', to activate run:${normal}" +echo " source ${target}/bin/activate" diff --git a/uv/private/pip.bzl b/uv/private/pip.bzl new file mode 100644 index 0000000..bd31d7b --- /dev/null +++ b/uv/private/pip.bzl @@ -0,0 +1,81 @@ +"uv based pip compile rules" + +_PY_TOOLCHAIN = "@bazel_tools//tools/python:toolchain_type" + +_common_attrs = { + "requirements_in": attr.label(mandatory = True, allow_single_file = True), + "requirements_txt": attr.label(mandatory = True, allow_single_file = True), + "_uv": attr.label(default = "@multitool//tools/uv", executable = True, cfg = "exec"), +} + +def _uv_pip_compile(ctx, template, executable): + py_toolchain = ctx.toolchains[_PY_TOOLCHAIN] + ctx.actions.expand_template( + template = template, + output = executable, + substitutions = { + "{{uv}}": ctx.executable._uv.short_path, + "{{requirements_in}}": ctx.file.requirements_in.short_path, + "{{requirements_txt}}": ctx.file.requirements_txt.short_path, + "{{resolved_python}}": py_toolchain.py3_runtime.interpreter.short_path, + }, + ) + +def _runfiles(ctx): + py_toolchain = ctx.toolchains[_PY_TOOLCHAIN] + runfiles = ctx.runfiles( + files = [ctx.file.requirements_in, ctx.file.requirements_txt], + transitive_files = py_toolchain.py3_runtime.files, + ) + runfiles = runfiles.merge(ctx.attr._uv.default_runfiles) + return runfiles + +def _pip_compile_impl(ctx): + executable = ctx.actions.declare_file(ctx.attr.name) + _uv_pip_compile(ctx, ctx.file._template, executable) + return DefaultInfo( + executable = executable, + runfiles = _runfiles(ctx), + ) + +_pip_compile = rule( + attrs = _common_attrs | { + "_template": attr.label(default = "//uv/private:pip_compile.sh", allow_single_file = True), + }, + toolchains = [_PY_TOOLCHAIN], + implementation = _pip_compile_impl, + executable = True, +) + +def _pip_compile_test_impl(ctx): + executable = ctx.actions.declare_file(ctx.attr.name) + _uv_pip_compile(ctx, ctx.file._template, executable) + return DefaultInfo( + executable = executable, + runfiles = _runfiles(ctx), + ) + +_pip_compile_test = rule( + attrs = _common_attrs | { + "_template": attr.label(default = "//uv/private:pip_compile_test.sh", allow_single_file = True), + }, + toolchains = [_PY_TOOLCHAIN], + implementation = _pip_compile_test_impl, + test = True, +) + +def pip_compile(name, requirements_in = None, requirements_txt = None, target_compatible_with = None): + _pip_compile( + name = name, + requirements_in = requirements_in or "//:requirements.in", + requirements_txt = requirements_txt or "//:requirements.txt", + target_compatible_with = target_compatible_with, + ) + + _pip_compile_test( + name = name + "_diff_test", + requirements_in = requirements_in or "//:requirements.in", + requirements_txt = requirements_txt or "//:requirements.txt", + target_compatible_with = target_compatible_with, + tags = ["requires-network"], + ) diff --git a/uv/private/pip_compile.sh b/uv/private/pip_compile.sh new file mode 100644 index 0000000..609e632 --- /dev/null +++ b/uv/private/pip_compile.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +set -euo pipefail + +UV="{{uv}}" +RESOLVED_PYTHON="{{resolved_python}}" +REQUIREMENTS_IN="{{requirements_in}}" +REQUIREMENTS_TXT="{{requirements_txt}}" + +RESOLVED_PYTHON_BIN="$(dirname "$RESOLVED_PYTHON")" + +# set resolved python to front of the path +export PATH="$RESOLVED_PYTHON_BIN:$PATH" + +# get the version of python to hand to uv pip compile +PYTHON_VERSION="$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')" + +$UV pip compile \ + --generate-hashes \ + --no-header \ + --python-version=$PYTHON_VERSION \ + -o $REQUIREMENTS_TXT \ + $REQUIREMENTS_IN \ + $@ diff --git a/uv/private/pip_compile_test.sh b/uv/private/pip_compile_test.sh new file mode 100644 index 0000000..fc4574e --- /dev/null +++ b/uv/private/pip_compile_test.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +set -euo pipefail + +UV="{{uv}}" +RESOLVED_PYTHON="{{resolved_python}}" +REQUIREMENTS_IN="{{requirements_in}}" +REQUIREMENTS_TXT="{{requirements_txt}}" + +RESOLVED_PYTHON_BIN="$(dirname "$RESOLVED_PYTHON")" + +# set resolved python to front of the path +export PATH="$RESOLVED_PYTHON_BIN:$PATH" + +# get the version of python to hand to uv pip compile +PYTHON_VERSION="$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')" + +$UV pip compile \ + --no-cache \ + --generate-hashes \ + --no-header \ + --python-version=$PYTHON_VERSION \ + -c $REQUIREMENTS_TXT \ + $REQUIREMENTS_IN diff --git a/uv/private/uv.lock.json b/uv/private/uv.lock.json new file mode 100644 index 0000000..f62755f --- /dev/null +++ b/uv/private/uv.lock.json @@ -0,0 +1,30 @@ +{ + "uv": { + "binaries": [ + { + "kind": "archive", + "url": "https://github.com/astral-sh/uv/releases/download/0.1.17/uv-x86_64-unknown-linux-gnu.tar.gz", + "sha256": "add91a881b3de0a2defa6cf363bbbbfb5dc58e85cff52e8164052e86fee73809", + "file": "uv-x86_64-unknown-linux-gnu/uv", + "os": "linux", + "cpu": "x86_64" + }, + { + "kind": "archive", + "url": "https://github.com/astral-sh/uv/releases/download/0.1.17/uv-x86_64-apple-darwin.tar.gz", + "sha256": "957fbe84e1fb1a7e8b43ed4db7bd5ef5ea3b3f996c92144d5eaf6b3e259859cd", + "file": "uv-x86_64-apple-darwin/uv", + "os": "macos", + "cpu": "x86_64" + }, + { + "kind": "archive", + "url": "https://github.com/astral-sh/uv/releases/download/0.1.17/uv-aarch64-apple-darwin.tar.gz", + "sha256": "8f6d1b142dfc4d2040b86a94956eb3c2f5436fd0e889e0d0d1c59dbb8fbbf9de", + "file": "uv-aarch64-apple-darwin/uv", + "os": "macos", + "cpu": "arm64" + } + ] + } +} diff --git a/uv/private/venv.bzl b/uv/private/venv.bzl new file mode 100644 index 0000000..1aa3457 --- /dev/null +++ b/uv/private/venv.bzl @@ -0,0 +1,50 @@ +"uv based venv generation" + +_PY_TOOLCHAIN = "@bazel_tools//tools/python:toolchain_type" + +def _uv_template(ctx, template, executable): + py_toolchain = ctx.toolchains[_PY_TOOLCHAIN] + ctx.actions.expand_template( + template = template, + output = executable, + substitutions = { + "{{uv}}": ctx.executable._uv.short_path, + "{{requirements_txt}}": ctx.file.requirements_txt.short_path, + "{{resolved_python}}": py_toolchain.py3_runtime.interpreter.short_path, + }, + ) + +def _runfiles(ctx): + py_toolchain = ctx.toolchains[_PY_TOOLCHAIN] + runfiles = ctx.runfiles( + files = [ctx.file.requirements_txt], + transitive_files = py_toolchain.py3_runtime.files, + ) + runfiles = runfiles.merge(ctx.attr._uv.default_runfiles) + return runfiles + +def _venv_impl(ctx): + executable = ctx.actions.declare_file(ctx.attr.name) + _uv_template(ctx, ctx.file._template, executable) + return DefaultInfo( + executable = executable, + runfiles = _runfiles(ctx), + ) + +_venv = rule( + attrs = { + "requirements_txt": attr.label(mandatory = True, allow_single_file = True), + "_uv": attr.label(default = "@multitool//tools/uv", executable = True, cfg = "exec"), + "_template": attr.label(default = "//uv/private:create_venv.sh", allow_single_file = True), + }, + toolchains = [_PY_TOOLCHAIN], + implementation = _venv_impl, + executable = True, +) + +def create_venv(name, requirements_txt = None, target_compatible_with = None): + _venv( + name = name, + requirements_txt = requirements_txt or "//:requirements.txt", + target_compatible_with = target_compatible_with, + ) diff --git a/uv/venv.bzl b/uv/venv.bzl new file mode 100644 index 0000000..12dbed0 --- /dev/null +++ b/uv/venv.bzl @@ -0,0 +1,5 @@ +"uv based virtual env generation" + +load("//uv/private:venv.bzl", _create_venv = "create_venv") + +create_venv = _create_venv