Skip to content

Commit

Permalink
Publish binaries on releases (#292)
Browse files Browse the repository at this point in the history
Fixes #290 
Fixes #289 

---

### Type of change

- Bug fix (change which fixes an issue)

### Test plan

- Covered by existing test cases
- Manual testing; please provide instructions so we can reproduce:
  • Loading branch information
alexeagle committed Mar 2, 2024
1 parent 334dd44 commit 9ee2b08
Show file tree
Hide file tree
Showing 19 changed files with 310 additions and 81 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,10 @@ jobs:
steps:
- uses: actions/checkout@v4
- run: patch --dry-run -p1 < .bcr/patches/*.patch

test-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- working-directory: e2e/use_release
run: ./minimal_download_test.sh
13 changes: 7 additions & 6 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ bazel_dep(name = "bazel_skylib", version = "1.4.2")
bazel_dep(name = "rules_python", version = "0.29.0")
bazel_dep(name = "platforms", version = "0.0.7")

archive_override(
module_name = "rules_python",
urls = "https://github.com/bazelbuild/rules_python/archive/52381415be9d3618130f02a821aef50de1e3af09.tar.gz",
integrity = "sha256-pYfEFNWqygSEElDYgJsuIeDYn9oll/rZB0GcR+6rirA=",
strip_prefix = "rules_python-52381415be9d3618130f02a821aef50de1e3af09",
)
tools = use_extension("//py:extensions.bzl", "py_tools")
tools.rules_py_tools()
use_repo(tools, "rules_py_tools")

register_toolchains(
"@rules_py_tools//:all",

# Register the "from source" toolchains last, so there's no accidental dependency on Rust
# For manual testing: comment these out to force use of pre-built binaries.
"@aspect_rules_py//py/private/toolchain/venv/...",
"@aspect_rules_py//py/private/toolchain/unpack/...",
)
Expand Down
1 change: 1 addition & 0 deletions e2e/use_release/.bazelversion
6 changes: 6 additions & 0 deletions e2e/use_release/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
load("@aspect_rules_py//py:defs.bzl", "py_binary")

py_binary(
name = "main",
srcs = ["__main__.py"],
)
10 changes: 10 additions & 0 deletions e2e/use_release/MODULE.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
bazel_dep(name = "aspect_rules_py", version = "0.7.0")

local_path_override(
module_name = "aspect_rules_py",
path = "../..",
)

tools = use_extension("@aspect_rules_py//py:extensions.bzl", "py_tools")
tools.rules_py_tools(is_prerelease = False)
use_repo(tools, "rules_py_tools")
6 changes: 6 additions & 0 deletions e2e/use_release/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Test releases

This is currently a manual test we can perform _after_ publishing a release.

It asserts that the release_prep.sh constructed a correct tarball of the ruleset (the "Bazel Module")
and that usage of a release has the correct minimal toolchain behaviors.
27 changes: 27 additions & 0 deletions e2e/use_release/WORKSPACE.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Override http_archive for local testing
local_repository(
name = "aspect_rules_py",
path = "../..",
)

# Fetches the rules_py dependencies.
# If you want to have a different version of some dependency,
# you should fetch it *before* calling this.
# Alternatively, you can skip calling this function, so long as you've
# already fetched all the dependencies.
load("@aspect_rules_py//py:repositories.bzl", "rules_py_dependencies", "rules_py_toolchains")

rules_py_dependencies(register_toolchains = False)

# Force use of pre-built release binaries regardless of the content of /tools/version.bzl
rules_py_toolchains(is_prerelease = False)

# "Installation" for rules_python
load("@rules_python//python:repositories.bzl", "py_repositories", "python_register_toolchains")

python_register_toolchains(
name = "python_toolchain",
python_version = "3.9",
)

py_repositories()
1 change: 1 addition & 0 deletions e2e/use_release/WORKSPACE.bzlmod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# This file replaces WORKSPACE.bazel under --enable_bzlmod
1 change: 1 addition & 0 deletions e2e/use_release/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
print("hello world")
61 changes: 61 additions & 0 deletions e2e/use_release/minimal_download_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env bash

set -o errexit -o pipefail -o nounset

OS="$(uname | tr '[:upper:]' '[:lower:]')"
ARCH="$(arch)"
ALLOWED="rules_py_tools.${OS}_${ARCH}"
if [ "$ARCH" == "x86_64" ]; then
ALLOWED="rules_py_tools.${OS}_amd64"
fi

# This test references pre-built artifacts from a prior release.
# Will need to bump this version in the future when there are breaking changes.
export RULES_PY_RELEASE_VERSION=0.7.0

#############
# Test bzlmod
(
cd ../..
patch -p1 < .bcr/patches/*.patch
)
OUTPUT_BASE=$(mktemp -d)
output=$(bazel "--output_base=$OUTPUT_BASE" run --enable_bzlmod //:main)
if [[ "$output" != "hello world" ]]; then
>&2 echo "ERROR: bazel command did not produce expected output"
exit 1
fi
externals=$(ls $OUTPUT_BASE/external)

if echo "$externals" | grep -v "${ALLOWED}" | grep -v ".marker" | grep rules_py_tools.
then
>&2 echo "ERROR: rules_py binaries were fetched for platform other than ${ALLOWED}"
exit 1
fi
if echo "$externals" | grep rust
then
>&2 echo "ERROR: we fetched a rust repository"
exit 1
fi

#############
# Test WORKSPACE
OUTPUT_BASE=$(mktemp -d)
output=$(bazel "--output_base=$OUTPUT_BASE" run --noenable_bzlmod //:main)
if [[ "$output" != "hello world" ]]; then
>&2 echo "ERROR: bazel command did not produce expected output"
exit 1
fi

externals=$(ls $OUTPUT_BASE/external)

if echo "$externals" | grep -v "${ALLOWED}" | grep -v ".marker" | grep rules_py_tools.
then
>&2 echo "ERROR: rules_py binaries were fetched for platform other than ${ALLOWED}"
exit 1
fi
if echo "$externals" | grep rust
then
>&2 echo "ERROR: we fetched a rust repository"
exit 1
fi
41 changes: 41 additions & 0 deletions py/extensions.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"Module Extensions used from MODULE.bazel"

load(":repositories.bzl", "DEFAULT_TOOLS_REPOSITORY", "rules_py_toolchains")
load("//tools:version.bzl", "IS_PRERELEASE")

py_toolchain = tag_class(attrs = {
"name": attr.string(doc = """\
Base name for generated repositories, allowing more than one toolchain to be registered.
Overriding the default is only permitted in the root module.
""", default = DEFAULT_TOOLS_REPOSITORY),
"is_prerelease": attr.bool(
doc = "True iff there are no pre-built tool binaries for this version of rules_py",
default = IS_PRERELEASE,
),
})

def _toolchains_extension_impl(module_ctx):
registrations = []
root_name = None
for mod in module_ctx.modules:
for toolchain in mod.tags.rules_py_tools:
if toolchain.name != DEFAULT_TOOLS_REPOSITORY and not mod.is_root:
fail("""\
Only the root module may override the default name for the rules_py_tools toolchain.
This prevents conflicting registrations in the global namespace of external repos.
""")

# Ensure the root wins in case of differences
if mod.is_root:
rules_py_toolchains(toolchain.name, register = False, is_prerelease = toolchain.is_prerelease)
root_name = toolchain.name
else:
registrations.append(toolchain.name)
for name in registrations:
if name != root_name:
rules_py_toolchains(name, register = False)

py_tools = module_extension(
implementation = _toolchains_extension_impl,
tag_classes = {"rules_py_tools": py_toolchain},
)
10 changes: 10 additions & 0 deletions py/private/toolchain/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ exports_files(
visibility = ["//visibility:public"],
)

toolchain_type(
name = "unpack_toolchain_type",
visibility = ["//visibility:public"],
)

toolchain_type(
name = "venv_toolchain_type",
visibility = ["//visibility:public"],
)

bzl_library(
name = "autodetecting",
srcs = ["autodetecting.bzl"],
Expand Down
77 changes: 77 additions & 0 deletions py/private/toolchain/repo.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""Create a repository to hold the toolchains
This follows guidance here:
https://docs.bazel.build/versions/main/skylark/deploying.html#registering-toolchains
"
Note that in order to resolve toolchains in the analysis phase
Bazel needs to analyze all toolchain targets that are registered.
Bazel will not need to analyze all targets referenced by toolchain.toolchain attribute.
If in order to register toolchains you need to perform complex computation in the repository,
consider splitting the repository with toolchain targets
from the repository with <LANG>_toolchain targets.
Former will be always fetched,
and the latter will only be fetched when user actually needs to build <LANG> code.
"
The "complex computation" in our case is simply downloading our pre-built rust binaries.
This guidance tells us how to avoid that: we put the toolchain targets in the alias repository
with only the toolchain attribute pointing into the platform-specific repositories.
"""

load("//py/private/toolchain:tools.bzl", "RUST_BIN_CFG", "TOOLCHAIN_PLATFORMS")

def _toolchains_repo_impl(repository_ctx):
build_content = """# Generated by toolchains_repo.bzl
#
# These can be registered in the workspace file or passed to --extra_toolchains flag.
# By default all these toolchains are registered by the py_register_toolchains macro
# so you don't normally need to interact with these targets.
"""
for tool, cfg in RUST_BIN_CFG.items():
for [platform, meta] in TOOLCHAIN_PLATFORMS.items():
build_content += """
# Declare a toolchain Bazel will select for running {tool} on the {cfg} platform.
toolchain(
name = "{tool}_{platform}_toolchain",
{cfg}_compatible_with = {compatible_with},
# Bazel does not follow this attribute during analysis, so the referenced repo
# will only be fetched if this toolchain is selected.
toolchain = "@{user_repository_name}.{platform}//:{tool}_toolchain",
toolchain_type = "@aspect_rules_py//py/private/toolchain:{tool}_toolchain_type",
)
""".format(
cfg = cfg,
tool = tool,
platform = platform,
user_repository_name = repository_ctx.attr.user_repository_name,
compatible_with = meta.compatible_with,
)

# Base BUILD file for this repository
repository_ctx.file("BUILD.bazel", build_content)

toolchains_repo = repository_rule(
_toolchains_repo_impl,
doc = """\
Creates a single repository with toolchain definitions for all known platforms
that can be registered or selected.
""",
attrs = {
"user_repository_name": attr.string(mandatory = True, doc = """\
What the user chose for the base name.
Needed since bzlmod apparent name has extra tilde segments.
"""),
},
)

def _prerelease_toolchains_repo_impl(repository_ctx):
repository_ctx.file("BUILD.bazel", "# No toolchains created for pre-releases")

prerelease_toolchains_repo = repository_rule(
_prerelease_toolchains_repo_impl,
doc = """Create a repo with an empty BUILD file, which registers no toolchains.
This is used for pre-releases, which have no pre-built binaries, but still want to call
register_toolchains("@this_repo//:all")
By doing this, we can avoid those register_toolchains callsites needing to be conditional on IS_PRERELEASE
""",
)
52 changes: 15 additions & 37 deletions py/private/toolchain/tools.bzl
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
"""Utilities for making toolchains"""
"""Declaration of concrete toolchains for our Rust tools"""

load("//tools:integrity.bzl", "RELEASED_BINARY_INTEGRITY")
load("//tools:version.bzl", "VERSION")

# The expected config for each tool, whether it runs in an action or at runtime
RUST_BIN_CFG = {
# unpack wheels happens inside an action
"unpack": "exec",
# creating the virtualenv happens when the binary is running
"venv": "target",
}

Expand Down Expand Up @@ -100,13 +103,14 @@ def source_toolchain(name, toolchain_type, bin):
toolchain_type = toolchain_type,
)

def _tool_repo_impl(rctx):
def _prebuilt_tool_repo_impl(rctx):
build_content = """\
# Generated by @aspect_rules_py//py/private/toolchain:tools.bzl
load("@aspect_rules_py//py/private/toolchain:tools.bzl", "py_tool_toolchain")
package(default_visibility = ["//visibility:public"])
"""

# For manual testing, override these environment variables
# TODO: use rctx.getenv when available, see https://github.com/bazelbuild/bazel/pull/20944
release_fork = "aspect-build"
Expand All @@ -125,51 +129,25 @@ package(default_visibility = ["//visibility:public"])
url = "https://github.com/{}/rules_py/releases/download/v{}/{}".format(
release_fork,
release_version,
filename
filename,
)
rctx.download(
url = url,
sha256 = RELEASED_BINARY_INTEGRITY[filename],
executable = True,
output = tool,
)
build_content += """\
py_tool_toolchain(name = "concrete_{tool}_toolchain", bin = "{tool}", template_var = "{tool_upper}_BIN")
toolchain(
name = "{tool}_toolchain",
{cfg}_compatible_with = {compatible_with},
toolchain = "concrete_{tool}_toolchain",
toolchain_type = "@aspect_rules_py//py/private/toolchain/{tool}:toolchain_type",
)
""".format(
cfg = cfg,
compatible_with = TOOLCHAIN_PLATFORMS[rctx.attr.platform].compatible_with,
tool = tool,
tool_upper = tool.upper(),
)
build_content += """py_tool_toolchain(name = "{tool}_toolchain", bin = "{tool}", template_var = "{tool_upper}_BIN")\n""".format(
tool = tool,
tool_upper = tool.upper(),
)

rctx.file("BUILD.bazel", build_content)

_tool_repo = repository_rule(
doc = "Download pre-built binary tools and create toolchains for them",
implementation = _tool_repo_impl,
prebuilt_tool_repo = repository_rule(
doc = "Download pre-built binary tools and create concrete toolchains for them",
implementation = _prebuilt_tool_repo_impl,
attrs = {
"platform": attr.string(mandatory = True, values = TOOLCHAIN_PLATFORMS.keys()),
}
},
)

def binary_tool_repos(name):
"""Create a downloaded toolchain for every tool under every supported platform.
Args:
name: prefix used in created repositories
Returns:
list of toolchain targets to register
"""
result = []
for platform in TOOLCHAIN_PLATFORMS.keys():
plat_repo_name = ".".join([name, platform])
result.append("@{}//:all".format(plat_repo_name))
_tool_repo(name = plat_repo_name, platform = platform)
return result
4 changes: 2 additions & 2 deletions py/private/toolchain/types.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ PY_TOOLCHAIN = "@bazel_tools//tools/python:toolchain_type"
SH_TOOLCHAIN = "@bazel_tools//tools/sh:toolchain_type"

# Toolchain type for the virtual env creation tools.
VENV_TOOLCHAIN = "@aspect_rules_py//py/private/toolchain/venv:toolchain_type"
UNPACK_TOOLCHAIN = "@aspect_rules_py//py/private/toolchain/unpack:toolchain_type"
VENV_TOOLCHAIN = "@aspect_rules_py//py/private/toolchain:venv_toolchain_type"
UNPACK_TOOLCHAIN = "@aspect_rules_py//py/private/toolchain:unpack_toolchain_type"
Loading

0 comments on commit 9ee2b08

Please sign in to comment.