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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions e2e/MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,8 @@ uv.project(
# }}}

use_repo(uv, "pypi")

# For cases/source-built-python
# {{{
include("//cases/source-built-python:source-built-python.MODULE.bazel")
# }}}
94 changes: 94 additions & 0 deletions e2e/cases/source-built-python/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
load("@aspect_rules_py//py/unstable:defs.bzl", "py_venv_test")
load("@bazel_skylib//rules:common_settings.bzl", "string_flag")
load("@rules_python//python:defs.bzl", "py_runtime", "py_runtime_pair")
load(":defs.bzl", "build_cpython", "python_interpreter_wrapper", "source_built_test")

# -- Build CPython 3.11.9 from source --

build_cpython(
name = "cpython",
src = "@cpython_src//:all_srcs",
)

# Wrapper script that sets PYTHONHOME before exec-ing the real interpreter
python_interpreter_wrapper(
name = "python3",
cpython = ":cpython",
)

# -- Toolchain registration --

py_runtime(
name = "source_built_runtime",
files = [":cpython"],
interpreter = ":python3",
interpreter_version_info = {
"major": "3",
"minor": "11",
"micro": "9",
},
python_version = "PY3",
)

py_runtime_pair(
name = "source_built_py_runtime_pair",
py3_runtime = ":source_built_runtime",
)

# Gate the toolchain behind a build setting so it doesn't affect other tests
string_flag(
name = "python_build_type",
build_setting_default = "prebuilt",
values = [
"prebuilt",
"source",
],
)

config_setting(
name = "is_source_built",
flag_values = {":python_build_type": "source"},
)

toolchain(
name = "source_built_toolchain",
target_settings = [":is_source_built"],
toolchain = ":source_built_py_runtime_pair",
toolchain_type = "@rules_python//python:toolchain_type",
)

# -- Tests --

# Inner test (built under the source-built Python toolchain via transition)
py_venv_test(
name = "_test_source_built_inner",
srcs = ["test_source_built.py"],
main = "test_source_built.py",
python_version = "3.11",
tags = ["manual"],
)

# Outer test that transitions to activate the source-built toolchain
source_built_test(
name = "test_source_built",
test = ":_test_source_built_inner",
)

# Inner test with sdist-built markupsafe (forced via no-binary-package in pyproject.toml)
py_venv_test(
name = "_test_with_deps_inner",
srcs = ["test_with_deps.py"],
main = "test_with_deps.py",
python_version = "3.11",
tags = ["manual"],
venv = "test",
deps = [
"@pypi//markupsafe",
],
)

# Outer test with deps, transitioned to use source-built debug Python
source_built_test(
name = "test_with_deps",
test = ":_test_with_deps_inner",
)
5 changes: 5 additions & 0 deletions e2e/cases/source-built-python/cpython_src.BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
filegroup(
name = "all_srcs",
srcs = glob(["**"]),
visibility = ["//visibility:public"],
)
157 changes: 157 additions & 0 deletions e2e/cases/source-built-python/defs.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
"""Helpers for the source-built Python e2e test."""

# -- Build CPython from source as a Bazel action --

def _build_cpython_impl(ctx):
"""Builds CPython from source using ./configure && make && make install."""
install_dir = ctx.actions.declare_directory(ctx.attr.name)

src_files = ctx.attr.src[DefaultInfo].files

# Find the configure script — pick the one with the shortest path
# (the top-level one, vs. subdirectory configure scripts).
configure = None
for f in src_files.to_list():
if f.basename == "configure":
if configure == None or len(f.path) < len(configure.path):
configure = f

if not configure:
fail("Could not find 'configure' script in source files")

ctx.actions.run_shell(
inputs = src_files,
outputs = [install_dir],
command = """\
set -euo pipefail
SRC_DIR="$(cd "$(dirname "{configure}")" && pwd)"
INSTALL_DIR="$(pwd)/{install_dir}"

cd "$SRC_DIR"
./configure \
--prefix="$INSTALL_DIR" \
--without-ensurepip \
--disable-test-modules \
--with-pydebug \
2>&1

make -j"$(nproc)" 2>&1
make install 2>&1
""".format(
configure = configure.path,
install_dir = install_dir.path,
),
mnemonic = "BuildCPython",
progress_message = "Building CPython from source",
use_default_shell_env = True,
)

return [DefaultInfo(files = depset([install_dir]))]

build_cpython = rule(
implementation = _build_cpython_impl,
attrs = {
"src": attr.label(mandatory = True),
},
)

# -- Wrapper script to set PYTHONHOME --

def _python_interpreter_wrapper_impl(ctx):
"""Creates a wrapper script that sets PYTHONHOME before exec-ing the real interpreter."""
cpython_tree = None
for f in ctx.attr.cpython[DefaultInfo].files.to_list():
if f.is_directory:
cpython_tree = f
break

if not cpython_tree:
fail("No directory (tree artifact) found in cpython outputs")

wrapper = ctx.actions.declare_file(ctx.attr.name)
ctx.actions.write(
output = wrapper,
content = """\
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
export PYTHONHOME="$SCRIPT_DIR/{tree_basename}"
exec "$PYTHONHOME/bin/{binary}" "$@"
""".format(
tree_basename = cpython_tree.basename,
binary = ctx.attr.binary,
),
is_executable = True,
)

return [DefaultInfo(
files = depset([wrapper]),
runfiles = ctx.runfiles(files = [cpython_tree]),
)]

python_interpreter_wrapper = rule(
implementation = _python_interpreter_wrapper_impl,
attrs = {
"cpython": attr.label(mandatory = True),
"binary": attr.string(default = "python3.11"),
},
)

# -- Transition to activate the source-built Python toolchain --

def _source_built_transition_impl(settings, attr):
return {"//cases/source-built-python:python_build_type": "source"}

_source_built_transition = transition(
implementation = _source_built_transition_impl,
inputs = [],
outputs = ["//cases/source-built-python:python_build_type"],
)

def _source_built_test_impl(ctx):
inner = ctx.attr.test[0]
inner_di = inner[DefaultInfo]

inner_executable = inner_di.files_to_run.executable

executable = ctx.actions.declare_file(ctx.attr.name + ".sh")
ctx.actions.write(
output = executable,
content = """\
#!/bin/bash
# Resolve RUNFILES_DIR from the Bazel test environment
if [[ -z "$RUNFILES_DIR" ]]; then
if [[ -d "$TEST_SRCDIR" ]]; then
RUNFILES_DIR="$TEST_SRCDIR"
elif [[ -d "$0.runfiles" ]]; then
RUNFILES_DIR="$0.runfiles"
fi
fi
exec "$RUNFILES_DIR/{workspace}/{inner}" "$@"
""".format(
workspace = ctx.workspace_name,
inner = inner_executable.short_path,
),
is_executable = True,
)

runfiles = ctx.runfiles(files = [inner_executable])
runfiles = runfiles.merge(inner_di.default_runfiles)

return [DefaultInfo(
executable = executable,
runfiles = runfiles,
)]

source_built_test = rule(
implementation = _source_built_test_impl,
test = True,
attrs = {
"test": attr.label(
mandatory = True,
cfg = _source_built_transition,
),
"_allowlist_function_transition": attr.label(
default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
),
},
)
15 changes: 15 additions & 0 deletions e2e/cases/source-built-python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[project]
name = "source-built-python-test"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = []

[dependency-groups]
test = [
"markupsafe>=2.1",
"build",
"setuptools",
]

[tool.uv]
no-binary-package = ["markupsafe"]
20 changes: 20 additions & 0 deletions e2e/cases/source-built-python/source-built-python.MODULE.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"Source-built Python interpreter e2e test"

http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

http_archive(
name = "cpython_src",
build_file = "//cases/source-built-python:cpython_src.BUILD.bazel",
sha256 = "e7de3240a8bc2b1e1ba5c81bf943f06861ff494b69fda990ce2722a504c6153d",
strip_prefix = "Python-3.11.9",
url = "https://www.python.org/ftp/python/3.11.9/Python-3.11.9.tgz",
)

register_toolchains("//cases/source-built-python:source_built_toolchain")

uv = use_extension("@aspect_rules_py//uv/unstable:extension.bzl", "uv")
uv.project(
hub_name = "pypi",
lock = "//cases/source-built-python:uv.lock",
pyproject = "//cases/source-built-python:pyproject.toml",
)
54 changes: 54 additions & 0 deletions e2e/cases/source-built-python/test_source_built.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Verify that the Python interpreter was built from source."""

import sys
import sysconfig


def test_version():
"""Check we're running on CPython 3.11.9."""
assert sys.version_info[:3] == (3, 11, 9), (
f"Expected CPython 3.11.9, got {sys.version}"
)


def test_source_built_prefix():
"""Check that the interpreter was built with a non-standard prefix.

Source-built interpreters have sysconfig CONFIG_ARGS reflecting the
configure invocation, and sys.prefix pointing to the build install tree
rather than a standard system path like /usr or /usr/local.
"""
config_args = sysconfig.get_config_var("CONFIG_ARGS") or ""
# The configure_make rule uses --prefix during the build
assert "--prefix" in config_args, (
f"Expected --prefix in CONFIG_ARGS, got: {config_args}"
)

# The prefix should NOT be a standard system path
prefix = sys.prefix
assert prefix not in ("/usr", "/usr/local", "/opt/homebrew"), (
f"Expected non-system prefix, got: {prefix}"
)


def test_debug_build():
"""Verify this is a debug build (--with-pydebug).

Debug builds set sys.abiflags = 'd' and SOABI contains 'cpython-311d'.
PBS never ships debug builds, so this definitively proves source-built.
"""
assert hasattr(sys, "abiflags"), "sys.abiflags not found"
assert "d" in sys.abiflags, (
f"Expected 'd' in abiflags (debug build), got: {sys.abiflags!r}"
)
soabi = sysconfig.get_config_var("SOABI") or ""
assert "cpython-311d" in soabi, (
f"Expected 'cpython-311d' in SOABI, got: {soabi!r}"
)


if __name__ == "__main__":
test_version()
test_source_built_prefix()
test_debug_build()
print("All checks passed: running on source-built debug CPython 3.11.9")
Loading
Loading