Skip to content

Commit 70b7a53

Browse files
hoe-joLittleHuba
andauthored
[rules score] make sphinx build fully hermetic (#293)
* [rules score] make sphinx build fully hermetic - include graphviz - rework plantuml integration - include graphviz system deps via fakechroot Co-authored-by: Ulrich Huber <ulrich@huberulrich.de> * [rules score]: adapt integration of dot and plantuml into sphinx - exec_in_sysroot.sh: compute SYSROOT_INTERP and SYSROOT_LIBPATH, export them, and add SYSROOT_INTERP to FAKECHROOT_EXCLUDE_PATH so fakechroot does not intercept the interpreter's own exec - exec_in_sysroot.bzl: rewrite _setup_block() to invoke each sysroot_setup_commands entry via sysroot's ld-linux.so - dot.sh: replace /usr/bin/dot exec with sysroot-interpreter-based invocation using SYSROOT_INTERP / SYSROOT_LIBPATH / SYSROOT_DIR - BUILD: add exclude_paths = ["/tmp"] to the dot exec_in_sysroot target so PlantUML's temp .dot/.svg files in /tmp are not chrooted - Adapt conf.py to use the predefined variables * [rules score] refactor exec_in_sysroot - Remove duplicate implementation of sysroot-execution - Extract Shell script into template - Update Docs --------- Co-authored-by: Ulrich Huber <ulrich@huberulrich.de>
1 parent 7099e27 commit 70b7a53

23 files changed

Lines changed: 2625 additions & 261 deletions

MODULE.bazel

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,6 @@ bazel_dep(name = "bazel_skylib", version = "1.7.1")
3434
bazel_dep(name = "buildifier_prebuilt", version = "8.2.0.2")
3535
bazel_dep(name = "flatbuffers", version = "25.9.23")
3636
bazel_dep(name = "download_utils", version = "1.2.2")
37-
git_override(
38-
module_name = "download_utils",
39-
commit = "3b96912fb6622dda83f25efd1f8ae596fc4a63a6",
40-
remote = "https://gitlab.arm.com/bazel/download_utils.git",
41-
)
4237

4338
# flatbuffers depends on this transitively, but older grpc-java version
4439
# The main problem is that there the command `bazel mod deps` is broken, which
@@ -260,17 +255,24 @@ deb(
260255
)
261256

262257
###############################################################################
263-
# Graphviz deb package (cmake release; bundles all graphviz .so files so
264-
# dot_builtins runs without system graphviz installation)
265-
# Uses download_deb from @download_utils at a commit that includes
266-
# data.tar.gz support in download/deb/repository.bzl.
258+
# Hermetic doc-tool sysroot (docs_runtime)
259+
#
260+
# Distroless rootfs providing graphviz + fakechroot for hermetic dot execution
261+
# via //third_party/docs_runtime:dot (exec_in_sysroot).
267262
###############################################################################
268-
deb(
269-
name = "graphviz_deb",
270-
build = "//third_party/graphviz:graphviz.BUILD",
271-
integrity = "sha256-Jk5gSqo8l0INoY+kr1ZAsi2WhZY8LlAFlEag54H3Q2Q=",
272-
urls = ["https://gitlab.com/api/v4/projects/4207231/packages/generic/graphviz-releases/12.2.1/ubuntu_24.04_graphviz-12.2.1-cmake.deb"],
263+
bazel_dep(name = "rules_distroless", version = "0.6.2")
264+
265+
# bsdtar (used by //bazel/rules/exec_in_sysroot to extract sysroot archives).
266+
bazel_dep(name = "tar.bzl", version = "0.6.0")
267+
268+
apt = use_extension("@rules_distroless//apt:extensions.bzl", "apt")
269+
apt.install(
270+
name = "docs_runtime",
271+
lock = "//third_party/docs_runtime:docs_runtime.lock.json",
272+
manifest = "//third_party/docs_runtime:docs_runtime.yaml",
273+
mergedusr = True,
273274
)
275+
use_repo(apt, "docs_runtime")
274276

275277
register_toolchains(
276278
"//bazel/rules/rules_score:sphinx_default_toolchain",

bazel/rules/exec_in_sysroot/BUILD

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# *******************************************************************************
2+
# Copyright (c) 2026 Contributors to the Eclipse Foundation
3+
#
4+
# See the NOTICE file(s) distributed with this work for additional
5+
# information regarding copyright ownership.
6+
#
7+
# This program and the accompanying materials are made available under the
8+
# terms of the Apache License Version 2.0 which is available at
9+
# https://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# SPDX-License-Identifier: Apache-2.0
12+
# *******************************************************************************
13+
14+
load("@rules_shell//shell:sh_binary.bzl", "sh_binary")
15+
16+
package(default_visibility = ["//visibility:public"])
17+
18+
sh_binary(
19+
name = "exec_in_sysroot",
20+
srcs = ["exec_in_sysroot.sh"],
21+
)
22+
23+
# Referenced as implicit deps by the exec_in_sysroot / prepare_sysroot rules:
24+
# exec_in_sysroot.sh - shared sysroot launcher (also reused by
25+
# prepare_sysroot for sysroot_setup_commands).
26+
# exec_in_sysroot_launcher.sh.tpl - template for the generated runfiles-aware
27+
# launcher emitted by exec_in_sysroot.
28+
exports_files([
29+
"exec_in_sysroot.sh",
30+
"exec_in_sysroot_launcher.sh.tpl",
31+
])
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
# *******************************************************************************
2+
# Copyright (c) 2026 Contributors to the Eclipse Foundation
3+
#
4+
# See the NOTICE file(s) distributed with this work for additional
5+
# information regarding copyright ownership.
6+
#
7+
# This program and the accompanying materials are made available under the
8+
# terms of the Apache License Version 2.0 which is available at
9+
# https://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# SPDX-License-Identifier: Apache-2.0
12+
# *******************************************************************************
13+
_TAR_TOOLCHAIN_TYPE = "@tar.bzl//tar/toolchain:type"
14+
15+
def _merge_default_and_data_runfiles(target, runfiles):
16+
default_info = target[DefaultInfo]
17+
if default_info.default_runfiles:
18+
runfiles = runfiles.merge(default_info.default_runfiles)
19+
if default_info.data_runfiles:
20+
runfiles = runfiles.merge(default_info.data_runfiles)
21+
return runfiles
22+
23+
def _extract_and_clean(tar_bin, src, dest):
24+
"""POSIX-sh snippet: extract `src` into `dest`, then drop the symlinks that
25+
break Bazel TreeArtifact validation:
26+
* self-referential links (e.g. Debian x11-common's `usr/bin/X11 -> .`),
27+
which make validation recurse infinitely; and
28+
* now-dangling links, which validation also rejects.
29+
"""
30+
return (
31+
"mkdir -p \"" + dest + "\"\n" +
32+
"\"" + tar_bin + "\" -xf \"" + src + "\" -C \"" + dest + "\"\n" +
33+
"find \"" + dest + "\" -type l -lname '.' -delete\n" +
34+
"find \"" + dest + "\" -xtype l -delete\n"
35+
)
36+
37+
def _setup_block(sysroot_dir, exec_wrapper, host_setup_commands, sysroot_setup_commands):
38+
"""POSIX-sh snippet running the optional post-extract setup against
39+
`sysroot_dir` (an unquoted shell path expression) while it is still writable.
40+
41+
host_setup_commands run in the outer shell with $SYSROOT set to sysroot_dir.
42+
Use them for pure filesystem edits that only need the host shell.
43+
44+
sysroot_setup_commands are each executed inside the sysroot by delegating to
45+
the shared `exec_in_sysroot.sh` launcher (`exec_wrapper`) with SYSROOT_DIR
46+
set to sysroot_dir. This is the *same* launcher exec_in_sysroot uses at
47+
runtime, so build-time setup and runtime execution share a single
48+
implementation of "run a binary inside the sysroot" (the sysroot's own
49+
ld-linux.so + --library-path + LD_PRELOAD=libfakechroot.so + FAKECHROOT_BASE,
50+
giving a fully consistent single-libc environment).
51+
52+
Each entry in sysroot_setup_commands must be a space-separated ELF binary
53+
invocation starting with an absolute sysroot path, e.g. "/usr/bin/tool --flag".
54+
Shell metacharacters (pipes, redirects, etc.) are not supported.
55+
"""
56+
block = ""
57+
if host_setup_commands:
58+
block += "SYSROOT=\"" + sysroot_dir + "\"\n"
59+
block += "\n".join(host_setup_commands) + "\n"
60+
for cmd in sysroot_setup_commands:
61+
# Delegate to the shared launcher. `cmd` is intentionally left unquoted
62+
# so the shell word-splits "<binary> <args...>" into the launcher's
63+
# positional parameters (the space-separated contract documented above).
64+
block += (
65+
"SYSROOT_DIR=\"" + sysroot_dir + "\" " +
66+
"sh \"" + exec_wrapper + "\" " + cmd + "\n"
67+
)
68+
return block
69+
70+
def _prepare_sysroot_impl(ctx):
71+
if len(ctx.files.sysroot) != 1:
72+
fail("sysroot '{}' must provide exactly one archive file".format(ctx.attr.sysroot.label))
73+
74+
sysroot_archive = ctx.files.sysroot[0]
75+
bsdtar = ctx.toolchains[_TAR_TOOLCHAIN_TYPE]
76+
out_archive = ctx.actions.declare_file(ctx.label.name + ".tar")
77+
tar_bin = bsdtar.tarinfo.binary.path
78+
79+
command = (
80+
"set -eu\n" +
81+
"work=\"$(mktemp -d)\"\n" +
82+
"trap 'rm -rf \"$work\"' EXIT\n" +
83+
_extract_and_clean(tar_bin, sysroot_archive.path, "$work") +
84+
_setup_block("$work", ctx.file._exec_wrapper.path, ctx.attr.host_setup_commands, ctx.attr.sysroot_setup_commands) +
85+
"\"" + tar_bin + "\" -cf \"" + out_archive.path + "\" -C \"$work\" .\n"
86+
)
87+
88+
ctx.actions.run_shell(
89+
inputs = [sysroot_archive, ctx.file._exec_wrapper],
90+
outputs = [out_archive],
91+
tools = [bsdtar.default.files],
92+
command = command,
93+
mnemonic = "PrepareSysroot",
94+
progress_message = "Preparing sysroot archive %s" % ctx.label.name,
95+
)
96+
return [DefaultInfo(files = depset([out_archive]))]
97+
98+
prepare_sysroot = rule(
99+
implementation = _prepare_sysroot_impl,
100+
attrs = {
101+
"sysroot": attr.label(
102+
mandatory = True,
103+
allow_single_file = True,
104+
doc = "Input sysroot archive (e.g. a rules_distroless `:flat` tar).",
105+
),
106+
"host_setup_commands": attr.string_list(
107+
default = [],
108+
doc = "Shell lines run in the outer (host) shell after extraction while " +
109+
"the sysroot is still writable. $SYSROOT is set to the sysroot " +
110+
"directory. Use this for filesystem operations that only need the " +
111+
"host shell (e.g. removing files or creating symlinks with standard " +
112+
"shell tools such as find/rm/ln).",
113+
),
114+
"sysroot_setup_commands": attr.string_list(
115+
default = [],
116+
doc = "ELF binary invocations run inside the sysroot after " +
117+
"host_setup_commands complete. Each entry is word-split by the " +
118+
"shell to separate the binary path from its arguments; entries must " +
119+
"not contain arguments with embedded spaces, and shell metacharacters " +
120+
"(pipes, redirects) are not supported. The binary path must be " +
121+
"absolute within the sysroot (e.g. '/usr/bin/tool --flag'). The binary " +
122+
"is executed via the sysroot's own ld-linux.so with --library-path " +
123+
"pointing at the sysroot's /usr/lib tree, giving a fully consistent " +
124+
"single-libc environment. LD_PRELOAD=libfakechroot + FAKECHROOT_BASE " +
125+
"are still active so absolute filesystem calls are transparently " +
126+
"redirected into the sysroot. Requires fakechroot and " +
127+
"ld-linux.so to be present in the sysroot. NOTE: sysroot binaries are " +
128+
"executed via the sysroot's ELF interpreter on the host kernel, so the " +
129+
"sysroot architecture must match the host architecture.",
130+
),
131+
"_exec_wrapper": attr.label(
132+
default = Label("//bazel/rules/exec_in_sysroot:exec_in_sysroot.sh"),
133+
allow_single_file = True,
134+
doc = "Shared sysroot launcher reused to run sysroot_setup_commands, " +
135+
"so build-time setup and runtime execution use one implementation.",
136+
),
137+
},
138+
toolchains = [_TAR_TOOLCHAIN_TYPE],
139+
doc = """
140+
Unpacks a sysroot archive, removes symlinks that break Bazel TreeArtifact
141+
validation, runs optional host/sysroot setup commands while the tree is
142+
writable, and repackages the result into a single `<name>.tar` archive.
143+
""",
144+
)
145+
146+
def _exec_in_sysroot_impl(ctx):
147+
if len(ctx.files.sysroot) != 1:
148+
fail("sysroot '{}' must provide exactly one archive file".format(ctx.attr.sysroot.label))
149+
150+
sysroot_archive = ctx.files.sysroot[0]
151+
bsdtar = ctx.toolchains[_TAR_TOOLCHAIN_TYPE]
152+
sysroot = ctx.actions.declare_directory(ctx.label.name + "_sysroot")
153+
154+
# Extract the sysroot archive into a TreeArtifact so the wrapped executable
155+
# can reference it at action time via fakechroot. Any filesystem preparation
156+
# (plugin pruning, post-install commands, …) should be done upfront in a
157+
# prepare_sysroot rule; the symlink cleanup from _extract_and_clean still
158+
# runs here because Bazel rejects TreeArtifacts with broken symlinks.
159+
ctx.actions.run_shell(
160+
inputs = [sysroot_archive],
161+
outputs = [sysroot],
162+
tools = [bsdtar.default.files],
163+
command = "set -eu\n" + _extract_and_clean(
164+
bsdtar.tarinfo.binary.path,
165+
sysroot_archive.path,
166+
sysroot.path,
167+
),
168+
mnemonic = "ExecInSysrootExtract",
169+
progress_message = "Extracting sysroot %s" % ctx.label.name,
170+
)
171+
172+
sysroot_short_path = sysroot.short_path
173+
if sysroot_short_path.startswith("../"):
174+
sysroot_runfiles_path = sysroot_short_path[3:]
175+
else:
176+
sysroot_runfiles_path = ctx.workspace_name + "/" + sysroot_short_path
177+
178+
out = ctx.actions.declare_file(ctx.label.name)
179+
180+
# Build exclude paths string - colon-separated list
181+
exclude_paths = ":".join(ctx.attr.exclude_paths) if ctx.attr.exclude_paths else ""
182+
183+
ctx.actions.expand_template(
184+
template = ctx.file._launcher_template,
185+
output = out,
186+
is_executable = True,
187+
substitutions = {
188+
"{{WRAPPER_SHORT_PATH}}": ctx.workspace_name + "/" + ctx.executable._fakechroot_wrapper.short_path,
189+
"{{SYSROOT_SHORT_PATH}}": sysroot_runfiles_path,
190+
"{{SYSROOT_BINARY}}": ctx.attr.sysroot_binary,
191+
"{{EXCLUDE_PATHS}}": exclude_paths,
192+
},
193+
)
194+
195+
runfiles = ctx.runfiles(
196+
files = [ctx.executable._fakechroot_wrapper, sysroot],
197+
)
198+
runfiles = _merge_default_and_data_runfiles(ctx.attr._fakechroot_wrapper, runfiles)
199+
runfiles = _merge_default_and_data_runfiles(ctx.attr.sysroot, runfiles)
200+
201+
return [DefaultInfo(
202+
executable = out,
203+
files = depset([out]),
204+
runfiles = runfiles,
205+
)]
206+
207+
exec_in_sysroot = rule(
208+
implementation = _exec_in_sysroot_impl,
209+
executable = True,
210+
attrs = {
211+
"sysroot_binary": attr.string(
212+
mandatory = True,
213+
doc = "Sysroot-relative path of the ELF binary to execute, e.g. '/usr/bin/tool'.",
214+
),
215+
"sysroot": attr.label(
216+
mandatory = True,
217+
allow_single_file = True,
218+
doc = "Prepared sysroot archive, typically the output of a prepare_sysroot rule.",
219+
),
220+
"exclude_paths": attr.string_list(
221+
default = [],
222+
doc = "List of paths to exclude from fakechroot path-redirection.",
223+
),
224+
"_fakechroot_wrapper": attr.label(
225+
default = Label("//bazel/rules/exec_in_sysroot"),
226+
executable = True,
227+
cfg = "exec",
228+
doc = "Shared sysroot launcher (exec_in_sysroot.sh) invoked at runtime.",
229+
),
230+
"_launcher_template": attr.label(
231+
default = Label("//bazel/rules/exec_in_sysroot:exec_in_sysroot_launcher.sh.tpl"),
232+
allow_single_file = True,
233+
),
234+
},
235+
toolchains = [_TAR_TOOLCHAIN_TYPE],
236+
doc = """
237+
Produces an executable wrapper that runs a sysroot ELF binary via the
238+
sysroot's own ld-linux.so (SYSROOT_INTERP) and fakechroot (LD_PRELOAD),
239+
giving the binary a hermetic single-libc environment backed by the sysroot.
240+
241+
The sysroot archive is expected to be prepared by a prepare_sysroot rule,
242+
which performs filesystem preparation / post-install setup once and caches the result.
243+
""",
244+
)

0 commit comments

Comments
 (0)