From ecd6801c2d2864f2126590b1eb42f971ae164b3f Mon Sep 17 00:00:00 2001 From: Jochen Hoenle <173445474+hoe-jo@users.noreply.github.com> Date: Wed, 24 Jun 2026 17:20:17 +0200 Subject: [PATCH 1/3] [rules score] make sphinx build fully hermetic - include graphviz - rework plantuml integration - include graphviz system deps via fakechroot Co-authored-by: Ulrich Huber --- MODULE.bazel | 25 +- .../rules/exec_in_sysroot}/BUILD | 11 +- .../rules/exec_in_sysroot/exec_in_sysroot.bzl | 297 ++++ .../rules/exec_in_sysroot/exec_in_sysroot.sh | 87 ++ bazel/rules/rules_score/BUILD | 8 - .../rules_score/docs/integration_guide.rst | 117 ++ .../rules_score/private/sphinx_module.bzl | 62 +- .../rules_score/templates/conf.template.py | 116 +- bazel/rules/rules_score/test/MODULE.bazel | 7 + third_party/docs_runtime/BUILD | 76 + third_party/docs_runtime/README.md | 65 + .../docs_runtime/docs_runtime.lock.json | 1387 +++++++++++++++++ third_party/docs_runtime/docs_runtime.yaml | 32 + third_party/docs_runtime/dot.sh | 17 + third_party/docs_runtime/tests/BUILD | 29 + .../docs_runtime/tests/dot_smoke_test.sh | 67 + third_party/graphviz/graphviz.BUILD | 49 - third_party/plantuml/BUILD | 23 + .../lobster_rst_report/lobster_rst_report.bzl | 18 +- tools/lobster_rst_report/rst_report.py | 5 +- tools/sphinx/BUILD | 11 - 21 files changed, 2302 insertions(+), 207 deletions(-) rename {third_party/graphviz => bazel/rules/exec_in_sysroot}/BUILD (64%) create mode 100644 bazel/rules/exec_in_sysroot/exec_in_sysroot.bzl create mode 100755 bazel/rules/exec_in_sysroot/exec_in_sysroot.sh create mode 100644 third_party/docs_runtime/BUILD create mode 100644 third_party/docs_runtime/README.md create mode 100644 third_party/docs_runtime/docs_runtime.lock.json create mode 100644 third_party/docs_runtime/docs_runtime.yaml create mode 100755 third_party/docs_runtime/dot.sh create mode 100644 third_party/docs_runtime/tests/BUILD create mode 100755 third_party/docs_runtime/tests/dot_smoke_test.sh delete mode 100644 third_party/graphviz/graphviz.BUILD create mode 100644 third_party/plantuml/BUILD diff --git a/MODULE.bazel b/MODULE.bazel index afa42287..353b3f6e 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -260,17 +260,24 @@ deb( ) ############################################################################### -# Graphviz deb package (cmake release; bundles all graphviz .so files so -# dot_builtins runs without system graphviz installation) -# Uses download_deb from @download_utils at a commit that includes -# data.tar.gz support in download/deb/repository.bzl. +# Hermetic doc-tool sysroot (docs_runtime) +# +# Distroless rootfs providing graphviz + fakechroot for hermetic dot execution +# via //third_party/docs_runtime:dot (exec_in_sysroot). ############################################################################### -deb( - name = "graphviz_deb", - build = "//third_party/graphviz:graphviz.BUILD", - integrity = "sha256-Jk5gSqo8l0INoY+kr1ZAsi2WhZY8LlAFlEag54H3Q2Q=", - urls = ["https://gitlab.com/api/v4/projects/4207231/packages/generic/graphviz-releases/12.2.1/ubuntu_24.04_graphviz-12.2.1-cmake.deb"], +bazel_dep(name = "rules_distroless", version = "0.6.2") + +# bsdtar (used by //bazel/rules/exec_in_sysroot to extract sysroot archives). +bazel_dep(name = "tar.bzl", version = "0.6.0") + +apt = use_extension("@rules_distroless//apt:extensions.bzl", "apt") +apt.install( + name = "docs_runtime", + lock = "//third_party/docs_runtime:docs_runtime.lock.json", + manifest = "//third_party/docs_runtime:docs_runtime.yaml", + mergedusr = True, ) +use_repo(apt, "docs_runtime") register_toolchains( "//bazel/rules/rules_score:sphinx_default_toolchain", diff --git a/third_party/graphviz/BUILD b/bazel/rules/exec_in_sysroot/BUILD similarity index 64% rename from third_party/graphviz/BUILD rename to bazel/rules/exec_in_sysroot/BUILD index e9f4f198..9e0f07ff 100644 --- a/third_party/graphviz/BUILD +++ b/bazel/rules/exec_in_sysroot/BUILD @@ -1,5 +1,5 @@ # ******************************************************************************* -# Copyright (c) 2025 Contributors to the Eclipse Foundation +# Copyright (c) 2026 Contributors to the Eclipse Foundation # # See the NOTICE file(s) distributed with this work for additional # information regarding copyright ownership. @@ -11,6 +11,9 @@ # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -# This package hosts the BUILD file used by the @graphviz_deb external repository. -# The download_deb rule from @download_utils extracts the Graphviz cmake -# release .deb and uses graphviz.BUILD as its top-level BUILD file. +load("@rules_shell//shell:sh_binary.bzl", "sh_binary") + +sh_binary( + name = "exec_in_sysroot", + srcs = ["exec_in_sysroot.sh"], +) diff --git a/bazel/rules/exec_in_sysroot/exec_in_sysroot.bzl b/bazel/rules/exec_in_sysroot/exec_in_sysroot.bzl new file mode 100644 index 00000000..017b7f6e --- /dev/null +++ b/bazel/rules/exec_in_sysroot/exec_in_sysroot.bzl @@ -0,0 +1,297 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +_TAR_TOOLCHAIN_TYPE = "@tar.bzl//tar/toolchain:type" + +def _merge_default_and_data_runfiles(target, runfiles): + default_info = target[DefaultInfo] + if default_info.default_runfiles: + runfiles = runfiles.merge(default_info.default_runfiles) + if default_info.data_runfiles: + runfiles = runfiles.merge(default_info.data_runfiles) + return runfiles + +def _extract_and_clean(tar_bin, src, dest): + """POSIX-sh snippet: extract `src` into `dest`, then drop the symlinks that + break Bazel TreeArtifact validation: + * self-referential links (e.g. Debian x11-common's `usr/bin/X11 -> .`), + which make validation recurse infinitely; and + * now-dangling links, which validation also rejects. + """ + return ( + "mkdir -p \"" + dest + "\"\n" + + tar_bin + " -xf " + src + " -C \"" + dest + "\"\n" + + "find \"" + dest + "\" -type l -lname '.' -delete\n" + + "find \"" + dest + "\" -xtype l -delete\n" + ) + +def _setup_block(sysroot_dir, host_setup_commands, sysroot_setup_commands): + """POSIX-sh snippet running the optional post-extract setup against + `sysroot_dir` (an unquoted shell path expression) while it is still writable. + + host_setup_commands run in the outer shell with $SYSROOT set to sysroot_dir. + + sysroot_setup_commands run inside a temporary /bin/sh script with + LD_PRELOAD=libfakechroot.so + FAKECHROOT_BASE=sysroot_dir, so absolute-path + accesses are transparently redirected into the sysroot. + """ + block = "" + if host_setup_commands: + block += "SYSROOT=\"" + sysroot_dir + "\"\n" + block += "\n".join(host_setup_commands) + "\n" + if sysroot_setup_commands: + script_lines = ["#!/bin/sh", "set -eu"] + sysroot_setup_commands + printf_calls = "\n".join([ + "printf '%s\\n' '" + line.replace("'", "'\\''") + "' >> \"$_FC_SCRIPT\"" + for line in script_lines + ]) + block += ( + "_FC_LIB=\"$(find \"" + sysroot_dir + "/usr/lib\" -path '*/fakechroot/libfakechroot.so' -type f 2>/dev/null | head -1 || true)\"\n" + + "if [ -z \"$_FC_LIB\" ]; then\n" + + " echo \"ERROR: sysroot_setup_commands require fakechroot, but libfakechroot.so was not found under " + sysroot_dir + "/usr/lib\" >&2\n" + + " exit 1\n" + + "fi\n" + + "_FC_SCRIPT=\"$(mktemp \"${TMPDIR:-/tmp}/_prepare_sysroot_setup_XXXXXX.sh\")\"\n" + + printf_calls + "\n" + + "chmod +x \"$_FC_SCRIPT\"\n" + + "LD_PRELOAD=\"$_FC_LIB\" " + + "LD_LIBRARY_PATH=\"$(dirname \"$_FC_LIB\")${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}\" " + + "FAKECHROOT_BASE=\"" + sysroot_dir + "\" " + + "FAKECHROOT_EXCLUDE_PATH=\"$_FC_SCRIPT\" \"$_FC_SCRIPT\" 2>&1\n" + + "rm -f \"$_FC_SCRIPT\"\n" + ) + return block + +def _prepare_sysroot_impl(ctx): + if len(ctx.files.sysroot) != 1: + fail("sysroot '{}' must provide exactly one archive file".format(ctx.attr.sysroot.label)) + + sysroot_archive = ctx.files.sysroot[0] + bsdtar = ctx.toolchains[_TAR_TOOLCHAIN_TYPE] + out_archive = ctx.actions.declare_file(ctx.label.name + ".tar") + work = out_archive.path + ".work" + tar_bin = bsdtar.tarinfo.binary.path + + command = ( + "set -eu\n" + + "rm -rf \"" + work + "\"\n" + + _extract_and_clean(tar_bin, sysroot_archive.path, work) + + _setup_block(work, ctx.attr.host_setup_commands, ctx.attr.sysroot_setup_commands) + + tar_bin + " -cf " + out_archive.path + " -C \"" + work + "\" .\n" + + "rm -rf \"" + work + "\"\n" + ) + + ctx.actions.run_shell( + inputs = [sysroot_archive], + outputs = [out_archive], + tools = [bsdtar.default.files], + command = command, + mnemonic = "PrepareSysroot", + progress_message = "Preparing sysroot archive %s" % ctx.label.name, + ) + return [DefaultInfo(files = depset([out_archive]))] + +prepare_sysroot = rule( + implementation = _prepare_sysroot_impl, + attrs = { + "sysroot": attr.label( + mandatory = True, + allow_single_file = True, + doc = "Input sysroot archive (e.g. a rules_distroless `:flat` tar).", + ), + "host_setup_commands": attr.string_list( + default = [], + doc = "Shell lines run in the outer (host) shell after extraction while " + + "the sysroot is still writable. $SYSROOT is set to the sysroot " + + "directory. Use this for filesystem operations that only need the " + + "host shell (e.g. removing unwanted plugins with find/rm).", + ), + "sysroot_setup_commands": attr.string_list( + default = [], + doc = "Shell lines run inside the sysroot via LD_PRELOAD=libfakechroot.so " + + "+ FAKECHROOT_BASE after host_setup_commands complete. All absolute " + + "path accesses are redirected into the sysroot. Use this for post-" + + "install steps that need the sysroot's own binaries (e.g. " + + "'/usr/bin/dot -c' to regenerate the graphviz plugin manifest). " + + "Requires fakechroot to be present in the sysroot.", + ), + }, + toolchains = [_TAR_TOOLCHAIN_TYPE], + doc = """ + Unpacks a sysroot archive, removes symlinks that break Bazel TreeArtifact + validation, runs optional host/sysroot setup commands while the tree is + writable, and repackages the result into a single `.tar` archive. + + """, +) + +def _exec_in_sysroot_impl(ctx): + if len(ctx.files.sysroot) != 1: + fail("sysroot '{}' must provide exactly one archive file".format(ctx.attr.sysroot.label)) + + sysroot_archive = ctx.files.sysroot[0] + bsdtar = ctx.toolchains[_TAR_TOOLCHAIN_TYPE] + sysroot = ctx.actions.declare_directory(ctx.label.name + "_sysroot") + + # Extract the sysroot archive into a TreeArtifact so the wrapped executable + # can reference it at action time via fakechroot. Any filesystem preparation + # (plugin pruning, post-install commands, …) should be done upfront in a + # prepare_sysroot rule; the symlink cleanup from _extract_and_clean still + # runs here because Bazel rejects TreeArtifacts with broken symlinks. + ctx.actions.run_shell( + inputs = [sysroot_archive], + outputs = [sysroot], + tools = [bsdtar.default.files], + command = "set -eu\n" + _extract_and_clean( + bsdtar.tarinfo.binary.path, + sysroot_archive.path, + sysroot.path, + ), + mnemonic = "ExecInSysrootExtract", + progress_message = "Extracting sysroot %s" % ctx.label.name, + ) + + sysroot_short_path = sysroot.short_path + if sysroot_short_path.startswith("../"): + sysroot_runfiles_path = sysroot_short_path[3:] + else: + sysroot_runfiles_path = ctx.workspace_name + "/" + sysroot_short_path + + executable_file = ctx.executable.executable + if executable_file == None: + fail("executable must provide a runnable target") + executable_short_path = executable_file.short_path + if executable_short_path.startswith("../"): + executable_runfiles_path = executable_short_path[3:] + else: + executable_runfiles_path = ctx.workspace_name + "/" + executable_short_path + + out = ctx.actions.declare_file(ctx.label.name) + + # Build exclude paths string - colon-separated list + exclude_paths = ":".join(ctx.attr.exclude_paths) if ctx.attr.exclude_paths else "" + + wrapper_script = """#!/usr/bin/env bash +set -euo pipefail + +# --- begin runfiles.bash initialization --- +if [[ ! -d "${{RUNFILES_DIR:-/dev/null}}" && ! -f "${{RUNFILES_MANIFEST_FILE:-/dev/null}}" ]]; then + if [[ -f "$0.runfiles_manifest" ]]; then + export RUNFILES_MANIFEST_FILE="$0.runfiles_manifest" + elif [[ -f "$0.runfiles/MANIFEST" ]]; then + export RUNFILES_MANIFEST_FILE="$0.runfiles/MANIFEST" + elif [[ -f "$0.runfiles/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then + export RUNFILES_DIR="$0.runfiles" + fi +fi +if [[ -f "${{RUNFILES_DIR:-/dev/null}}/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then + source "${{RUNFILES_DIR}}/bazel_tools/tools/bash/runfiles/runfiles.bash" +elif [[ -f "${{RUNFILES_MANIFEST_FILE:-/dev/null}}" ]]; then + source "$(grep -m1 '^bazel_tools/tools/bash/runfiles/runfiles.bash ' "$RUNFILES_MANIFEST_FILE" | cut -d ' ' -f 2-)" +else + echo >&2 "ERROR: cannot find @bazel_tools//tools/bash/runfiles:runfiles.bash" + exit 1 +fi +# --- end runfiles.bash initialization --- + +FAKECHROOT_WRAPPER="$(rlocation '{wrapper_short_path}')" +SYSROOT_DIR="$(rlocation '{sysroot_short_path}')" +EXECUTABLE_FILE="$(rlocation '{executable_runfiles_path}')" + +if [[ -z "${{FAKECHROOT_WRAPPER}}" || ! -x "${{FAKECHROOT_WRAPPER}}" ]]; then + echo "ERROR: could not resolve fakechroot wrapper: {wrapper_short_path}" >&2 + exit 1 +fi + +if [[ -z "${{SYSROOT_DIR}}" || ! -d "${{SYSROOT_DIR}}" ]]; then + echo "ERROR: could not resolve sysroot directory: {sysroot_short_path}" >&2 + exit 1 +fi + +if [[ ! -x "${{SYSROOT_DIR}}/usr/bin/fakechroot" ]]; then + echo "ERROR: sysroot does not provide /usr/bin/fakechroot: ${{SYSROOT_DIR}}" >&2 + exit 1 +fi + +if [[ -z "${{EXECUTABLE_FILE}}" || ! -f "${{EXECUTABLE_FILE}}" ]]; then + echo "ERROR: could not resolve executable target: {executable_runfiles_path}" >&2 + exit 1 +fi + +export SYSROOT_DIR +if [[ -n "{exclude_paths}" ]]; then + export FAKECHROOT_EXCLUDE_PATH="{exclude_paths}" +fi + +# The executable lives in host runfiles, not in the sysroot. Exclude its path +# so fakechroot does not redirect accesses to it into the sysroot. +EXECUTABLE_DIR="$(dirname "${{EXECUTABLE_FILE}}")" +if [[ -n "${{FAKECHROOT_EXCLUDE_PATH:-}}" ]]; then + export FAKECHROOT_EXCLUDE_PATH="${{EXECUTABLE_DIR}}:${{EXECUTABLE_FILE}}:${{FAKECHROOT_EXCLUDE_PATH}}" +else + export FAKECHROOT_EXCLUDE_PATH="${{EXECUTABLE_DIR}}:${{EXECUTABLE_FILE}}" +fi + +exec "${{FAKECHROOT_WRAPPER}}" "${{EXECUTABLE_FILE}}" "$@" +""".format( + wrapper_short_path = ctx.workspace_name + "/" + ctx.executable._fakechroot_wrapper.short_path, + sysroot_short_path = sysroot_runfiles_path, + executable_runfiles_path = executable_runfiles_path, + exclude_paths = exclude_paths, + ) + ctx.actions.write(output = out, content = wrapper_script, is_executable = True) + + runfiles = ctx.runfiles( + files = [out, ctx.executable._fakechroot_wrapper, sysroot, executable_file] + ctx.files._bash_runfiles, + ) + runfiles = _merge_default_and_data_runfiles(ctx.attr.executable, runfiles) + runfiles = _merge_default_and_data_runfiles(ctx.attr._fakechroot_wrapper, runfiles) + runfiles = _merge_default_and_data_runfiles(ctx.attr._bash_runfiles, runfiles) + runfiles = _merge_default_and_data_runfiles(ctx.attr.sysroot, runfiles) + + return [DefaultInfo( + executable = out, + files = depset([out]), + runfiles = runfiles, + )] + +exec_in_sysroot = rule( + implementation = _exec_in_sysroot_impl, + executable = True, + attrs = { + "executable": attr.label(mandatory = True, executable = True, cfg = "exec"), + "sysroot": attr.label(mandatory = True, allow_single_file = True), + "exclude_paths": attr.string_list( + default = [], + doc = "Paths to exclude from fakechroot path-redirection (colon-separated).", + ), + "_bash_runfiles": attr.label( + default = Label("@bazel_tools//tools/bash/runfiles"), + allow_files = True, + ), + "_fakechroot_wrapper": attr.label( + default = Label("//bazel/rules/exec_in_sysroot"), + executable = True, + cfg = "exec", + ), + }, + toolchains = [_TAR_TOOLCHAIN_TYPE], + doc = """ + Produces an executable wrapper that runs a given executable target using the + supplied sysroot archive. The archive is unpacked in-rule and the wrapped + executable runs within fakechroot via LD_PRELOAD, allowing access to sysroot + tools and libraries hermetically. + + The archive is expected to be a reworked sysroot (see prepare_sysroot), which + performs plugin pruning / post-install setup once and caches the result. + """, +) diff --git a/bazel/rules/exec_in_sysroot/exec_in_sysroot.sh b/bazel/rules/exec_in_sysroot/exec_in_sysroot.sh new file mode 100755 index 00000000..a0752917 --- /dev/null +++ b/bazel/rules/exec_in_sysroot/exec_in_sysroot.sh @@ -0,0 +1,87 @@ +#!/bin/sh +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +# Generic wrapper script that executes a command in fakechroot using the provided sysroot. +# +# Rather than using `fakechroot -- chroot SYSROOT COMMAND` (which requires COMMAND +# to exist inside the chroot), this script uses fakechroot's LD_PRELOAD mechanism +# directly: libfakechroot.so intercepts all file-system calls and redirects absolute +# paths to the sysroot, while FAKECHROOT_EXCLUDE_PATH allows the host-side launcher +# script to be found at its real location. +# +# Usage: +# exec_in_sysroot.sh [arguments...] +# +# Environment variables (set by the generated exec_in_sysroot wrapper): +# SYSROOT_DIR - Path to the extracted sysroot directory (required) +# FAKECHROOT_EXCLUDE_PATH - Colon-separated paths NOT to redirect (the host +# executable and its runfiles dir are pre-excluded) +# BUILD_WORKSPACE_DIRECTORY - Workspace root, automatically excluded (optional) + +set -eu + +COMMAND_IN_SYSROOT="${1:?Command must be provided as first argument}" +shift || true + +SYSROOT_DIR="${SYSROOT_DIR:?SYSROOT_DIR must be set}" + +FAKECHROOT_LIB="" +if [ -f "${SYSROOT_DIR}/usr/lib/x86_64-linux-gnu/fakechroot/libfakechroot.so" ]; then + FAKECHROOT_LIB="${SYSROOT_DIR}/usr/lib/x86_64-linux-gnu/fakechroot/libfakechroot.so" +elif [ -f "${SYSROOT_DIR}/usr/lib/aarch64-linux-gnu/fakechroot/libfakechroot.so" ]; then + FAKECHROOT_LIB="${SYSROOT_DIR}/usr/lib/aarch64-linux-gnu/fakechroot/libfakechroot.so" +fi +if [ -z "${FAKECHROOT_LIB}" ]; then + FAKECHROOT_LIB="$(find "${SYSROOT_DIR}/usr/lib" -path '*/fakechroot/libfakechroot.so' -type f | head -1 || true)" +fi +if [ -z "${FAKECHROOT_LIB}" ]; then + echo "ERROR: libfakechroot.so not found in sysroot" >&2 + exit 1 +fi + +FAKECHROOT_LIB_DIR="$(dirname "${FAKECHROOT_LIB}")" + +# Build the exclude paths list. +# Always exclude BUILD_WORKSPACE_DIRECTORY so workspace operations stay on host. +EXCLUDE_PATHS="" +if [ -n "${BUILD_WORKSPACE_DIRECTORY:-}" ]; then + EXCLUDE_PATHS="${BUILD_WORKSPACE_DIRECTORY}" +fi + +# Append any user-specified exclude paths. +if [ -n "${FAKECHROOT_EXCLUDE_PATH:-}" ]; then + if [ -n "${EXCLUDE_PATHS}" ]; then + EXCLUDE_PATHS="${EXCLUDE_PATHS}:${FAKECHROOT_EXCLUDE_PATH}" + else + EXCLUDE_PATHS="${FAKECHROOT_EXCLUDE_PATH}" + fi +fi + +if [ -n "${EXCLUDE_PATHS}" ]; then + export FAKECHROOT_EXCLUDE_PATH="${EXCLUDE_PATHS}" +fi + +# Wire libfakechroot.so as LD_PRELOAD and point FAKECHROOT_BASE at the sysroot +# directory. All absolute file-system calls (open, stat, execve, …) inside the +# launched process are transparently redirected to SYSROOT_DIR/ unless the +# path is listed in FAKECHROOT_EXCLUDE_PATH. +# +# Only the fakechroot-lib directory is added to LD_LIBRARY_PATH; adding the full +# sysroot lib tree would make the host linker pick up sysroot-snapshot versions of +# libgvc/libc/… which may differ from the build the sysroot binary was built with +# and cause crashes. +export FAKECHROOT_BASE="${SYSROOT_DIR}" +export LD_PRELOAD="${FAKECHROOT_LIB}${LD_PRELOAD:+:${LD_PRELOAD}}" +export LD_LIBRARY_PATH="${FAKECHROOT_LIB_DIR}${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" + +exec "${COMMAND_IN_SYSROOT}" "$@" diff --git a/bazel/rules/rules_score/BUILD b/bazel/rules/rules_score/BUILD index 970b4a7b..c67c552d 100644 --- a/bazel/rules/rules_score/BUILD +++ b/bazel/rules/rules_score/BUILD @@ -189,13 +189,6 @@ sphinx_module( py_binary( name = "raw_build", srcs = ["src/sphinx_wrapper.py"], - data = [ - "//tools/sphinx:plantuml", - # Ship the FTA metamodel in the docs-build runfiles so conf.py can put it - # on PlantUML's include path; FTA diagrams keep ``!include - # fta_metamodel.puml`` and resolve it even under -pipe rendering. - "//plantuml:fta_metamodel", - ], env = { "SOURCE_DIRECTORY": "", "DATA": "", @@ -206,7 +199,6 @@ py_binary( deps = [ ":sphinx_module_ext", "@lobster//sphinx_lobster:sphinx_lobster_builder", - "@rules_python//python/runfiles", "@score_tooling//plantuml/sphinx/clickable_plantuml", "@trlc//tools/sphinx/extensions/trlc", requirement("sphinx"), diff --git a/bazel/rules/rules_score/docs/integration_guide.rst b/bazel/rules/rules_score/docs/integration_guide.rst index 313df2f1..313f7b7e 100644 --- a/bazel/rules/rules_score/docs/integration_guide.rst +++ b/bazel/rules/rules_score/docs/integration_guide.rst @@ -175,3 +175,120 @@ Design Rationale 6. **Build System Integration** — Bazel ensures reproducible, cacheable documentation builds Reference implementation: `examples/seooc `_ in the score-tooling repository. + +--- + +.. _sphinx-hermetic-tool-setup: + +Hermetic Diagram Tools (Graphviz and PlantUML) +---------------------------------------------- + +The Sphinx HTML action shells out to two diagram tools at **runtime** (inside +Bazel actions): ``dot`` from Graphviz and PlantUML. Both are hermetic — +i.e.\ no host installation required. The two tools use different +delivery mechanisms, described below. + +Graphviz / ``dot`` +~~~~~~~~~~~~~~~~~~ + +**Source and packaging** + +Graphviz now comes directly from the docs runtime sysroot +(``@docs_runtime//:flat``), built with ``rules_distroless`` from +``//third_party/docs_runtime/docs_runtime.yaml``. The Sphinx action does not +call ``dot`` directly; it uses ``//third_party/docs_runtime:dot`` — an +``exec_in_sysroot`` wrapper that unpacks the sysroot archive and runs +``/usr/bin/dot`` inside it through ``fakechroot``. + +**Where the files land (execroot-relative paths)** + +.. code-block:: text + + bazel-bin/third_party/docs_runtime/dot ← GRAPHVIZ_DOT env var + bazel-bin/third_party/docs_runtime/dot_sysroot/ ← unpacked docs_runtime rootfs + usr/bin/dot + usr/lib/graphviz/... + usr/bin/fakechroot + +**Wiring into the Sphinx action** + +The Bazel rule sets one variable: + +.. list-table:: + :widths: 30 70 + :header-rows: 1 + + * - Env var + - Content + * - ``GRAPHVIZ_DOT`` + - Path to the ``dot`` binary +The value points to the hermetic wrapper executable. The wrapper resolves and +executes graphviz from the sysroot itself, so no custom ``LD_LIBRARY_PATH`` / +``GVBINDIR`` wiring is required in the Sphinx action. + +**Resolving paths in conf.py** + +``GRAPHVIZ_DOT`` is set as an *execroot-relative* path. Because Sphinx changes +the process working directory during the build, these paths would break if +used as-is. ``conf.template.py`` therefore: + +1. Captures ``_EXECROOT = Path.cwd()`` at **module import time** (cwd is still + the execroot at that point). +2. Calls ``_resolve_execroot_path()`` on the value to prepend ``_EXECROOT`` + and produce absolute paths. + +PlantUML +~~~~~~~~ + +**Source and packaging** + +PlantUML is fetched from **Maven Central** via ``rules_jvm_external`` +(declared in ``MODULE.bazel``). It is wrapped as a ``java_binary`` at +``//tools/sphinx:plantuml`` in ``tools/sphinx/BUILD``. + +The PlantUML target is added to the sphinx-build binary (``raw_build``) as a +``data`` dependency, making it a **runfile** of that binary — not an +independent action input. + +**Where the file lands (runfiles-relative path)** + +.. code-block:: text + + {sphinx_build_binary}.runfiles/ + {repo_name}/tools/sphinx/plantuml ← wrapper script (absolute path via Runfiles API) + +The ``{repo_name}`` prefix depends on the Bzlmod configuration: + +- ``_main`` — when score_tooling is the root module (e.g.\ building within + the score-tooling repo itself) +- ``score_tooling`` / ``score_tooling+`` / ``score_tooling~`` — when + score_tooling is an external dependency of another project + +**Discovering the binary in conf.py** + +Because the repo name varies, ``conf.template.py`` uses two-stage discovery: + +1. **Manifest scan (primary):** Read ``RUNFILES_MANIFEST_FILE`` and search + for any entry whose runfiles path ends in ``/tools/sphinx/plantuml``. This + requires no knowledge of the repo name prefix. +2. **Runfiles API fallback:** If no manifest file is available (directory-based + runfiles trees on some platforms), fall back to + ``Runfiles.Create().Rlocation()`` with a list of known repo-name candidates + (``_main``, ``score_tooling``, ``score_tooling+``, …). + +The Runfiles API returns an **absolute path** directly, so no +``_resolve_execroot_path()`` is required for PlantUML. + +**Connecting PlantUML to Graphviz** + +Once both paths are resolved, ``conf.template.py`` assembles the PlantUML +command: + +.. code-block:: python + + plantuml = f"{plantuml_path} -graphvizdot {graphviz_dot}" + +The ``-graphvizdot`` flag makes PlantUML use the hermetic ``dot`` binary for +diagram layout instead of its bundled Java port (Smetana). This ensures that +the graphviz version is identical for both ``sphinx.ext.graphviz`` directives +and PlantUML diagrams. diff --git a/bazel/rules/rules_score/private/sphinx_module.bzl b/bazel/rules/rules_score/private/sphinx_module.bzl index ac230727..3ea4c621 100644 --- a/bazel/rules/rules_score/private/sphinx_module.bzl +++ b/bazel/rules/rules_score/private/sphinx_module.bzl @@ -69,6 +69,16 @@ sphinx_rule_attrs = dict( "deps": attr.label_list( doc = "List of other sphinx_module targets this module depends on for intersphinx.", ), + "_plantuml": attr.label( + default = Label("//third_party/plantuml:plantuml"), + executable = True, + cfg = "exec", + ), + "_graphviz": attr.label( + default = Label("//third_party/docs_runtime:dot"), + executable = True, + cfg = "exec", + ), }, **VERBOSITY_ATTR ) @@ -110,10 +120,16 @@ def _score_needs_impl(ctx): inputs = needs_inputs, outputs = [needs_output], arguments = needs_args, + env = { + "PLANTUML_BIN": ctx.executable._plantuml.path, + "GRAPHVIZ_DOT": ctx.executable._graphviz.path, + }, progress_message = "Generating needs.json for: %s" % ctx.label.name, executable = sphinx_toolchain.sphinx.files_to_run.executable, tools = [ sphinx_toolchain.sphinx.files_to_run, + ctx.attr._plantuml.files_to_run, + ctx.attr._graphviz.files_to_run, ], ) transitive_needs = [dep[SphinxNeedsInfo].needs_json_files for dep in ctx.attr.deps if SphinxNeedsInfo in dep] @@ -238,39 +254,24 @@ def _score_html_impl(ctx): get_log_level(ctx), ] - # Wire in the hermetic graphviz deb (dot_builtins + bundled shared libs) if provided. - # conf.template.py resolves all three env vars (GRAPHVIZ_DOT, - # LD_LIBRARY_PATH, LTDL_LIBRARY_PATH) from execroot-relative to absolute - # paths so dot_builtins can load its plugins without a system installation. - graphviz_env = {} - graphviz_files = ctx.files.graphviz - if graphviz_files: - _dot_suffix = "/usr/bin/dot_builtins" - dot_binary = None - for f in graphviz_files: - if f.path.endswith(_dot_suffix): - dot_binary = f - break - if not dot_binary: - fail("graphviz target {} must provide usr/bin/dot_builtins".format(ctx.attr.graphviz)) - - graphviz_prefix = dot_binary.path[:-len(_dot_suffix)] - graphviz_env = { - "GRAPHVIZ_DOT": dot_binary.path, - "LD_LIBRARY_PATH": graphviz_prefix + "/usr/lib", - "LTDL_LIBRARY_PATH": graphviz_prefix + "/usr/lib/graphviz", - } - html_inputs = html_inputs + graphviz_files + # Use the hermetic graphviz wrapper that executes `/usr/bin/dot` inside the + # docs_runtime sysroot via exec_in_sysroot. + action_env = { + "PLANTUML_BIN": ctx.executable._plantuml.path, + "GRAPHVIZ_DOT": ctx.executable._graphviz.path, + } ctx.actions.run( inputs = html_inputs, outputs = [sphinx_html_output], arguments = html_args + [args], - env = graphviz_env, + env = action_env, progress_message = "Building HTML: %s" % ctx.label.name, executable = sphinx_toolchain.sphinx.files_to_run.executable, tools = [ sphinx_toolchain.sphinx.files_to_run, + ctx.attr._plantuml.files_to_run, + ctx.attr._graphviz.files_to_run, ], ) @@ -358,13 +359,6 @@ _score_html = rule( "destination paths relative to the Sphinx source root. Exactly one " + "file per label. Mirrors sphinx_docs.renamed_srcs from rules_python.", ), - graphviz = attr.label( - default = None, - allow_files = True, - doc = "Graphviz cmake-release deb files (dot_builtins binary + bundled libs). " + - "Only available on Linux x86_64; provides a hermetic 'dot' binary without requiring a system graphviz installation. " + - "Defaults to @graphviz_deb//:all on Linux x86_64.", - ), ), toolchains = ["//bazel/rules/rules_score:toolchain_type"], ) @@ -383,7 +377,6 @@ def sphinx_module( strip_prefix = "", extra_opts = [], extra_opts_targets = [], - graphviz = None, testonly = False, **kwargs): """Build a Sphinx module with transitive HTML dependencies. @@ -408,10 +401,6 @@ def sphinx_module( extra_opts_targets: {type}`list[label]` Label targets that resolve to extra Sphinx arguments at analysis time. Each target must provide FilteredExecpathInfo (e.g. filter_execpath targets). - graphviz: Graphviz cmake-release deb files (dot_builtins + bundled libs). On Linux x86_64, - defaults to @graphviz_deb//:all for hermetic graphviz support. On other platforms - or if explicitly set to None, no graphviz support is provided (the sphinx.ext.graphviz - extension will not be available). visibility: Bazel visibility """ _score_needs( @@ -432,7 +421,6 @@ def sphinx_module( needs = [d + "_needs" for d in deps], extra_opts = extra_opts, extra_opts_targets = extra_opts_targets, - graphviz = graphviz, testonly = testonly, **kwargs ) diff --git a/bazel/rules/rules_score/templates/conf.template.py b/bazel/rules/rules_score/templates/conf.template.py index a694e406..58d89e6b 100644 --- a/bazel/rules/rules_score/templates/conf.template.py +++ b/bazel/rules/rules_score/templates/conf.template.py @@ -22,9 +22,7 @@ import os import sys from pathlib import Path -from typing import List -from python.runfiles import Runfiles from sphinx.util import logging # Create a logger with the Sphinx namespace @@ -45,9 +43,9 @@ def _resolve_execroot_path(path_value: str) -> str: """Resolve an execroot-relative path to an absolute filesystem path. Bazel passes action inputs as paths relative to the execroot (e.g. - ``external/+_repo_rules2+graphviz_deb/usr/bin/dot_builtins``). Those - paths are only valid when the process' cwd is the execroot — which is - not guaranteed once Sphinx changes directories during the build. + ``bazel-bin/third_party/docs_runtime/dot``). Those paths are only valid + when the process' cwd is the execroot — which is not guaranteed once + Sphinx changes directories during the build. This function makes them absolute so they work regardless of cwd. Absolute paths and plain command names (e.g. ``dot``) are returned @@ -169,104 +167,48 @@ def _resolve_execroot_path(path_value: str) -> str: needs_id_regex = "^[A-Za-z0-9_-]{6,}" -# Use the runfiles to find the plantuml binary. -# Runfiles are only available when running in Bazel. -r = Runfiles.Create() -if r is None: - raise ValueError("Could not initialize Bazel runfiles.") - -plantuml_repo_candidates: List[str] = [] - -for repo_name in [ - os.environ.get("TEST_WORKSPACE"), - "_main", - "score_tooling", - "score_tooling~", - "score_tooling+", -]: - if repo_name and repo_name not in plantuml_repo_candidates: - plantuml_repo_candidates.append(repo_name) - -plantuml_runfiles_candidates = [ - f"{repo_name}/tools/sphinx/plantuml" for repo_name in plantuml_repo_candidates -] - -plantuml_path = None -for runfile_path in plantuml_runfiles_candidates: - candidate = r.Rlocation(runfile_path, source_repo="") - if candidate and Path(candidate).exists(): - plantuml_path = Path(candidate) - logger.info(f"Selected PlantUML from runfiles path {runfile_path}: {candidate}") - break - -if plantuml_path is None: - searched = ", ".join(plantuml_runfiles_candidates) +# --------------------------------------------------------------------------- +# PlantUML binary discovery +# --------------------------------------------------------------------------- +# PLANTUML_BIN is set by the Bazel sphinx_module rule to the explicit execroot- +# relative path of the //third_party/plantuml:plantuml java_binary launcher script. +_plantuml_bin = os.environ.get("PLANTUML_BIN") +if not _plantuml_bin: raise ValueError( - f"Could not find plantuml binary via runfiles lookup. Searched: {searched}." - ) - -# Locate the FTA metamodel in the docs-build runfiles (shipped via -# //tools/sphinx:sphinx-build data) so it can be put on PlantUML's include path. -# FTA diagrams keep their ``!include fta_metamodel.puml``; sphinxcontrib-plantuml -# renders via ``-pipe`` (no source-file dir), so a relative include only resolves -# through ``plantuml.include.path``. -fta_metamodel_dir = "" -for repo_name in plantuml_repo_candidates: - candidate = r.Rlocation(f"{repo_name}/plantuml/fta_metamodel.puml", source_repo="") - if candidate and Path(candidate).exists(): - fta_metamodel_dir = str(Path(candidate).parent) - logger.info(f"Found fta_metamodel.puml on include path: {candidate}") - break - -if not fta_metamodel_dir: - logger.warning( - "fta_metamodel.puml not found in runfiles — FTA diagrams using " - "!include fta_metamodel.puml will fail to render. " - f"Searched repo candidates: {plantuml_repo_candidates}" + "PLANTUML_BIN environment variable is not set. It must point at the " + "//third_party/plantuml:plantuml launcher and is normally provided by the " + "sphinx_module Bazel rule. If you are invoking Sphinx outside that rule, " + "set PLANTUML_BIN to the plantuml binary path." ) +plantuml_path = _resolve_execroot_path(_plantuml_bin) -# Use PlantUML's built-in Smetana layout engine (Java port of Graphviz). -# This avoids requiring an external dot binary in the Bazel sandbox. -# ``--jvm_flag`` is consumed by the java_binary launcher and sets the JVM system -# property PlantUML reads for its include search path. It must precede the -# program args (``-Playout`` etc.), or the launcher forwards it to PlantUML -# (which ignores it) instead of the JVM. -_include_flag = ( - f" --jvm_flag=-Dplantuml.include.path={fta_metamodel_dir}" - if fta_metamodel_dir - else "" -) -plantuml = f"{plantuml_path}{_include_flag} -Playout=smetana" plantuml_output_format = "svg_obj" +# `plantuml` is defined below, after graphviz_dot is resolved, so PlantUML can +# render with the same hermetic Graphviz dot (see end of the Graphviz section). # --------------------------------------------------------------------------- # Graphviz (sphinx.ext.graphviz) # --------------------------------------------------------------------------- -# GRAPHVIZ_DOT is set by the Bazel sphinx_module rule to point at the hermetic -# dot_builtins binary from @graphviz_deb. The path is execroot-relative, so -# we resolve it to an absolute path here so it remains valid after any cwd -# change that Sphinx may perform during the build. +# GRAPHVIZ_DOT is set by the Bazel sphinx_module rule to point at the +# exec_in_sysroot wrapper from //third_party/docs_runtime:dot. +# The wrapper executes /usr/bin/dot inside the hermetic docs_runtime sysroot. +# Paths are passed execroot-relative, so resolve to absolute for robustness if +# Sphinx changes cwd before spawning dot. # If GRAPHVIZ_DOT is absent, force a known-invalid dot path so Sphinx fails # clearly on graphviz directives instead of silently using host-installed dot. if "GRAPHVIZ_DOT" in os.environ: graphviz_dot = _resolve_execroot_path(os.environ["GRAPHVIZ_DOT"]) graphviz_output_format = "svg" - - # LD_LIBRARY_PATH and LTDL_LIBRARY_PATH are set by the Bazel rule as - # execroot-relative paths. We mutate os.environ (not just a local) because - # sphinx.ext.graphviz spawns `dot` as a child process that inherits these - # variables to locate the bundled shared libraries and plugins. Each - # component is resolved to absolute so it stays valid if Sphinx changes cwd - # before spawning the dot subprocess. - for _env_var in ("LD_LIBRARY_PATH", "LTDL_LIBRARY_PATH"): - _env_val = os.environ.get(_env_var, "") - if _env_val: - os.environ[_env_var] = ":".join( - _resolve_execroot_path(p) for p in _env_val.split(":") - ) else: graphviz_dot = "/__hermetic_graphviz_not_configured__/dot" +# Render PlantUML diagrams with the real (hermetic) Graphviz dot resolved above, +# giving reference Graphviz layout. Smetana (PlantUML's bundled Java port) was +# previously used only to avoid an external dot in the sandbox; the hermetic dot +# removes that constraint. Swap back to `-Playout=smetana` if the per-diagram dot +# subprocess regresses build time. +plantuml = f"{plantuml_path} -graphvizdot {graphviz_dot}" + # HTML theme html_theme = "sphinx_rtd_theme" diff --git a/bazel/rules/rules_score/test/MODULE.bazel b/bazel/rules/rules_score/test/MODULE.bazel index 040ca638..21c70974 100644 --- a/bazel/rules/rules_score/test/MODULE.bazel +++ b/bazel/rules/rules_score/test/MODULE.bazel @@ -20,6 +20,13 @@ module( ############################################################################### # Core Dependencies ############################################################################### +bazel_dep(name = "download_utils", version = "1.2.2") +git_override( + module_name = "download_utils", + commit = "3b96912fb6622dda83f25efd1f8ae596fc4a63a6", + remote = "https://gitlab.arm.com/bazel/download_utils.git", +) + bazel_dep(name = "rules_python", version = "1.4.1") bazel_dep(name = "rules_cc", version = "0.2.17") bazel_dep(name = "aspect_rules_py", version = "1.4.0") diff --git a/third_party/docs_runtime/BUILD b/third_party/docs_runtime/BUILD new file mode 100644 index 00000000..14283d87 --- /dev/null +++ b/third_party/docs_runtime/BUILD @@ -0,0 +1,76 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +# Hermetic doc-tool runtime rootfs. The @docs_runtime repo (rules_distroless +# apt.install) is defined in //MODULE.bazel; its manifest + lock live here. + +load("@rules_shell//shell:sh_binary.bzl", "sh_binary") +load("//bazel/rules/exec_in_sysroot:exec_in_sysroot.bzl", "exec_in_sysroot", "prepare_sysroot") + +package(default_visibility = ["//visibility:public"]) + +exports_files([ + "docs_runtime.yaml", + "docs_runtime.lock.json", +]) + +# Host-side launcher that resolves `/usr/bin/dot` inside the fakechroot sysroot. +sh_binary( + name = "dot_in_sysroot", + srcs = ["dot.sh"], +) + +# Rework the distroless rootfs once into a cached `dot`-ready sysroot archive. +# The apt graphviz package ships /usr/bin/dot as a symlink to +# libgvc6-config-update; its postinst (`dot -c`) generates config6a so dot can +# locate and load its plugins at runtime. rules_distroless never runs postinst, +# so we generate it here while the sysroot is still writable. Keeping this in a +# separate rule lets Bazel cache the result (the expensive `dot -c` step is not +# re-run when only the wrapped executable / exclude_paths change). +prepare_sysroot( + name = "dot_sysroot", + # fakechroot + the x86_64 sysroot binaries only run on a linux/x86_64 host. + exec_compatible_with = [ + "@platforms//cpu:x86_64", + "@platforms//os:linux", + ], + # Prune plugins whose X11/pango/GD dependencies are absent from this minimal + # sysroot (they cannot be dlopened, causing dot -c to crash). Use find (not + # a hardcoded arch path) so the rule works on both amd64 and aarch64. + host_setup_commands = [ + "find \"$SYSROOT/usr/lib\" -name 'libgvplugin_pango*' -delete", + "find \"$SYSROOT/usr/lib\" -name 'libgvplugin_gd*' -delete", + "find \"$SYSROOT/usr/lib\" -name 'libgvplugin_xlib*' -delete", + "find \"$SYSROOT/usr/lib\" -name 'libgvplugin_webp*' -delete", + "find \"$SYSROOT/usr/lib\" -name 'libgvplugin_visio*' -delete", + ], + sysroot = "@docs_runtime//:flat", + sysroot_setup_commands = [ + # Generate config6a for the remaining plugins. Runs inside the sysroot + # context so FAKECHROOT_BASE redirects /usr/bin/dot to the sysroot's dot + # and the resulting config6a is written into the sysroot. + "/usr/bin/dot -c", + ], +) + +# Hermetic graphviz dot executable used by doc actions. +exec_in_sysroot( + name = "dot", + # fakechroot + the x86_64 sysroot binaries only run on a linux/x86_64 host. + exec_compatible_with = [ + "@platforms//cpu:x86_64", + "@platforms//os:linux", + ], + executable = ":dot_in_sysroot", + sysroot = ":dot_sysroot", +) diff --git a/third_party/docs_runtime/README.md b/third_party/docs_runtime/README.md new file mode 100644 index 00000000..6db55cdf --- /dev/null +++ b/third_party/docs_runtime/README.md @@ -0,0 +1,65 @@ + + +# Hermetic graphviz for docs (`docs_runtime` + `exec_in_sysroot`) + +The docs build needs Graphviz `dot` at action runtime (Sphinx graphviz extension +and PlantUML `-graphvizdot`). This package makes that use hermetic: + +1. `@docs_runtime//:flat` provides a distroless rootfs tar (from + `docs_runtime.yaml` + `docs_runtime.lock.json`) containing `graphviz` and + `fakechroot`. +2. `//third_party/docs_runtime:dot_sysroot` (a `prepare_sysroot` rule) unpacks + that tar, prunes plugins with missing host dependencies, runs `dot -c` to + generate the plugin manifest, and repackages the result as a single cached + archive. +3. `//third_party/docs_runtime:dot` (an `exec_in_sysroot` rule) extracts the + prepared archive at build time and wraps `dot.sh` so that `/usr/bin/dot` + inside the sysroot is invoked via `LD_PRELOAD=libfakechroot.so`. + +Rules that need graphviz use `//third_party/docs_runtime:dot` as the executable +and receive its path via the `GRAPHVIZ_DOT` environment variable. + +## Caveats + +This setup is reproducible but not *fully* hermetic — two host dependencies +remain: + +1. **Host glibc / ld.so ABI compatibility.** `exec_in_sysroot` runs `dot` via + `LD_PRELOAD=libfakechroot.so` rather than a real chroot, so the kernel still + launches the sysroot's `dot` with the *host* ELF interpreter (`ld.so`), which + then loads the sysroot's glibc. The sysroot is pinned to Ubuntu 24.04 (glibc + 2.39); on a build host whose glibc is older than the sysroot's, `dot` can + fail to start with `GLIBC_2.xx not found`. Build hosts therefore need a glibc + at least as new as the pinned sysroot. +2. **Host shell tools.** The sysroot-rework and extraction actions run under a + POSIX `sh` and use standard coreutils (`find`, `mktemp`, `chmod`, `rm`), + assumed present in the build environment. The generated `dot` launcher itself + requires `bash`, because it sources Bazel's `runfiles.bash` library (there is + no POSIX-`sh` runfiles equivalent in `@bazel_tools`). + +## Updating packages + +Edit `docs_runtime.yaml`, then regenerate and commit the lock: + +```bash +bazel run @docs_runtime//:lock +``` + +## Targets in this package + +- `//third_party/docs_runtime:dot` - hermetic executable used by doc actions. +- `//third_party/docs_runtime/tests:dot_smoke_test` - renders a tiny SVG through + the wrapper to verify runtime wiring. diff --git a/third_party/docs_runtime/docs_runtime.lock.json b/third_party/docs_runtime/docs_runtime.lock.json new file mode 100644 index 00000000..849ff117 --- /dev/null +++ b/third_party/docs_runtime/docs_runtime.lock.json @@ -0,0 +1,1387 @@ +{ + "packages": [ + { + "arch": "amd64", + "dependencies": [ + { + "key": "libxt6_1-1.2.1-1.1_amd64", + "name": "libxt6", + "version": "1:1.2.1-1.1" + }, + { + "key": "libx11-6_2-1.8.7-1_amd64", + "name": "libx11-6", + "version": "2:1.8.7-1" + }, + { + "key": "libx11-data_2-1.8.7-1_amd64", + "name": "libx11-data", + "version": "2:1.8.7-1" + }, + { + "key": "libxcb1_1.15-1_amd64", + "name": "libxcb1", + "version": "1.15-1" + }, + { + "key": "libxdmcp6_1-1.1.3-0ubuntu5_amd64", + "name": "libxdmcp6", + "version": "1:1.1.3-0ubuntu5" + }, + { + "key": "libc6_2.39-0ubuntu2_amd64", + "name": "libc6", + "version": "2.39-0ubuntu2" + }, + { + "key": "libgcc-s1_14-20240221-2.1ubuntu1_amd64", + "name": "libgcc-s1", + "version": "14-20240221-2.1ubuntu1" + }, + { + "key": "gcc-14-base_14-20240221-2.1ubuntu1_amd64", + "name": "gcc-14-base", + "version": "14-20240221-2.1ubuntu1" + }, + { + "key": "libbsd0_0.11.8-1_amd64", + "name": "libbsd0", + "version": "0.11.8-1" + }, + { + "key": "libmd0_1.1.0-2_amd64", + "name": "libmd0", + "version": "1.1.0-2" + }, + { + "key": "libxau6_1-1.0.9-1build5_amd64", + "name": "libxau6", + "version": "1:1.0.9-1build5" + }, + { + "key": "libsm6_2-1.2.3-1build2_amd64", + "name": "libsm6", + "version": "2:1.2.3-1build2" + }, + { + "key": "libuuid1_2.39.3-6ubuntu2_amd64", + "name": "libuuid1", + "version": "2.39.3-6ubuntu2" + }, + { + "key": "libice6_2-1.0.10-1build2_amd64", + "name": "libice6", + "version": "2:1.0.10-1build2" + }, + { + "key": "x11-common_1-7.7-p-23ubuntu2_amd64", + "name": "x11-common", + "version": "1:7.7+23ubuntu2" + }, + { + "key": "lsb-base_11.6_amd64", + "name": "lsb-base", + "version": "11.6" + }, + { + "key": "sysvinit-utils_3.08-6ubuntu2_amd64", + "name": "sysvinit-utils", + "version": "3.08-6ubuntu2" + }, + { + "key": "libxmu6_2-1.1.3-3_amd64", + "name": "libxmu6", + "version": "2:1.1.3-3" + }, + { + "key": "libxext6_2-1.3.4-1build1_amd64", + "name": "libxext6", + "version": "2:1.3.4-1build1" + }, + { + "key": "libxaw7_2-1.0.14-1_amd64", + "name": "libxaw7", + "version": "2:1.0.14-1" + }, + { + "key": "libxpm4_1-3.5.17-1_amd64", + "name": "libxpm4", + "version": "1:3.5.17-1" + }, + { + "key": "libstdc-p--p-6_14-20240221-2.1ubuntu1_amd64", + "name": "libstdc++6", + "version": "14-20240221-2.1ubuntu1" + }, + { + "key": "liblab-gamut1_2.42.2-8build1_amd64", + "name": "liblab-gamut1", + "version": "2.42.2-8build1" + }, + { + "key": "libgvpr2_2.42.2-8build1_amd64", + "name": "libgvpr2", + "version": "2.42.2-8build1" + }, + { + "key": "libcgraph6_2.42.2-8build1_amd64", + "name": "libcgraph6", + "version": "2.42.2-8build1" + }, + { + "key": "libcdt5_2.42.2-8build1_amd64", + "name": "libcdt5", + "version": "2.42.2-8build1" + }, + { + "key": "libgvc6_2.42.2-8build1_amd64", + "name": "libgvc6", + "version": "2.42.2-8build1" + }, + { + "key": "zlib1g_1-1.3.dfsg-3ubuntu1_amd64", + "name": "zlib1g", + "version": "1:1.3.dfsg-3ubuntu1" + }, + { + "key": "libwebp7_1.3.2-0.4_amd64", + "name": "libwebp7", + "version": "1.3.2-0.4" + }, + { + "key": "libsharpyuv0_1.3.2-0.4_amd64", + "name": "libsharpyuv0", + "version": "1.3.2-0.4" + }, + { + "key": "libpathplan4_2.42.2-8build1_amd64", + "name": "libpathplan4", + "version": "2.42.2-8build1" + }, + { + "key": "libpangoft2-1.0-0_1.51.0-p-ds-4_amd64", + "name": "libpangoft2-1.0-0", + "version": "1.51.0+ds-4" + }, + { + "key": "libpango-1.0-0_1.51.0-p-ds-4_amd64", + "name": "libpango-1.0-0", + "version": "1.51.0+ds-4" + }, + { + "key": "libthai0_0.1.29-2_amd64", + "name": "libthai0", + "version": "0.1.29-2" + }, + { + "key": "libdatrie1_0.2.13-3_amd64", + "name": "libdatrie1", + "version": "0.2.13-3" + }, + { + "key": "libthai-data_0.1.29-2_amd64", + "name": "libthai-data", + "version": "0.1.29-2" + }, + { + "key": "libharfbuzz0b_8.3.0-2_amd64", + "name": "libharfbuzz0b", + "version": "8.3.0-2" + }, + { + "key": "libgraphite2-3_1.3.14-2_amd64", + "name": "libgraphite2-3", + "version": "1.3.14-2" + }, + { + "key": "libglib2.0-0_2.79.2-1_ubuntu1_amd64", + "name": "libglib2.0-0", + "version": "2.79.2-1~ubuntu1" + }, + { + "key": "libselinux1_3.5-2build1_amd64", + "name": "libselinux1", + "version": "3.5-2build1" + }, + { + "key": "libpcre2-8-0_10.42-4ubuntu1_amd64", + "name": "libpcre2-8-0", + "version": "10.42-4ubuntu1" + }, + { + "key": "libmount1_2.39.3-6ubuntu2_amd64", + "name": "libmount1", + "version": "2.39.3-6ubuntu2" + }, + { + "key": "libblkid1_2.39.3-6ubuntu2_amd64", + "name": "libblkid1", + "version": "2.39.3-6ubuntu2" + }, + { + "key": "libffi8_3.4.6-1_amd64", + "name": "libffi8", + "version": "3.4.6-1" + }, + { + "key": "libfreetype6_2.13.2-p-dfsg-1_amd64", + "name": "libfreetype6", + "version": "2.13.2+dfsg-1" + }, + { + "key": "libpng16-16_1.6.43-1_amd64", + "name": "libpng16-16", + "version": "1.6.43-1" + }, + { + "key": "libbz2-1.0_1.0.8-5ubuntu1_amd64", + "name": "libbz2-1.0", + "version": "1.0.8-5ubuntu1" + }, + { + "key": "libbrotli1_1.1.0-2_amd64", + "name": "libbrotli1", + "version": "1.1.0-2" + }, + { + "key": "libfribidi0_1.0.13-3_amd64", + "name": "libfribidi0", + "version": "1.0.13-3" + }, + { + "key": "fontconfig_2.15.0-1ubuntu1_amd64", + "name": "fontconfig", + "version": "2.15.0-1ubuntu1" + }, + { + "key": "fontconfig-config_2.15.0-1ubuntu1_amd64", + "name": "fontconfig-config", + "version": "2.15.0-1ubuntu1" + }, + { + "key": "fonts-dejavu-core_2.37-8_amd64", + "name": "fonts-dejavu-core", + "version": "2.37-8" + }, + { + "key": "fonts-dejavu-mono_2.37-8_amd64", + "name": "fonts-dejavu-mono", + "version": "2.37-8" + }, + { + "key": "libfontconfig1_2.15.0-1ubuntu1_amd64", + "name": "libfontconfig1", + "version": "2.15.0-1ubuntu1" + }, + { + "key": "libexpat1_2.6.0-1_amd64", + "name": "libexpat1", + "version": "2.6.0-1" + }, + { + "key": "libpangocairo-1.0-0_1.51.0-p-ds-4_amd64", + "name": "libpangocairo-1.0-0", + "version": "1.51.0+ds-4" + }, + { + "key": "libcairo2_1.18.0-1_amd64", + "name": "libcairo2", + "version": "1.18.0-1" + }, + { + "key": "libxrender1_1-0.9.10-1.1_amd64", + "name": "libxrender1", + "version": "1:0.9.10-1.1" + }, + { + "key": "libxcb-shm0_1.15-1_amd64", + "name": "libxcb-shm0", + "version": "1.15-1" + }, + { + "key": "libxcb-render0_1.15-1_amd64", + "name": "libxcb-render0", + "version": "1.15-1" + }, + { + "key": "libpixman-1-0_0.42.2-1_amd64", + "name": "libpixman-1-0", + "version": "0.42.2-1" + }, + { + "key": "libltdl7_2.4.7-7_amd64", + "name": "libltdl7", + "version": "2.4.7-7" + }, + { + "key": "libgts-0.7-5_0.7.6-p-darcs121130-5_amd64", + "name": "libgts-0.7-5", + "version": "0.7.6+darcs121130-5" + }, + { + "key": "libgd3_2.3.3-9ubuntu1_amd64", + "name": "libgd3", + "version": "2.3.3-9ubuntu1" + }, + { + "key": "libtiff6_4.5.1-p-git230720-3ubuntu1_amd64", + "name": "libtiff6", + "version": "4.5.1+git230720-3ubuntu1" + }, + { + "key": "libzstd1_1.5.5-p-dfsg2-2_amd64", + "name": "libzstd1", + "version": "1.5.5+dfsg2-2" + }, + { + "key": "liblzma5_5.4.5-0.3_amd64", + "name": "liblzma5", + "version": "5.4.5-0.3" + }, + { + "key": "liblerc4_4.0.0-p-ds-4ubuntu1_amd64", + "name": "liblerc4", + "version": "4.0.0+ds-4ubuntu1" + }, + { + "key": "libjpeg8_8c-2ubuntu11_amd64", + "name": "libjpeg8", + "version": "8c-2ubuntu11" + }, + { + "key": "libjpeg-turbo8_2.1.5-2ubuntu1_amd64", + "name": "libjpeg-turbo8", + "version": "2.1.5-2ubuntu1" + }, + { + "key": "libjbig0_2.1-6.1ubuntu1_amd64", + "name": "libjbig0", + "version": "2.1-6.1ubuntu1" + }, + { + "key": "libdeflate0_1.19-1_amd64", + "name": "libdeflate0", + "version": "1.19-1" + }, + { + "key": "libann0_1.1.2-p-doc-9_amd64", + "name": "libann0", + "version": "1.1.2+doc-9" + } + ], + "key": "graphviz_2.42.2-8build1_amd64", + "name": "graphviz", + "sha256": "374d49111008ae01c6084d867d619127fe5288d95e02cd34e7cbb5b41f1ebf68", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/universe/g/graphviz/graphviz_2.42.2-8build1_amd64.deb" + ], + "version": "2.42.2-8build1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libxt6_1-1.2.1-1.1_amd64", + "name": "libxt6", + "sha256": "d986c63413ad679d9d90e54febe368626cc6dc02379fd33f41e62b8c88cee762", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/libx/libxt/libxt6_1.2.1-1.1_amd64.deb" + ], + "version": "1:1.2.1-1.1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libx11-6_2-1.8.7-1_amd64", + "name": "libx11-6", + "sha256": "7fa366fcb328ec857795055c7023b2a7c86ee7edd30d970317d7301f98aca464", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/libx/libx11/libx11-6_1.8.7-1_amd64.deb" + ], + "version": "2:1.8.7-1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libx11-data_2-1.8.7-1_amd64", + "name": "libx11-data", + "sha256": "c5a338c5a95da05a7efc8d1f6fbff9ee23c2b3c9ef3e2c4e602a72f91083fff6", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/libx/libx11/libx11-data_1.8.7-1_all.deb" + ], + "version": "2:1.8.7-1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libxcb1_1.15-1_amd64", + "name": "libxcb1", + "sha256": "b0520c13009a66323747692b8d05b2b253d20748109cb4e7606e83a644fd2669", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/libx/libxcb/libxcb1_1.15-1_amd64.deb" + ], + "version": "1.15-1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libxdmcp6_1-1.1.3-0ubuntu5_amd64", + "name": "libxdmcp6", + "sha256": "6f6c869dcfc072fa77d24b72ba127bdf15d606bdffbeba4314fb39f4f324363d", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/libx/libxdmcp/libxdmcp6_1.1.3-0ubuntu5_amd64.deb" + ], + "version": "1:1.1.3-0ubuntu5" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libc6_2.39-0ubuntu2_amd64", + "name": "libc6", + "sha256": "4bd128b75db38b7e9147c0333908e2c7fbc41631f284360f95118fe1c6c162f3", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/g/glibc/libc6_2.39-0ubuntu2_amd64.deb" + ], + "version": "2.39-0ubuntu2" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libgcc-s1_14-20240221-2.1ubuntu1_amd64", + "name": "libgcc-s1", + "sha256": "ffc195df7e897aaec468e8f62b08660cc711c7449113102491fdd6baa6901f6d", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/g/gcc-14/libgcc-s1_14-20240221-2.1ubuntu1_amd64.deb" + ], + "version": "14-20240221-2.1ubuntu1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "gcc-14-base_14-20240221-2.1ubuntu1_amd64", + "name": "gcc-14-base", + "sha256": "2e1ae2c2ccf2d1b6d09c657af1492a8b7a348e899f9ad25d4925b170571a0887", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/g/gcc-14/gcc-14-base_14-20240221-2.1ubuntu1_amd64.deb" + ], + "version": "14-20240221-2.1ubuntu1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libbsd0_0.11.8-1_amd64", + "name": "libbsd0", + "sha256": "7b6d144fa736de5572ba8558a1c72ff1cd5c3ff08aa462d861858ece75fb1cf3", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/libb/libbsd/libbsd0_0.11.8-1_amd64.deb" + ], + "version": "0.11.8-1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libmd0_1.1.0-2_amd64", + "name": "libmd0", + "sha256": "128be9909c4ce8f2126e5f3d1a04fc11510c519409d64d324d724aae8347cd13", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/libm/libmd/libmd0_1.1.0-2_amd64.deb" + ], + "version": "1.1.0-2" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libxau6_1-1.0.9-1build5_amd64", + "name": "libxau6", + "sha256": "551fa4101394af894f5f401285eeca9756da94bb31ecb08ed36a549ec580cd10", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/libx/libxau/libxau6_1.0.9-1build5_amd64.deb" + ], + "version": "1:1.0.9-1build5" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libsm6_2-1.2.3-1build2_amd64", + "name": "libsm6", + "sha256": "e652e286a79d9e8e15189d79b290bdce20aca83651f3dfed1b9b7dc0bbf0702f", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/libs/libsm/libsm6_1.2.3-1build2_amd64.deb" + ], + "version": "2:1.2.3-1build2" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libuuid1_2.39.3-6ubuntu2_amd64", + "name": "libuuid1", + "sha256": "eec85f07cf7a65483d953b4dbdd857b9b34d33f69b317580d18b8a6c90f628dc", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/u/util-linux/libuuid1_2.39.3-6ubuntu2_amd64.deb" + ], + "version": "2.39.3-6ubuntu2" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libice6_2-1.0.10-1build2_amd64", + "name": "libice6", + "sha256": "eea6d52d12ad610d70b0f70c825ed3c560dafb09f4c272e3c6b0b493d984d13f", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/libi/libice/libice6_1.0.10-1build2_amd64.deb" + ], + "version": "2:1.0.10-1build2" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "x11-common_1-7.7-p-23ubuntu2_amd64", + "name": "x11-common", + "sha256": "c0a20d0b7ba899f00d22b6f18e49a336aace0514f9ef7f3858c1ab689011c559", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/x/xorg/x11-common_7.7+23ubuntu2_all.deb" + ], + "version": "1:7.7+23ubuntu2" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "lsb-base_11.6_amd64", + "name": "lsb-base", + "sha256": "08a563e3c1984acdb295f12b77b4ec18d2ccf52fd03280a5bea29e506a3350ee", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/l/lsb/lsb-base_11.6_all.deb" + ], + "version": "11.6" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "sysvinit-utils_3.08-6ubuntu2_amd64", + "name": "sysvinit-utils", + "sha256": "5e716fef226555264232a284cbd95f17dda7020989fb57d3e0bf5a82f3e5e031", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/s/sysvinit/sysvinit-utils_3.08-6ubuntu2_amd64.deb" + ], + "version": "3.08-6ubuntu2" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libxmu6_2-1.1.3-3_amd64", + "name": "libxmu6", + "sha256": "4f820d1ce1171fc67f62172874b3380a45757222c96f8c60b154118893f2c46c", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/libx/libxmu/libxmu6_1.1.3-3_amd64.deb" + ], + "version": "2:1.1.3-3" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libxext6_2-1.3.4-1build1_amd64", + "name": "libxext6", + "sha256": "875ec849749511dff61c8cc5a775f6f93427b9c3f392fdcec6925f14428e89cd", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/libx/libxext/libxext6_1.3.4-1build1_amd64.deb" + ], + "version": "2:1.3.4-1build1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libxaw7_2-1.0.14-1_amd64", + "name": "libxaw7", + "sha256": "b3cd73ad59824f72193194e984ff433eabc2e4618047ca91e9d66011b5e0b78c", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/libx/libxaw/libxaw7_1.0.14-1_amd64.deb" + ], + "version": "2:1.0.14-1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libxpm4_1-3.5.17-1_amd64", + "name": "libxpm4", + "sha256": "768bf1c25aaa8e457b837a054a596143a3896227e539043fcfdf2f76b34598e8", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/libx/libxpm/libxpm4_3.5.17-1_amd64.deb" + ], + "version": "1:3.5.17-1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libstdc-p--p-6_14-20240221-2.1ubuntu1_amd64", + "name": "libstdc++6", + "sha256": "3311c13f2e26c20369e937051c78f07c495f6112a0d6c32d3285b47021457ec2", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/g/gcc-14/libstdc++6_14-20240221-2.1ubuntu1_amd64.deb" + ], + "version": "14-20240221-2.1ubuntu1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "liblab-gamut1_2.42.2-8build1_amd64", + "name": "liblab-gamut1", + "sha256": "8bc2b25cf9ce39d0d9418d74db1356e5689eda1e17b83683524538515c0e8cd3", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/universe/g/graphviz/liblab-gamut1_2.42.2-8build1_amd64.deb" + ], + "version": "2.42.2-8build1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libgvpr2_2.42.2-8build1_amd64", + "name": "libgvpr2", + "sha256": "bea31611f89412c79c37d64c33eaff5b683b7d7a38976424fdc97ab1f623387e", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/universe/g/graphviz/libgvpr2_2.42.2-8build1_amd64.deb" + ], + "version": "2.42.2-8build1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libcgraph6_2.42.2-8build1_amd64", + "name": "libcgraph6", + "sha256": "d7d7beaaa6e12d58731dbffbf6eedd26a0696a226fb9947f4ded38a6a47cf3c6", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/universe/g/graphviz/libcgraph6_2.42.2-8build1_amd64.deb" + ], + "version": "2.42.2-8build1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libcdt5_2.42.2-8build1_amd64", + "name": "libcdt5", + "sha256": "b5ab71098443817ef224824642d5c7baf0d44238099c981c05e407fffaa2365c", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/universe/g/graphviz/libcdt5_2.42.2-8build1_amd64.deb" + ], + "version": "2.42.2-8build1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libgvc6_2.42.2-8build1_amd64", + "name": "libgvc6", + "sha256": "c9b14f9cd6d4d4fbd7460dcc209700b30b9c250a2ca61e7a501d9ad65f1128d8", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/universe/g/graphviz/libgvc6_2.42.2-8build1_amd64.deb" + ], + "version": "2.42.2-8build1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "zlib1g_1-1.3.dfsg-3ubuntu1_amd64", + "name": "zlib1g", + "sha256": "35cfe44912765862374112e83c178c095448f247785772147c42c0c843b67c97", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/z/zlib/zlib1g_1.3.dfsg-3ubuntu1_amd64.deb" + ], + "version": "1:1.3.dfsg-3ubuntu1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libwebp7_1.3.2-0.4_amd64", + "name": "libwebp7", + "sha256": "606fcfa98061d56b7d9b4ebd3020a42a06e2284792d0f047db6b1e53cc460b54", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/libw/libwebp/libwebp7_1.3.2-0.4_amd64.deb" + ], + "version": "1.3.2-0.4" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libsharpyuv0_1.3.2-0.4_amd64", + "name": "libsharpyuv0", + "sha256": "9aaaa112b6af98d954721df6fcde87f55123730616e4a79ba7d83ce2d0c06beb", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/libw/libwebp/libsharpyuv0_1.3.2-0.4_amd64.deb" + ], + "version": "1.3.2-0.4" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libpathplan4_2.42.2-8build1_amd64", + "name": "libpathplan4", + "sha256": "5070946c9436ae1d9e2f88d74ef2b31d8eabfcdf0f236eb62eaff0ad20c4d6d1", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/universe/g/graphviz/libpathplan4_2.42.2-8build1_amd64.deb" + ], + "version": "2.42.2-8build1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libpangoft2-1.0-0_1.51.0-p-ds-4_amd64", + "name": "libpangoft2-1.0-0", + "sha256": "cc28d62a7d28f1628de46c9245957e450101ad75ebf935d5c71851f4db663aa2", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/p/pango1.0/libpangoft2-1.0-0_1.51.0+ds-4_amd64.deb" + ], + "version": "1.51.0+ds-4" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libpango-1.0-0_1.51.0-p-ds-4_amd64", + "name": "libpango-1.0-0", + "sha256": "a1269198f67c0e65ca048a5bdeab271580ab47b439b4681b890423ced9081854", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/p/pango1.0/libpango-1.0-0_1.51.0+ds-4_amd64.deb" + ], + "version": "1.51.0+ds-4" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libthai0_0.1.29-2_amd64", + "name": "libthai0", + "sha256": "44b44cea90bc3ce86f5939484ec4e2c1f15374f7c5cdeb0a633f49925f1b4a57", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/libt/libthai/libthai0_0.1.29-2_amd64.deb" + ], + "version": "0.1.29-2" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libdatrie1_0.2.13-3_amd64", + "name": "libdatrie1", + "sha256": "3e42fdb17441dd98e2774ecd54f2f700debfaead777c34947288381f113ccb5c", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/libd/libdatrie/libdatrie1_0.2.13-3_amd64.deb" + ], + "version": "0.2.13-3" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libthai-data_0.1.29-2_amd64", + "name": "libthai-data", + "sha256": "2616af1ff9f29f9bc76b65060808621fb028721adee7e0ccf15f7b06f55fe4c5", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/libt/libthai/libthai-data_0.1.29-2_all.deb" + ], + "version": "0.1.29-2" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libharfbuzz0b_8.3.0-2_amd64", + "name": "libharfbuzz0b", + "sha256": "b70b342b10796bd5dccd21ebec8aaef0d01520939d0991dd5a354db6f26aad74", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/h/harfbuzz/libharfbuzz0b_8.3.0-2_amd64.deb" + ], + "version": "8.3.0-2" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libgraphite2-3_1.3.14-2_amd64", + "name": "libgraphite2-3", + "sha256": "7ba5cdb08364ac065c945a5338adc199fe80086c2fc32116262d9647287fd65f", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/g/graphite2/libgraphite2-3_1.3.14-2_amd64.deb" + ], + "version": "1.3.14-2" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libglib2.0-0_2.79.2-1_ubuntu1_amd64", + "name": "libglib2.0-0", + "sha256": "1aa724b05316bea1cbeae71681aae81f8c3b3d29002ef3ab9eca9b156e0a4b41", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/g/glib2.0/libglib2.0-0_2.79.2-1~ubuntu1_amd64.deb" + ], + "version": "2.79.2-1~ubuntu1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libselinux1_3.5-2build1_amd64", + "name": "libselinux1", + "sha256": "139f29430e3d265fc8d9b9da7dd3f704ee3f1838c37a5d512cf265ec0b4eba28", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/libs/libselinux/libselinux1_3.5-2build1_amd64.deb" + ], + "version": "3.5-2build1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libpcre2-8-0_10.42-4ubuntu1_amd64", + "name": "libpcre2-8-0", + "sha256": "3fbf30adf862c4e510a9260c7666a1a5326bc5fed8021090bc75a4ecbaa52fa4", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/p/pcre2/libpcre2-8-0_10.42-4ubuntu1_amd64.deb" + ], + "version": "10.42-4ubuntu1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libmount1_2.39.3-6ubuntu2_amd64", + "name": "libmount1", + "sha256": "00ea5406ad8d934883a52d531c95e9faeb88e2e77fd0da59129eac0e126fec1b", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/u/util-linux/libmount1_2.39.3-6ubuntu2_amd64.deb" + ], + "version": "2.39.3-6ubuntu2" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libblkid1_2.39.3-6ubuntu2_amd64", + "name": "libblkid1", + "sha256": "9a16eb730c6e3bffd852c062a972942fdc653b994b8d3c9cadd6aec3c393d284", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/u/util-linux/libblkid1_2.39.3-6ubuntu2_amd64.deb" + ], + "version": "2.39.3-6ubuntu2" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libffi8_3.4.6-1_amd64", + "name": "libffi8", + "sha256": "bd30f638a82381979c4c07b3acabb7fccaeed7f9b094e27c9a676d2e94572b14", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/libf/libffi/libffi8_3.4.6-1_amd64.deb" + ], + "version": "3.4.6-1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libfreetype6_2.13.2-p-dfsg-1_amd64", + "name": "libfreetype6", + "sha256": "1b7e8083626fd0b2426c6f0099239da2cdc4f19abb2360ba981a2b67a0742d81", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/f/freetype/libfreetype6_2.13.2+dfsg-1_amd64.deb" + ], + "version": "2.13.2+dfsg-1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libpng16-16_1.6.43-1_amd64", + "name": "libpng16-16", + "sha256": "472bbb169d929d85a420723a7921b90e72c5e295cfdd3954135eb0f1b09a94fa", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/libp/libpng1.6/libpng16-16_1.6.43-1_amd64.deb" + ], + "version": "1.6.43-1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libbz2-1.0_1.0.8-5ubuntu1_amd64", + "name": "libbz2-1.0", + "sha256": "8925b88fac7e8162a5c9dfcb078bb33932cb8aee51bb33db209ca97840f65369", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/b/bzip2/libbz2-1.0_1.0.8-5ubuntu1_amd64.deb" + ], + "version": "1.0.8-5ubuntu1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libbrotli1_1.1.0-2_amd64", + "name": "libbrotli1", + "sha256": "a46004c3521a4ee502a6bc2d48e1e71d0f1bb0e0eac57538da323a271b0feb23", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/b/brotli/libbrotli1_1.1.0-2_amd64.deb" + ], + "version": "1.1.0-2" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libfribidi0_1.0.13-3_amd64", + "name": "libfribidi0", + "sha256": "65ae08f9f61a5b03fe54a0aa2f6bc2743bd8400d0868eaeca33f393702e6175b", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/f/fribidi/libfribidi0_1.0.13-3_amd64.deb" + ], + "version": "1.0.13-3" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "fontconfig_2.15.0-1ubuntu1_amd64", + "name": "fontconfig", + "sha256": "9adc8083ecb8219778e3b55c3508b173f33acef8fd11f06961f9f6e78335409b", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/f/fontconfig/fontconfig_2.15.0-1ubuntu1_amd64.deb" + ], + "version": "2.15.0-1ubuntu1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "fontconfig-config_2.15.0-1ubuntu1_amd64", + "name": "fontconfig-config", + "sha256": "616ecdbdc07ac946a51cdd79cb9ab2cb62d7a3dd2c5b31f678b88e3e3fba3112", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/f/fontconfig/fontconfig-config_2.15.0-1ubuntu1_amd64.deb" + ], + "version": "2.15.0-1ubuntu1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "fonts-dejavu-core_2.37-8_amd64", + "name": "fonts-dejavu-core", + "sha256": "40049660c194f3b8a2541fc7369efebb10e9f94bdac836a2f38fafedd10fa73a", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/f/fonts-dejavu/fonts-dejavu-core_2.37-8_all.deb" + ], + "version": "2.37-8" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "fonts-dejavu-mono_2.37-8_amd64", + "name": "fonts-dejavu-mono", + "sha256": "8a599d6553307db7ecb795d2f0e5a301e03234afc75c7358b0ba43466454c89a", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/f/fonts-dejavu/fonts-dejavu-mono_2.37-8_all.deb" + ], + "version": "2.37-8" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libfontconfig1_2.15.0-1ubuntu1_amd64", + "name": "libfontconfig1", + "sha256": "db339d0cc32e96db73f54d5b0fc5b1caf19fcedbb9b9383cc0f4c0596ef8175a", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/f/fontconfig/libfontconfig1_2.15.0-1ubuntu1_amd64.deb" + ], + "version": "2.15.0-1ubuntu1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libexpat1_2.6.0-1_amd64", + "name": "libexpat1", + "sha256": "3951af935e90fd2148c05c727c0b014f59af70c8ab534b8270d73fa4e6f7136c", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/e/expat/libexpat1_2.6.0-1_amd64.deb" + ], + "version": "2.6.0-1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libpangocairo-1.0-0_1.51.0-p-ds-4_amd64", + "name": "libpangocairo-1.0-0", + "sha256": "abaab59fef4291a42661342bd3f70f0ea2077cbd712babb7d3fb14ba95583767", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/p/pango1.0/libpangocairo-1.0-0_1.51.0+ds-4_amd64.deb" + ], + "version": "1.51.0+ds-4" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libcairo2_1.18.0-1_amd64", + "name": "libcairo2", + "sha256": "7455c38efbe1ea351c0527b8d5baa2a47cbf70a602ecbfec930e903367a04e3c", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/c/cairo/libcairo2_1.18.0-1_amd64.deb" + ], + "version": "1.18.0-1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libxrender1_1-0.9.10-1.1_amd64", + "name": "libxrender1", + "sha256": "28029f0f1ce6d1ae411a2e4b923d089f7db783d783c8d9d71dec70c0e4e97055", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/libx/libxrender/libxrender1_0.9.10-1.1_amd64.deb" + ], + "version": "1:0.9.10-1.1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libxcb-shm0_1.15-1_amd64", + "name": "libxcb-shm0", + "sha256": "fc5d86790eb2d9a354724aa1495d590f378ec6529833547c22e1bed492805775", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/libx/libxcb/libxcb-shm0_1.15-1_amd64.deb" + ], + "version": "1.15-1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libxcb-render0_1.15-1_amd64", + "name": "libxcb-render0", + "sha256": "02dc666363085db34369a80c7e184eab3ab44f185f023137fa36643fb4df13c1", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/libx/libxcb/libxcb-render0_1.15-1_amd64.deb" + ], + "version": "1.15-1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libpixman-1-0_0.42.2-1_amd64", + "name": "libpixman-1-0", + "sha256": "a1de5be8a592907bdddea465c895d6a10a9b09087abbee06f3ab015d011e8d65", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/p/pixman/libpixman-1-0_0.42.2-1_amd64.deb" + ], + "version": "0.42.2-1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libltdl7_2.4.7-7_amd64", + "name": "libltdl7", + "sha256": "4bfabbbaafbdaf912bf60bcb29aff8ee16016e792443523a1cba7d35b09b7bb1", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/libt/libtool/libltdl7_2.4.7-7_amd64.deb" + ], + "version": "2.4.7-7" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libgts-0.7-5_0.7.6-p-darcs121130-5_amd64", + "name": "libgts-0.7-5", + "sha256": "9f0d6b143b4f602dd5b78dbef49491c7e0e50154fdcfa5da6e1261e763d9cd32", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/universe/g/gts/libgts-0.7-5_0.7.6+darcs121130-5_amd64.deb" + ], + "version": "0.7.6+darcs121130-5" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libgd3_2.3.3-9ubuntu1_amd64", + "name": "libgd3", + "sha256": "97a8bedd02f36c1a2116378e25a034a08a174232ecd4a22ab172fa671a257e03", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/libg/libgd2/libgd3_2.3.3-9ubuntu1_amd64.deb" + ], + "version": "2.3.3-9ubuntu1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libtiff6_4.5.1-p-git230720-3ubuntu1_amd64", + "name": "libtiff6", + "sha256": "8e64d80b612d8b0a0b8010fb923a2512f9a364cfea52879df54cad967a742daa", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/t/tiff/libtiff6_4.5.1+git230720-3ubuntu1_amd64.deb" + ], + "version": "4.5.1+git230720-3ubuntu1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libzstd1_1.5.5-p-dfsg2-2_amd64", + "name": "libzstd1", + "sha256": "7926bb8267652dd7df2c78c5e7541df6e62dbc10ed2efd4c2b869c75538b2ff1", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/libz/libzstd/libzstd1_1.5.5+dfsg2-2_amd64.deb" + ], + "version": "1.5.5+dfsg2-2" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "liblzma5_5.4.5-0.3_amd64", + "name": "liblzma5", + "sha256": "02bb3148ccfa7408b3f12833aa483c2dd4e3a6ee647fe8bbc3bc60ef50761ead", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/x/xz-utils/liblzma5_5.4.5-0.3_amd64.deb" + ], + "version": "5.4.5-0.3" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "liblerc4_4.0.0-p-ds-4ubuntu1_amd64", + "name": "liblerc4", + "sha256": "496372cd2853f3d8e2d92980fa3b602f7a1acd7528bd3d7e9a41046d8286cc8b", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/l/lerc/liblerc4_4.0.0+ds-4ubuntu1_amd64.deb" + ], + "version": "4.0.0+ds-4ubuntu1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libjpeg8_8c-2ubuntu11_amd64", + "name": "libjpeg8", + "sha256": "4d6ed682972bddf62af158a06d68a0dda497e421423b7ca92fb450b3d9ceaac9", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/libj/libjpeg8-empty/libjpeg8_8c-2ubuntu11_amd64.deb" + ], + "version": "8c-2ubuntu11" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libjpeg-turbo8_2.1.5-2ubuntu1_amd64", + "name": "libjpeg-turbo8", + "sha256": "448bb8a52b1f223833f02cb08f7f03c68579cd989027ccf783ef1c37f4cef946", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/libj/libjpeg-turbo/libjpeg-turbo8_2.1.5-2ubuntu1_amd64.deb" + ], + "version": "2.1.5-2ubuntu1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libjbig0_2.1-6.1ubuntu1_amd64", + "name": "libjbig0", + "sha256": "b7630bdc99393411011cb71c7504f6366cf269deba9c58862b2580e3e4f8be1f", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/j/jbigkit/libjbig0_2.1-6.1ubuntu1_amd64.deb" + ], + "version": "2.1-6.1ubuntu1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libdeflate0_1.19-1_amd64", + "name": "libdeflate0", + "sha256": "635aea936792f65506256967ce957ed66a50cdb363264d1c5cea174c4705cedb", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/libd/libdeflate/libdeflate0_1.19-1_amd64.deb" + ], + "version": "1.19-1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libann0_1.1.2-p-doc-9_amd64", + "name": "libann0", + "sha256": "06be66eac231276f29fdb5d6f8e71fbc4ad714efe96ed8fbfaeecb73696e94ac", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/universe/a/ann/libann0_1.1.2+doc-9_amd64.deb" + ], + "version": "1.1.2+doc-9" + }, + { + "arch": "amd64", + "dependencies": [ + { + "key": "libfakechroot_2.20.1-p-ds-15_amd64", + "name": "libfakechroot", + "version": "2.20.1+ds-15" + }, + { + "key": "libc6_2.39-0ubuntu2_amd64", + "name": "libc6", + "version": "2.39-0ubuntu2" + }, + { + "key": "libgcc-s1_14-20240221-2.1ubuntu1_amd64", + "name": "libgcc-s1", + "version": "14-20240221-2.1ubuntu1" + }, + { + "key": "gcc-14-base_14-20240221-2.1ubuntu1_amd64", + "name": "gcc-14-base", + "version": "14-20240221-2.1ubuntu1" + }, + { + "key": "binutils_2.42-3ubuntu1_amd64", + "name": "binutils", + "version": "2.42-3ubuntu1" + }, + { + "key": "binutils-x86-64-linux-gnu_2.42-3ubuntu1_amd64", + "name": "binutils-x86-64-linux-gnu", + "version": "2.42-3ubuntu1" + }, + { + "key": "zlib1g_1-1.3.dfsg-3ubuntu1_amd64", + "name": "zlib1g", + "version": "1:1.3.dfsg-3ubuntu1" + }, + { + "key": "libzstd1_1.5.5-p-dfsg2-2_amd64", + "name": "libzstd1", + "version": "1.5.5+dfsg2-2" + }, + { + "key": "libstdc-p--p-6_14-20240221-2.1ubuntu1_amd64", + "name": "libstdc++6", + "version": "14-20240221-2.1ubuntu1" + }, + { + "key": "libsframe1_2.42-3ubuntu1_amd64", + "name": "libsframe1", + "version": "2.42-3ubuntu1" + }, + { + "key": "libjansson4_2.14-2_amd64", + "name": "libjansson4", + "version": "2.14-2" + }, + { + "key": "libgprofng0_2.42-3ubuntu1_amd64", + "name": "libgprofng0", + "version": "2.42-3ubuntu1" + }, + { + "key": "libbinutils_2.42-3ubuntu1_amd64", + "name": "libbinutils", + "version": "2.42-3ubuntu1" + }, + { + "key": "binutils-common_2.42-3ubuntu1_amd64", + "name": "binutils-common", + "version": "2.42-3ubuntu1" + }, + { + "key": "libctf0_2.42-3ubuntu1_amd64", + "name": "libctf0", + "version": "2.42-3ubuntu1" + }, + { + "key": "libctf-nobfd0_2.42-3ubuntu1_amd64", + "name": "libctf-nobfd0", + "version": "2.42-3ubuntu1" + } + ], + "key": "fakechroot_2.20.1-p-ds-15_amd64", + "name": "fakechroot", + "sha256": "f311e5c449314e8e66427277e901a415d3d77d4e3e8c91817a4a831e47467675", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/universe/f/fakechroot/fakechroot_2.20.1+ds-15_all.deb" + ], + "version": "2.20.1+ds-15" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libfakechroot_2.20.1-p-ds-15_amd64", + "name": "libfakechroot", + "sha256": "6a9dd5907de0b94ab9e79cedd39b91a5cc783708675869dd588b0f5bebb97d70", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/universe/f/fakechroot/libfakechroot_2.20.1+ds-15_amd64.deb" + ], + "version": "2.20.1+ds-15" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "binutils_2.42-3ubuntu1_amd64", + "name": "binutils", + "sha256": "f047bb9c3db90cb28d334e2c5bcc0914e1688e6e14d37afeb52100917e87c107", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/b/binutils/binutils_2.42-3ubuntu1_amd64.deb" + ], + "version": "2.42-3ubuntu1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "binutils-x86-64-linux-gnu_2.42-3ubuntu1_amd64", + "name": "binutils-x86-64-linux-gnu", + "sha256": "f68656486334d2d6e16c9ca296d99d175aa083979d7de03e1e62046f7774b117", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/b/binutils/binutils-x86-64-linux-gnu_2.42-3ubuntu1_amd64.deb" + ], + "version": "2.42-3ubuntu1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libsframe1_2.42-3ubuntu1_amd64", + "name": "libsframe1", + "sha256": "58d05ca5a4fd1790835c5c136d2a4a499d7d2513150b708cf8e12603e8e17add", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/b/binutils/libsframe1_2.42-3ubuntu1_amd64.deb" + ], + "version": "2.42-3ubuntu1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libjansson4_2.14-2_amd64", + "name": "libjansson4", + "sha256": "55703f7b04f0235f6b86d67a3c4263a41e6cf30c2b5d5e1e00312368ebd49971", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/j/jansson/libjansson4_2.14-2_amd64.deb" + ], + "version": "2.14-2" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libgprofng0_2.42-3ubuntu1_amd64", + "name": "libgprofng0", + "sha256": "0db398e08c159645833edad7b536f0afd67064cc2d1a9d9d3e9055d059760044", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/b/binutils/libgprofng0_2.42-3ubuntu1_amd64.deb" + ], + "version": "2.42-3ubuntu1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libbinutils_2.42-3ubuntu1_amd64", + "name": "libbinutils", + "sha256": "fa69181cf5034e7b136e666100963eba6fce7a835d4223b12b6c03f749420a0f", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/b/binutils/libbinutils_2.42-3ubuntu1_amd64.deb" + ], + "version": "2.42-3ubuntu1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "binutils-common_2.42-3ubuntu1_amd64", + "name": "binutils-common", + "sha256": "26129a7c67435a2f6107f05112596b59be66dd90a8349f13e8a7602587a7374c", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/b/binutils/binutils-common_2.42-3ubuntu1_amd64.deb" + ], + "version": "2.42-3ubuntu1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libctf0_2.42-3ubuntu1_amd64", + "name": "libctf0", + "sha256": "16b279d18d5d8223c248d20ded7e8983e4dc577378d4af903e7d2b450251811b", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/b/binutils/libctf0_2.42-3ubuntu1_amd64.deb" + ], + "version": "2.42-3ubuntu1" + }, + { + "arch": "amd64", + "dependencies": [], + "key": "libctf-nobfd0_2.42-3ubuntu1_amd64", + "name": "libctf-nobfd0", + "sha256": "455bd3996749dc3b3b74c9e0fea6865d1806f2ae650d3b30c505ca3643ed3c18", + "urls": [ + "https://snapshot.ubuntu.com/ubuntu/20240301T030400Z/pool/main/b/binutils/libctf-nobfd0_2.42-3ubuntu1_amd64.deb" + ], + "version": "2.42-3ubuntu1" + } + ], + "version": 1 +} diff --git a/third_party/docs_runtime/docs_runtime.yaml b/third_party/docs_runtime/docs_runtime.yaml new file mode 100644 index 00000000..f7ccce1a --- /dev/null +++ b/third_party/docs_runtime/docs_runtime.yaml @@ -0,0 +1,32 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +# Sysroot for hermetic docs tooling. It provides graphviz `dot` plus fakechroot, +# so //third_party/docs_runtime:dot can execute /usr/bin/dot fully from sysroot. +# +# Run `bazel run @docs_runtime//:lock` whenever you change this file, then commit +# the regenerated docs_runtime.lock.json. +# +# Pinned to the Ubuntu 24.04 (noble) snapshot so package closure is reproducible. +version: 1 +sources: + - channel: noble main universe + url: https://snapshot.ubuntu.com/ubuntu/20260401T000000Z + - channel: noble-security main universe + url: https://snapshot.ubuntu.com/ubuntu/20260401T000000Z + - channel: noble-updates main universe + url: https://snapshot.ubuntu.com/ubuntu/20260401T000000Z +archs: + - "amd64" +packages: + - "graphviz" # /usr/bin/dot and plugins used by Sphinx + PlantUML + - "fakechroot" # /usr/bin/fakechroot + libfakechroot.so for exec_in_sysroot diff --git a/third_party/docs_runtime/dot.sh b/third_party/docs_runtime/dot.sh new file mode 100755 index 00000000..cb56523f --- /dev/null +++ b/third_party/docs_runtime/dot.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +set -eu + +exec /usr/bin/dot "$@" diff --git a/third_party/docs_runtime/tests/BUILD b/third_party/docs_runtime/tests/BUILD new file mode 100644 index 00000000..9138a91d --- /dev/null +++ b/third_party/docs_runtime/tests/BUILD @@ -0,0 +1,29 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@rules_shell//shell:sh_test.bzl", "sh_test") + +# Smoke test: verify that the hermetic exec_in_sysroot graphviz wrapper can +# render a dot graph to SVG without a system graphviz installation. +sh_test( + name = "dot_smoke_test", + srcs = ["dot_smoke_test.sh"], + data = [ + "//third_party/docs_runtime:dot", + ], + # The sysroot is Ubuntu 24.04 amd64; the dot binary is x86_64 ELF. + target_compatible_with = [ + "@platforms//cpu:x86_64", + "@platforms//os:linux", + ], +) diff --git a/third_party/docs_runtime/tests/dot_smoke_test.sh b/third_party/docs_runtime/tests/dot_smoke_test.sh new file mode 100755 index 00000000..bdd60d7c --- /dev/null +++ b/third_party/docs_runtime/tests/dot_smoke_test.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +# +# Smoke test for the hermetic graphviz setup: +# //third_party/docs_runtime:dot — exec_in_sysroot wrapper executing /usr/bin/dot +# inside the docs_runtime sysroot. +# If this wrapper/sysroot pairing breaks, doc builds lose graphviz rendering. + +set -euo pipefail + +# --- begin runfiles.bash initialization --- +if [[ ! -d "${RUNFILES_DIR:-/dev/null}" && ! -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then + if [[ -f "$0.runfiles_manifest" ]]; then + export RUNFILES_MANIFEST_FILE="$0.runfiles_manifest" + elif [[ -f "$0.runfiles/MANIFEST" ]]; then + export RUNFILES_MANIFEST_FILE="$0.runfiles/MANIFEST" + elif [[ -f "$0.runfiles/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then + export RUNFILES_DIR="$0.runfiles" + fi +fi +if [[ -f "${RUNFILES_DIR:-/dev/null}/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then + source "${RUNFILES_DIR}/bazel_tools/tools/bash/runfiles/runfiles.bash" +elif [[ -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then + source "$(grep -m1 '^bazel_tools/tools/bash/runfiles/runfiles.bash ' "$RUNFILES_MANIFEST_FILE" | cut -d ' ' -f 2-)" +else + echo >&2 "ERROR: cannot find @bazel_tools//tools/bash/runfiles:runfiles.bash" + exit 1 +fi +# --- end runfiles.bash initialization --- + +dot_wrapper="$(rlocation "${TEST_WORKSPACE}/third_party/docs_runtime/dot")" +if [[ -z "${dot_wrapper}" || ! -x "${dot_wrapper}" ]]; then + echo "ERROR: could not resolve //third_party/docs_runtime:dot from runfiles" >&2 + exit 1 +fi + +# --------------------------------------------------------------------------- +# 1. Sanity: version string. +# --------------------------------------------------------------------------- +echo "=== dot -V ===" +"${dot_wrapper}" -V 2>&1 + +# --------------------------------------------------------------------------- +# 2. Render a minimal digraph to SVG and verify the output contains valid SVG. +# --------------------------------------------------------------------------- +echo "=== rendering digraph to SVG ===" +svg="$(printf 'digraph G { A -> B }' | "${dot_wrapper}" -Tsvg 2>&1)" + +if [[ "${svg}" != *"&2 + echo "--- output ---" >&2 + echo "${svg}" >&2 + exit 1 +fi + +echo "ok: hermetic dot works" diff --git a/third_party/graphviz/graphviz.BUILD b/third_party/graphviz/graphviz.BUILD deleted file mode 100644 index bbc54ccb..00000000 --- a/third_party/graphviz/graphviz.BUILD +++ /dev/null @@ -1,49 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2025 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* - -# This BUILD file is injected into the @graphviz_deb external repository by the -# graphviz_deb rule. It exposes dot_builtins and required bundled shared -# libraries for Sphinx graphviz rendering in a hermetic way. - -package(default_visibility = ["//visibility:public"]) - -# The actual graphviz rendering binary (not the dot wrapper/launcher). -# Uses RUNPATH $ORIGIN/../lib to find bundled shared libraries. -filegroup( - name = "dot_binary", - srcs = ["usr/bin/dot_builtins"], -) - -# Bundled graphviz shared libraries (libgvc, libcgraph, libcdt, libpathplan, libxdot). -# These are found automatically by dot_builtins via RUNPATH $ORIGIN/../lib. -filegroup( - name = "core_libs", - srcs = glob(["usr/lib/*.so*"]), -) - -# Graphviz plugin shared libraries (libgvplugin_core, libgvplugin_dot_layout, etc.). -# Loaded at runtime via libltdl; requires LTDL_LIBRARY_PATH=usr/lib/graphviz. -filegroup( - name = "plugin_libs", - srcs = glob(["usr/lib/graphviz/*.so*"]), -) - -# All graphviz files needed to run dot_builtins. -filegroup( - name = "all", - srcs = [ - ":core_libs", - ":dot_binary", - ":plugin_libs", - ], -) diff --git a/third_party/plantuml/BUILD b/third_party/plantuml/BUILD new file mode 100644 index 00000000..f0f61ebd --- /dev/null +++ b/third_party/plantuml/BUILD @@ -0,0 +1,23 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@rules_java//java:defs.bzl", "java_binary") + +java_binary( + name = "plantuml", + main_class = "net.sourceforge.plantuml.Run", + visibility = ["//visibility:public"], + runtime_deps = [ + "@blueprint_maven_dependencies//:net_sourceforge_plantuml_plantuml", + ], +) diff --git a/tools/lobster_rst_report/lobster_rst_report.bzl b/tools/lobster_rst_report/lobster_rst_report.bzl index b9319934..b5d45e7e 100644 --- a/tools/lobster_rst_report/lobster_rst_report.bzl +++ b/tools/lobster_rst_report/lobster_rst_report.bzl @@ -54,11 +54,22 @@ def _lobster_report_rst_impl(ctx): rst_args.add_all(["--out-dir", rst_dir.path]) rst_args.add_all(["--source-root", source_root]) + # Wire in the hermetic graphviz dot wrapper (exec_in_sysroot + docs_runtime) so + # the generated RST emits the `.. graphviz::` policy-diagram directive (the + # tool checks dot availability via GRAPHVIZ_DOT) instead of the host-dependent + # fallback note. + rst_inputs = [lobster_report_json] + rst_env = { + "GRAPHVIZ_DOT": ctx.executable._graphviz.path, + } + ctx.actions.run( executable = ctx.executable._lobster_rst_report, - inputs = [lobster_report_json], + inputs = rst_inputs, outputs = [rst_dir], arguments = [rst_args], + env = rst_env, + tools = [ctx.attr._graphviz.files_to_run], progress_message = "lobster-rst-report (pages) %s" % ctx.label.name, ) @@ -87,5 +98,10 @@ lobster_report_rst = rule( executable = True, cfg = "exec", ), + "_graphviz": attr.label( + default = Label("//third_party/docs_runtime:dot"), + executable = True, + cfg = "exec", + ), }, ) diff --git a/tools/lobster_rst_report/rst_report.py b/tools/lobster_rst_report/rst_report.py index 309c3ae3..578f9277 100644 --- a/tools/lobster_rst_report/rst_report.py +++ b/tools/lobster_rst_report/rst_report.py @@ -362,7 +362,10 @@ def _run_impl(self, options: argparse.Namespace) -> int: err.dump() return 1 - if not is_dot_available(): + # Honor GRAPHVIZ_DOT (set by the Bazel rule to the hermetic dot) the same + # way PolicyDiagramBuilder does, so the warning reflects what is actually + # emitted rather than only checking for a system `dot` on PATH. + if not is_dot_available(os.environ.get("GRAPHVIZ_DOT") or None): print( "warning: dot utility not found, report will not include " "the tracing policy visualisation" diff --git a/tools/sphinx/BUILD b/tools/sphinx/BUILD index d1a64d03..86f4b637 100644 --- a/tools/sphinx/BUILD +++ b/tools/sphinx/BUILD @@ -12,18 +12,8 @@ # ******************************************************************************* load("@pip_rules_score//:requirements.bzl", "requirement") -load("@rules_java//java:defs.bzl", "java_binary") load("@rules_python//sphinxdocs:sphinx.bzl", "sphinx_build_binary") -java_binary( - name = "plantuml", - main_class = "net.sourceforge.plantuml.Run", - visibility = ["//visibility:public"], - runtime_deps = [ - "@blueprint_maven_dependencies//:net_sourceforge_plantuml_plantuml", - ], -) - sphinx_build_binary( name = "sphinx-build", visibility = ["//visibility:public"], @@ -39,6 +29,5 @@ sphinx_build_binary( requirement("sphinxcontrib-umlet"), requirement("svglib"), requirement("sphinxcontrib-plantuml"), - "@rules_python//python/runfiles", ], ) From 7157a33df994843d0a9e05a4af3d37d95c34b463 Mon Sep 17 00:00:00 2001 From: Jochen Hoenle Date: Mon, 29 Jun 2026 13:24:17 +0200 Subject: [PATCH 2/3] [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/exec_in_sysroot/exec_in_sysroot.bzl | 74 ++++++--- .../rules/exec_in_sysroot/exec_in_sysroot.sh | 39 ++++- .../rules_score/docs/tooling_architecture.rst | 142 ++++++++++++++++++ .../rules_score/private/sphinx_module.bzl | 20 +++ .../rules_score/templates/conf.template.py | 137 +++++++++-------- third_party/docs_runtime/BUILD | 5 + third_party/docs_runtime/dot.sh | 16 +- 7 files changed, 341 insertions(+), 92 deletions(-) diff --git a/bazel/rules/exec_in_sysroot/exec_in_sysroot.bzl b/bazel/rules/exec_in_sysroot/exec_in_sysroot.bzl index 017b7f6e..450f75ba 100644 --- a/bazel/rules/exec_in_sysroot/exec_in_sysroot.bzl +++ b/bazel/rules/exec_in_sysroot/exec_in_sysroot.bzl @@ -40,35 +40,61 @@ def _setup_block(sysroot_dir, host_setup_commands, sysroot_setup_commands): host_setup_commands run in the outer shell with $SYSROOT set to sysroot_dir. - sysroot_setup_commands run inside a temporary /bin/sh script with - LD_PRELOAD=libfakechroot.so + FAKECHROOT_BASE=sysroot_dir, so absolute-path - accesses are transparently redirected into the sysroot. + sysroot_setup_commands are invoked directly via the sysroot's own ELF + interpreter (ld-linux.so) with --library-path pointing at the sysroot's + /usr/lib tree. This gives each command a fully consistent single-libc + environment (all of the binary's dependencies — including libc.so.6 itself — + come from the sysroot). LD_PRELOAD=libfakechroot.so + FAKECHROOT_BASE are + still set so that glibc-level filesystem calls inside the command (e.g. + writing the graphviz config6 file to /usr/lib/…) are transparently + redirected into the sysroot. + + Each entry in sysroot_setup_commands must be a space-separated ELF binary + invocation starting with an absolute sysroot path, e.g. "/usr/bin/dot -c". + Shell metacharacters (pipes, redirects, etc.) are not supported. """ block = "" if host_setup_commands: block += "SYSROOT=\"" + sysroot_dir + "\"\n" block += "\n".join(host_setup_commands) + "\n" if sysroot_setup_commands: - script_lines = ["#!/bin/sh", "set -eu"] + sysroot_setup_commands - printf_calls = "\n".join([ - "printf '%s\\n' '" + line.replace("'", "'\\''") + "' >> \"$_FC_SCRIPT\"" - for line in script_lines - ]) block += ( "_FC_LIB=\"$(find \"" + sysroot_dir + "/usr/lib\" -path '*/fakechroot/libfakechroot.so' -type f 2>/dev/null | head -1 || true)\"\n" + "if [ -z \"$_FC_LIB\" ]; then\n" + " echo \"ERROR: sysroot_setup_commands require fakechroot, but libfakechroot.so was not found under " + sysroot_dir + "/usr/lib\" >&2\n" + " exit 1\n" + "fi\n" + - "_FC_SCRIPT=\"$(mktemp \"${TMPDIR:-/tmp}/_prepare_sysroot_setup_XXXXXX.sh\")\"\n" + - printf_calls + "\n" + - "chmod +x \"$_FC_SCRIPT\"\n" + - "LD_PRELOAD=\"$_FC_LIB\" " + - "LD_LIBRARY_PATH=\"$(dirname \"$_FC_LIB\")${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}\" " + - "FAKECHROOT_BASE=\"" + sysroot_dir + "\" " + - "FAKECHROOT_EXCLUDE_PATH=\"$_FC_SCRIPT\" \"$_FC_SCRIPT\" 2>&1\n" + - "rm -f \"$_FC_SCRIPT\"\n" + # Use the sysroot's own ELF interpreter with an explicit --library-path + # so each command loads all dependencies from the sysroot's /usr/lib tree. + # Use well-known Debian multiarch paths instead of a fragile glob search. + # Running via the HOST ld-linux.so + LD_LIBRARY_PATH would load the + # sysroot's libc.so.6 alongside the host's already-loaded libc (two libc + # instances in one process → segfault). The sysroot's own interpreter + # gives a single coherent libc. + "if [ -f \"" + sysroot_dir + "/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2\" ]; then\n" + + " _SYSROOT_INTERP=\"" + sysroot_dir + "/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2\"\n" + + " _SYSROOT_LIBPATH=\"" + sysroot_dir + "/usr/lib/x86_64-linux-gnu:" + sysroot_dir + "/usr/lib\"\n" + + "elif [ -f \"" + sysroot_dir + "/usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1\" ]; then\n" + + " _SYSROOT_INTERP=\"" + sysroot_dir + "/usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1\"\n" + + " _SYSROOT_LIBPATH=\"" + sysroot_dir + "/usr/lib/aarch64-linux-gnu:" + sysroot_dir + "/usr/lib\"\n" + + "else\n" + + " echo \"ERROR: sysroot ELF interpreter not found (tried x86_64 and aarch64 paths)\" >&2\n" + + " exit 1\n" + + "fi\n" ) + for cmd in sysroot_setup_commands: + parts = cmd.split(" ") + binary = parts[0] + args = parts[1:] + args_shell = " ".join(['"' + a + '"' for a in args]) + block += ( + "LD_PRELOAD=\"$_FC_LIB\" " + + "FAKECHROOT_BASE=\"" + sysroot_dir + "\" " + + "\"$_SYSROOT_INTERP\" --library-path \"$_SYSROOT_LIBPATH\" " + + "\"" + sysroot_dir + binary + "\"" + + (" " + args_shell if args_shell else "") + + "\n" + ) return block def _prepare_sysroot_impl(ctx): @@ -117,12 +143,16 @@ prepare_sysroot = rule( ), "sysroot_setup_commands": attr.string_list( default = [], - doc = "Shell lines run inside the sysroot via LD_PRELOAD=libfakechroot.so " + - "+ FAKECHROOT_BASE after host_setup_commands complete. All absolute " + - "path accesses are redirected into the sysroot. Use this for post-" + - "install steps that need the sysroot's own binaries (e.g. " + - "'/usr/bin/dot -c' to regenerate the graphviz plugin manifest). " + - "Requires fakechroot to be present in the sysroot.", + doc = "Shell lines run inside the sysroot after host_setup_commands " + + "complete. Each entry must be a space-separated ELF binary " + + "invocation with an absolute sysroot path (e.g. '/usr/bin/dot -c'). " + + "Shell metacharacters (pipes, redirects) are not supported. " + + "The binary is executed via the sysroot's own ld-linux.so with " + + "--library-path pointing at the sysroot's /usr/lib tree, giving a " + + "fully consistent single-libc environment. LD_PRELOAD=libfakechroot " + + "+ FAKECHROOT_BASE are still active so glibc-level filesystem calls " + + "(e.g. writing config6) are transparently redirected into the sysroot. " + + "Requires fakechroot and ld-linux.so to be present in the sysroot.", ), }, toolchains = [_TAR_TOOLCHAIN_TYPE], diff --git a/bazel/rules/exec_in_sysroot/exec_in_sysroot.sh b/bazel/rules/exec_in_sysroot/exec_in_sysroot.sh index a0752917..f4918dcd 100755 --- a/bazel/rules/exec_in_sysroot/exec_in_sysroot.sh +++ b/bazel/rules/exec_in_sysroot/exec_in_sysroot.sh @@ -51,6 +51,25 @@ fi FAKECHROOT_LIB_DIR="$(dirname "${FAKECHROOT_LIB}")" +# Determine the sysroot's ELF interpreter (ld-linux.so) and the arch-specific +# library search path using well-known Debian multiarch paths rather than a +# fragile glob search. These are exported so that the wrapped executable can +# invoke sysroot ELF binaries via the sysroot's own dynamic linker — avoiding +# loading the sysroot's libc.so.6 alongside the host's already-loaded libc +# (two libc instances → segfault on systems without host graphviz installed). +# See third_party/docs_runtime/dot.sh for the usage pattern. +if [ -f "${SYSROOT_DIR}/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2" ]; then + SYSROOT_INTERP="${SYSROOT_DIR}/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2" + SYSROOT_LIBPATH="${SYSROOT_DIR}/usr/lib/x86_64-linux-gnu:${SYSROOT_DIR}/usr/lib" +elif [ -f "${SYSROOT_DIR}/usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1" ]; then + SYSROOT_INTERP="${SYSROOT_DIR}/usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1" + SYSROOT_LIBPATH="${SYSROOT_DIR}/usr/lib/aarch64-linux-gnu:${SYSROOT_DIR}/usr/lib" +else + echo "ERROR: sysroot ELF interpreter not found (tried x86_64 and aarch64 paths)" >&2 + exit 1 +fi +export SYSROOT_INTERP SYSROOT_LIBPATH + # Build the exclude paths list. # Always exclude BUILD_WORKSPACE_DIRECTORY so workspace operations stay on host. EXCLUDE_PATHS="" @@ -67,6 +86,21 @@ if [ -n "${FAKECHROOT_EXCLUDE_PATH:-}" ]; then fi fi +# Exclude SYSROOT_INTERP (the sysroot's ld-linux.so) so fakechroot does not +# intercept its execution. When a sysroot binary is launched via +# exec "$SYSROOT_INTERP" --library-path "$SYSROOT_LIBPATH" "$SYSROOT_DIR/bin" +# the path "$SYSROOT_DIR/…/ld-linux.so.2" starts with FAKECHROOT_BASE. +# Fakechroot would translate it to a sysroot-relative path and fail to exec it +# (the kernel reports ENOENT). Listing the full path here tells fakechroot to +# pass the execve through unchanged. +if [ -n "${SYSROOT_INTERP}" ]; then + if [ -n "${EXCLUDE_PATHS}" ]; then + EXCLUDE_PATHS="${EXCLUDE_PATHS}:${SYSROOT_INTERP}" + else + EXCLUDE_PATHS="${SYSROOT_INTERP}" + fi +fi + if [ -n "${EXCLUDE_PATHS}" ]; then export FAKECHROOT_EXCLUDE_PATH="${EXCLUDE_PATHS}" fi @@ -75,11 +109,6 @@ fi # directory. All absolute file-system calls (open, stat, execve, …) inside the # launched process are transparently redirected to SYSROOT_DIR/ unless the # path is listed in FAKECHROOT_EXCLUDE_PATH. -# -# Only the fakechroot-lib directory is added to LD_LIBRARY_PATH; adding the full -# sysroot lib tree would make the host linker pick up sysroot-snapshot versions of -# libgvc/libc/… which may differ from the build the sysroot binary was built with -# and cause crashes. export FAKECHROOT_BASE="${SYSROOT_DIR}" export LD_PRELOAD="${FAKECHROOT_LIB}${LD_PRELOAD:+:${LD_PRELOAD}}" export LD_LIBRARY_PATH="${FAKECHROOT_LIB_DIR}${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" diff --git a/bazel/rules/rules_score/docs/tooling_architecture.rst b/bazel/rules/rules_score/docs/tooling_architecture.rst index a5ce61e1..f001c7b6 100644 --- a/bazel/rules/rules_score/docs/tooling_architecture.rst +++ b/bazel/rules/rules_score/docs/tooling_architecture.rst @@ -334,3 +334,145 @@ The ``fmea`` rule drives three actions, all reading the input artifacts above: The lobster outputs travel separately on ``AnalysisInfo.lobster_files`` (``failuremodes.lobster``, ``controlmeasures.lobster``, ``root_causes.lobster``) into the ``dependability_analysis`` traceability report. + to the outer index toctree. ``fmea`` uses this for the ``detail_*.rst`` + sub-pages, which are referenced from the inner ``.. toctree::`` inside + ``fmea.rst`` rather than from the section index. + +.. _hermetic-tool-path-resolution: + +Hermetic tool path resolution +---------------- + +Background: Bazel action environment +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Paths available at **analysis time** (Starlark ``ctx.executable.foo.path``, +``ctx.executable.foo.short_path``) are always relative to the *execroot* — +the per-action working directory Bazel creates under the output base. Two +variants exist: + +- ``file.path`` — ``bazel-out//bin/third_party/docs_runtime/dot``. + Contains the exec-configuration hash; valid only at action run-time as a + path relative to ``cwd``. +- ``file.short_path`` — ``third_party/docs_runtime/dot`` (or + ``../external_repo/…``). Hash-free; stable across rebuilds; the canonical + **rlocation key** after stripping a leading ``../``. + +What the rule passes +~~~~~~~~~~~~~~~~~~~~~ + +For each tool ``sphinx_module.bzl`` computes both variants and injects them as +environment variables: + +.. code-block:: text + + PLANTUML_BIN = ctx.executable._plantuml.path (execroot-relative) + PLANTUML_BIN_RLOC = ctx.executable._plantuml.short_path (rlocation key) + GRAPHVIZ_DOT = ctx.executable._graphviz.path (execroot-relative) + GRAPHVIZ_DOT_RLOC = ctx.executable._graphviz.short_path (rlocation key) + +The rlocation keys (``*_RLOC``) are computed once at analysis time: + +.. code-block:: python + + _gv_short = ctx.executable._graphviz.short_path + _graphviz_rloc = ( + _gv_short[3:] # strip "../" + if _gv_short.startswith("../") + else ctx.workspace_name + "/" + _gv_short + ) + +This matches the Bazel bash-runfiles convention used internally by the +``exec_in_sysroot`` wrapper itself (``rlocation +'/third_party/docs_runtime/dot'``). + +How conf.template.py resolves the paths +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``conf.py`` is loaded during **Sphinx initialisation**, before Sphinx performs +any ``os.chdir()``. Bazel guarantees that the action's ``cwd`` equals the +execroot at process start. Therefore a single ``os.path.abspath()`` call +converts the execroot-relative ``*_BIN`` / ``*_DOT`` value to a stable +absolute path for the entire action lifetime: + +.. code-block:: python + + plantuml_path = os.path.abspath(os.environ["PLANTUML_BIN"]) + graphviz_dot = os.path.abspath(os.environ["GRAPHVIZ_DOT"]) + + +Why rlocation alone cannot resolve the binary +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A natural question is: why not call +``python.runfiles.Runfiles.Create().Rlocation(graphviz_rloc_key)`` directly and +skip the ``abspath`` step? + +Two reasons make this impractical: + +1. **Wrong ``RUNFILES_DIR``.** The Sphinx Python process inherits + ``RUNFILES_DIR`` pointing to the *sphinx tool's* own runfiles tree (set by + the ``rules_python`` ``py_binary`` launcher). The Graphviz and PlantUML + tools are in ``tools=`` of the action, which makes their files available in + the sandbox but does **not** merge them into the sphinx binary's runfiles. + Calling ``Runfiles.Create()`` without an override therefore searches the + wrong tree and returns ``None`` for both tool keys. + +2. **The symlink-path problem.** As a workaround one could construct + ``Runfiles.Create({"RUNFILES_DIR": abspath_dot + ".runfiles"})`` (the + companion runfiles directory Bazel creates for every executable). The dot + wrapper *is* a member of its own runfiles tree (``exec_in_sysroot`` adds + the generated script to ``ctx.runfiles(files=[out, …])`` so the smoke test + can resolve it via ``rlocation``). However, ``Runfiles.Rlocation()`` + returns the path **inside the symlink forest**, not the real binary path. + Passing that symlink path to a subprocess means ``$0`` is the symlink, so + ``$0.runfiles/`` does not exist and the wrapper's runfiles bootstrap falls + back to ``RUNFILES_DIR`` — which still points to the sphinx binary's + runfiles. The wrapper fails to find ``SYSROOT_DIR``, ``FAKECHROOT_WRAPPER``, + and ``EXECUTABLE_FILE`` and exits with an error. + +The ``os.path.abspath()`` approach avoids both issues: it yields the real +binary path (not a symlink in a runfiles forest), so ``$0.runfiles/`` +bootstrapping in the wrapper works correctly. + +The exec_in_sysroot wrapper's own runfiles bootstrap +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The generated wrapper produced by ``exec_in_sysroot`` contains the standard +Bazel bash-runfiles initialisation block: + +.. code-block:: bash + + if [[ ! -d "${RUNFILES_DIR:-/dev/null}" && \ + ! -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then + if [[ -f "$0.runfiles_manifest" ]]; then + export RUNFILES_MANIFEST_FILE="$0.runfiles_manifest" + elif [[ -f "$0.runfiles/MANIFEST" ]]; then + export RUNFILES_MANIFEST_FILE="$0.runfiles/MANIFEST" + elif [[ -f "$0.runfiles/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then + export RUNFILES_DIR="$0.runfiles" + fi + fi + +Because ``graphviz_dot`` is the **absolute** path to the real binary (not a +symlink), ``$0`` equals that absolute path, and ``$0.runfiles/`` is the actual +companion runfiles directory Bazel created for the binary. The block finds +``bazel_tools/tools/bash/runfiles/runfiles.bash`` there and correctly +bootstraps ``RUNFILES_DIR`` to the dot wrapper's own runfiles — regardless of +what ``RUNFILES_DIR`` the parent process (Sphinx/Python) has set in its +environment. + +PlantUML and the Smetana fallback +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``sphinxcontrib.plantuml`` invokes PlantUML via +``shlex.split(app.config.plantuml)``. When the hermetic dot is available the +``plantuml`` config value is: + +.. code-block:: text + + -graphvizdot + +This tells PlantUML to use the hermetic dot for its internal layout calls +(PlantUML generates a ``.dot`` intermediate for class/component/sequence +diagrams and hands it to graphviz for layout). diff --git a/bazel/rules/rules_score/private/sphinx_module.bzl b/bazel/rules/rules_score/private/sphinx_module.bzl index 3ea4c621..06c3123c 100644 --- a/bazel/rules/rules_score/private/sphinx_module.bzl +++ b/bazel/rules/rules_score/private/sphinx_module.bzl @@ -116,13 +116,24 @@ def _score_needs_impl(ctx): "--log-level", get_log_level(ctx), ] + + # Compute analysis-time-stable rlocation keys from short_path (no exec-config + # hash, no parent-directory walking). These are passed to conf.py for + # diagnostic logging and as the canonical Bazel identity of each tool. + # See docs/tooling_architecture.rst §"Hermetic tool path resolution". + _gv_short = ctx.executable._graphviz.short_path + _graphviz_rloc = _gv_short[3:] if _gv_short.startswith("../") else ctx.workspace_name + "/" + _gv_short + _pl_short = ctx.executable._plantuml.short_path + _plantuml_rloc = _pl_short[3:] if _pl_short.startswith("../") else ctx.workspace_name + "/" + _pl_short ctx.actions.run( inputs = needs_inputs, outputs = [needs_output], arguments = needs_args, env = { "PLANTUML_BIN": ctx.executable._plantuml.path, + "PLANTUML_BIN_RLOC": _plantuml_rloc, "GRAPHVIZ_DOT": ctx.executable._graphviz.path, + "GRAPHVIZ_DOT_RLOC": _graphviz_rloc, }, progress_message = "Generating needs.json for: %s" % ctx.label.name, executable = sphinx_toolchain.sphinx.files_to_run.executable, @@ -256,9 +267,18 @@ def _score_html_impl(ctx): # Use the hermetic graphviz wrapper that executes `/usr/bin/dot` inside the # docs_runtime sysroot via exec_in_sysroot. + # Compute analysis-time-stable rlocation keys from short_path (no exec-config + # hash, no parent-directory walking). See docs/tooling_architecture.rst + # §"Hermetic tool path resolution". + _gv_short = ctx.executable._graphviz.short_path + _graphviz_rloc = _gv_short[3:] if _gv_short.startswith("../") else ctx.workspace_name + "/" + _gv_short + _pl_short = ctx.executable._plantuml.short_path + _plantuml_rloc = _pl_short[3:] if _pl_short.startswith("../") else ctx.workspace_name + "/" + _pl_short action_env = { "PLANTUML_BIN": ctx.executable._plantuml.path, + "PLANTUML_BIN_RLOC": _plantuml_rloc, "GRAPHVIZ_DOT": ctx.executable._graphviz.path, + "GRAPHVIZ_DOT_RLOC": _graphviz_rloc, } ctx.actions.run( diff --git a/bazel/rules/rules_score/templates/conf.template.py b/bazel/rules/rules_score/templates/conf.template.py index 58d89e6b..41259225 100644 --- a/bazel/rules/rules_score/templates/conf.template.py +++ b/bazel/rules/rules_score/templates/conf.template.py @@ -28,50 +28,6 @@ # Create a logger with the Sphinx namespace logger = logging.getLogger(__name__) -# --------------------------------------------------------------------------- -# Helpers: Bazel execroot path resolution -# --------------------------------------------------------------------------- - - -# Capture the current working directory at module import time. -# In Bazel action context, cwd == execroot. In IDE/non-Bazel runs, cwd is -# the current directory. This is captured once to avoid repeated resolution. -_EXECROOT = Path.cwd() - - -def _resolve_execroot_path(path_value: str) -> str: - """Resolve an execroot-relative path to an absolute filesystem path. - - Bazel passes action inputs as paths relative to the execroot (e.g. - ``bazel-bin/third_party/docs_runtime/dot``). Those paths are only valid - when the process' cwd is the execroot — which is not guaranteed once - Sphinx changes directories during the build. - - This function makes them absolute so they work regardless of cwd. - Absolute paths and plain command names (e.g. ``dot``) are returned - unchanged. - """ - p = Path(path_value) - if p.is_absolute(): - return str(p) - if path_value.startswith("external/") or path_value.startswith("bazel-out/"): - # First try cwd-as-execroot (fast path). - candidate = (_EXECROOT / p).resolve() - if candidate.exists(): - return str(candidate) - - # If cwd is nested under bazel-out, walk upward and locate the first - # parent that contains the requested relative path. - for parent in [_EXECROOT, *_EXECROOT.parents]: - candidate = (parent / p).resolve() - if candidate.exists(): - return str(candidate) - - # Fallback: preserve previous behavior even if the file does not exist - # yet (keeps logging/debug output deterministic). - return str((_EXECROOT / p).resolve()) - return path_value - logger.debug("#" * 80) logger.debug("# READING CONF.PY") @@ -170,8 +126,22 @@ def _resolve_execroot_path(path_value: str) -> str: # --------------------------------------------------------------------------- # PlantUML binary discovery # --------------------------------------------------------------------------- -# PLANTUML_BIN is set by the Bazel sphinx_module rule to the explicit execroot- -# relative path of the //third_party/plantuml:plantuml java_binary launcher script. +# PLANTUML_BIN — execroot-relative path of //third_party/plantuml:plantuml +# (a rules_java java_binary launcher script), injected by the +# sphinx_module Bazel rule via the action env. +# PLANTUML_BIN_RLOC — analysis-time-stable Bazel rlocation key derived from the +# target's short_path (no exec-config hash); used only for +# diagnostic logging here. +# +# Path resolution rationale (applies to all hermetic tool paths in this file): +# os.path.abspath() converts the execroot-relative path to an absolute path +# using the process's current working directory. Bazel guarantees that the +# action's cwd equals the execroot at process start. conf.py is loaded during +# Sphinx initialisation — before Sphinx can perform any os.chdir() — so the +# abspath() call is stable for the entire action lifetime. This replaces the +# previous _resolve_execroot_path() which walked parent directories as a +# fallback, a pattern that is fragile and wrong when nested under bazel-out/. +# See docs/tooling_architecture.rst §"Hermetic tool path resolution". _plantuml_bin = os.environ.get("PLANTUML_BIN") if not _plantuml_bin: raise ValueError( @@ -180,34 +150,73 @@ def _resolve_execroot_path(path_value: str) -> str: "sphinx_module Bazel rule. If you are invoking Sphinx outside that rule, " "set PLANTUML_BIN to the plantuml binary path." ) -plantuml_path = _resolve_execroot_path(_plantuml_bin) +plantuml_path = os.path.abspath(_plantuml_bin) +logger.debug( + f"plantuml resolved: {plantuml_path} " + f"(rloc: {os.environ.get('PLANTUML_BIN_RLOC', 'n/a')})" +) plantuml_output_format = "svg_obj" -# `plantuml` is defined below, after graphviz_dot is resolved, so PlantUML can -# render with the same hermetic Graphviz dot (see end of the Graphviz section). +# `plantuml` command is assembled below, after graphviz_dot is resolved, so +# PlantUML can use the same hermetic Graphviz dot (see Graphviz section). # --------------------------------------------------------------------------- # Graphviz (sphinx.ext.graphviz) # --------------------------------------------------------------------------- -# GRAPHVIZ_DOT is set by the Bazel sphinx_module rule to point at the -# exec_in_sysroot wrapper from //third_party/docs_runtime:dot. -# The wrapper executes /usr/bin/dot inside the hermetic docs_runtime sysroot. -# Paths are passed execroot-relative, so resolve to absolute for robustness if -# Sphinx changes cwd before spawning dot. -# If GRAPHVIZ_DOT is absent, force a known-invalid dot path so Sphinx fails -# clearly on graphviz directives instead of silently using host-installed dot. -if "GRAPHVIZ_DOT" in os.environ: - graphviz_dot = _resolve_execroot_path(os.environ["GRAPHVIZ_DOT"]) +# GRAPHVIZ_DOT — execroot-relative path of //third_party/docs_runtime:dot +# (the exec_in_sysroot bash wrapper), injected by the +# sphinx_module Bazel rule. +# GRAPHVIZ_DOT_RLOC — analysis-time-stable rlocation key; logged only. +# +# Path resolution: same os.path.abspath() rationale as PLANTUML_BIN above. +# +# The exec_in_sysroot wrapper is a runfiles-aware bash script that bootstraps +# its own runfiles via the standard $0.runfiles/ Bazel convention (identical +# to the runfiles.bash init block). Passing the ABSOLUTE path ensures $0 is +# absolute, so $0.runfiles/ resolves to the correct companion directory even +# when the wrapper is called as a subprocess from inside the Sphinx Python +# process (which carries its own RUNFILES_DIR pointing at the sphinx tool's +# runfiles, not the dot wrapper's runfiles). +# +# If GRAPHVIZ_DOT is absent a known-invalid sentinel is used so that +# sphinx.ext.graphviz fails loudly on any .. graphviz:: directive rather than +# silently using a host-installed dot binary. +_graphviz_dot_path = os.environ.get("GRAPHVIZ_DOT", "") +_graphviz_dot_rloc = os.environ.get("GRAPHVIZ_DOT_RLOC", "") +if _graphviz_dot_path: + graphviz_dot = os.path.abspath(_graphviz_dot_path) graphviz_output_format = "svg" + logger.debug( + f"graphviz dot resolved: {graphviz_dot} (rloc: {_graphviz_dot_rloc or 'n/a'})" + ) else: graphviz_dot = "/__hermetic_graphviz_not_configured__/dot" -# Render PlantUML diagrams with the real (hermetic) Graphviz dot resolved above, -# giving reference Graphviz layout. Smetana (PlantUML's bundled Java port) was -# previously used only to avoid an external dot in the sandbox; the hermetic dot -# removes that constraint. Swap back to `-Playout=smetana` if the per-diagram dot -# subprocess regresses build time. -plantuml = f"{plantuml_path} -graphvizdot {graphviz_dot}" +# --------------------------------------------------------------------------- +# PlantUML layout engine: hermetic dot or Smetana fallback +# --------------------------------------------------------------------------- +# Wire PlantUML to the hermetic dot via -graphvizdot when the wrapper is +# accessible; otherwise fall back to PlantUML's built-in Smetana layout +# engine (a pure-Java re-implementation of Graphviz) with a warning. +# The hermetic dot is the reference rendering path; Smetana output may differ +# visually (different edge routing, node spacing). +# Note: the smetana fallback only affects PlantUML diagrams. sphinx.ext.graphviz +# directives always use graphviz_dot; they will fail if it is the sentinel. +_dot_available = ( + graphviz_dot != "/__hermetic_graphviz_not_configured__/dot" + and Path(graphviz_dot).is_file() +) +if _dot_available: + plantuml = f"{plantuml_path} -graphvizdot {graphviz_dot}" +else: + logger.warning( + f"Hermetic Graphviz dot is not available at {graphviz_dot!r}. " + "PlantUML is falling back to the built-in Smetana layout engine. " + "Diagrams may differ from the reference output produced by the " + "hermetic Bazel build. Ensure GRAPHVIZ_DOT / GRAPHVIZ_DOT_RLOC are " + "set correctly when invoking the sphinx_module rule." + ) + plantuml = f"{plantuml_path} -Playout=smetana" # HTML theme html_theme = "sphinx_rtd_theme" diff --git a/third_party/docs_runtime/BUILD b/third_party/docs_runtime/BUILD index 14283d87..3a747b52 100644 --- a/third_party/docs_runtime/BUILD +++ b/third_party/docs_runtime/BUILD @@ -66,6 +66,11 @@ prepare_sysroot( # Hermetic graphviz dot executable used by doc actions. exec_in_sysroot( name = "dot", + # /tmp must not be chrooted so that PlantUML's per-diagram .dot / .svg temp + # files (written to the real /tmp) are accessible to dot without being + # redirected into the sysroot. Graphviz-internal paths (/usr/lib/…/graphviz, + # /usr/share/fonts, …) are still correctly redirected by fakechroot. + exclude_paths = ["/tmp"], # fakechroot + the x86_64 sysroot binaries only run on a linux/x86_64 host. exec_compatible_with = [ "@platforms//cpu:x86_64", diff --git a/third_party/docs_runtime/dot.sh b/third_party/docs_runtime/dot.sh index cb56523f..dc695147 100755 --- a/third_party/docs_runtime/dot.sh +++ b/third_party/docs_runtime/dot.sh @@ -14,4 +14,18 @@ set -eu -exec /usr/bin/dot "$@" +# Use the sysroot's own ELF interpreter (exported by exec_in_sysroot.sh as +# SYSROOT_INTERP) with an explicit --library-path so that dot and all of its +# shared-library dependencies (libgvc.so.6, libcgraph.so.6, …) are loaded +# entirely from the sysroot. +# +# Running dot through the HOST's ld-linux.so and a wide LD_LIBRARY_PATH would +# cause the sysroot's libc.so.6 to load alongside the host's already-resident +# libc (two libc instances → segfault on systems without a host graphviz). +# The sysroot's own interpreter gives a single coherent libc. +# +# LD_PRELOAD=libfakechroot.so + FAKECHROOT_BASE are still active (set by +# exec_in_sysroot.sh) so glibc-level filesystem calls inside dot (e.g. +# opening the graphviz plugin directory, reading config6) are transparently +# redirected into the sysroot. +exec "${SYSROOT_INTERP}" --library-path "${SYSROOT_LIBPATH}" "${SYSROOT_DIR}/usr/bin/dot" "$@" From 3e6f2926cd5f1d93501d5736ab5b2f690806d8ee Mon Sep 17 00:00:00 2001 From: Jochen Hoenle <173445474+hoe-jo@users.noreply.github.com> Date: Wed, 1 Jul 2026 16:04:03 +0200 Subject: [PATCH 3/3] [rules score] refactor exec_in_sysroot - Remove duplicate implementation of sysroot-execution - Extract Shell script into template - Update Docs --- MODULE.bazel | 5 - bazel/rules/exec_in_sysroot/BUILD | 12 + .../rules/exec_in_sysroot/exec_in_sysroot.bzl | 235 ++++++------------ .../rules/exec_in_sysroot/exec_in_sysroot.sh | 64 +++-- .../exec_in_sysroot_launcher.sh.tpl | 86 +++++++ .../rules_score/docs/integration_guide.rst | 67 +++-- .../rules_score/docs/tooling_architecture.rst | 47 ++-- .../rules_score/private/sphinx_module.bzl | 16 +- bazel/rules/rules_score/src/sphinx_wrapper.py | 10 + .../rules_score/templates/conf.template.py | 87 ++++--- third_party/docs_runtime/BUILD | 11 +- third_party/docs_runtime/README.md | 25 +- third_party/docs_runtime/dot.sh | 31 --- .../docs_runtime/tests/dot_smoke_test.sh | 50 ++-- 14 files changed, 383 insertions(+), 363 deletions(-) create mode 100755 bazel/rules/exec_in_sysroot/exec_in_sysroot_launcher.sh.tpl delete mode 100755 third_party/docs_runtime/dot.sh diff --git a/MODULE.bazel b/MODULE.bazel index 353b3f6e..df843b21 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -34,11 +34,6 @@ bazel_dep(name = "bazel_skylib", version = "1.7.1") bazel_dep(name = "buildifier_prebuilt", version = "8.2.0.2") bazel_dep(name = "flatbuffers", version = "25.9.23") bazel_dep(name = "download_utils", version = "1.2.2") -git_override( - module_name = "download_utils", - commit = "3b96912fb6622dda83f25efd1f8ae596fc4a63a6", - remote = "https://gitlab.arm.com/bazel/download_utils.git", -) # flatbuffers depends on this transitively, but older grpc-java version # The main problem is that there the command `bazel mod deps` is broken, which diff --git a/bazel/rules/exec_in_sysroot/BUILD b/bazel/rules/exec_in_sysroot/BUILD index 9e0f07ff..5562bf60 100644 --- a/bazel/rules/exec_in_sysroot/BUILD +++ b/bazel/rules/exec_in_sysroot/BUILD @@ -13,7 +13,19 @@ load("@rules_shell//shell:sh_binary.bzl", "sh_binary") +package(default_visibility = ["//visibility:public"]) + sh_binary( name = "exec_in_sysroot", srcs = ["exec_in_sysroot.sh"], ) + +# Referenced as implicit deps by the exec_in_sysroot / prepare_sysroot rules: +# exec_in_sysroot.sh - shared sysroot launcher (also reused by +# prepare_sysroot for sysroot_setup_commands). +# exec_in_sysroot_launcher.sh.tpl - template for the generated runfiles-aware +# launcher emitted by exec_in_sysroot. +exports_files([ + "exec_in_sysroot.sh", + "exec_in_sysroot_launcher.sh.tpl", +]) diff --git a/bazel/rules/exec_in_sysroot/exec_in_sysroot.bzl b/bazel/rules/exec_in_sysroot/exec_in_sysroot.bzl index 450f75ba..b7f202fe 100644 --- a/bazel/rules/exec_in_sysroot/exec_in_sysroot.bzl +++ b/bazel/rules/exec_in_sysroot/exec_in_sysroot.bzl @@ -29,72 +29,42 @@ def _extract_and_clean(tar_bin, src, dest): """ return ( "mkdir -p \"" + dest + "\"\n" + - tar_bin + " -xf " + src + " -C \"" + dest + "\"\n" + + "\"" + tar_bin + "\" -xf \"" + src + "\" -C \"" + dest + "\"\n" + "find \"" + dest + "\" -type l -lname '.' -delete\n" + "find \"" + dest + "\" -xtype l -delete\n" ) -def _setup_block(sysroot_dir, host_setup_commands, sysroot_setup_commands): +def _setup_block(sysroot_dir, exec_wrapper, host_setup_commands, sysroot_setup_commands): """POSIX-sh snippet running the optional post-extract setup against `sysroot_dir` (an unquoted shell path expression) while it is still writable. host_setup_commands run in the outer shell with $SYSROOT set to sysroot_dir. + Use them for pure filesystem edits that only need the host shell. - sysroot_setup_commands are invoked directly via the sysroot's own ELF - interpreter (ld-linux.so) with --library-path pointing at the sysroot's - /usr/lib tree. This gives each command a fully consistent single-libc - environment (all of the binary's dependencies — including libc.so.6 itself — - come from the sysroot). LD_PRELOAD=libfakechroot.so + FAKECHROOT_BASE are - still set so that glibc-level filesystem calls inside the command (e.g. - writing the graphviz config6 file to /usr/lib/…) are transparently - redirected into the sysroot. + sysroot_setup_commands are each executed inside the sysroot by delegating to + the shared `exec_in_sysroot.sh` launcher (`exec_wrapper`) with SYSROOT_DIR + set to sysroot_dir. This is the *same* launcher exec_in_sysroot uses at + runtime, so build-time setup and runtime execution share a single + implementation of "run a binary inside the sysroot" (the sysroot's own + ld-linux.so + --library-path + LD_PRELOAD=libfakechroot.so + FAKECHROOT_BASE, + giving a fully consistent single-libc environment). Each entry in sysroot_setup_commands must be a space-separated ELF binary - invocation starting with an absolute sysroot path, e.g. "/usr/bin/dot -c". + invocation starting with an absolute sysroot path, e.g. "/usr/bin/tool --flag". Shell metacharacters (pipes, redirects, etc.) are not supported. """ block = "" if host_setup_commands: block += "SYSROOT=\"" + sysroot_dir + "\"\n" block += "\n".join(host_setup_commands) + "\n" - if sysroot_setup_commands: + for cmd in sysroot_setup_commands: + # Delegate to the shared launcher. `cmd` is intentionally left unquoted + # so the shell word-splits " " into the launcher's + # positional parameters (the space-separated contract documented above). block += ( - "_FC_LIB=\"$(find \"" + sysroot_dir + "/usr/lib\" -path '*/fakechroot/libfakechroot.so' -type f 2>/dev/null | head -1 || true)\"\n" + - "if [ -z \"$_FC_LIB\" ]; then\n" + - " echo \"ERROR: sysroot_setup_commands require fakechroot, but libfakechroot.so was not found under " + sysroot_dir + "/usr/lib\" >&2\n" + - " exit 1\n" + - "fi\n" + - # Use the sysroot's own ELF interpreter with an explicit --library-path - # so each command loads all dependencies from the sysroot's /usr/lib tree. - # Use well-known Debian multiarch paths instead of a fragile glob search. - # Running via the HOST ld-linux.so + LD_LIBRARY_PATH would load the - # sysroot's libc.so.6 alongside the host's already-loaded libc (two libc - # instances in one process → segfault). The sysroot's own interpreter - # gives a single coherent libc. - "if [ -f \"" + sysroot_dir + "/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2\" ]; then\n" + - " _SYSROOT_INTERP=\"" + sysroot_dir + "/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2\"\n" + - " _SYSROOT_LIBPATH=\"" + sysroot_dir + "/usr/lib/x86_64-linux-gnu:" + sysroot_dir + "/usr/lib\"\n" + - "elif [ -f \"" + sysroot_dir + "/usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1\" ]; then\n" + - " _SYSROOT_INTERP=\"" + sysroot_dir + "/usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1\"\n" + - " _SYSROOT_LIBPATH=\"" + sysroot_dir + "/usr/lib/aarch64-linux-gnu:" + sysroot_dir + "/usr/lib\"\n" + - "else\n" + - " echo \"ERROR: sysroot ELF interpreter not found (tried x86_64 and aarch64 paths)\" >&2\n" + - " exit 1\n" + - "fi\n" + "SYSROOT_DIR=\"" + sysroot_dir + "\" " + + "sh \"" + exec_wrapper + "\" " + cmd + "\n" ) - for cmd in sysroot_setup_commands: - parts = cmd.split(" ") - binary = parts[0] - args = parts[1:] - args_shell = " ".join(['"' + a + '"' for a in args]) - block += ( - "LD_PRELOAD=\"$_FC_LIB\" " + - "FAKECHROOT_BASE=\"" + sysroot_dir + "\" " + - "\"$_SYSROOT_INTERP\" --library-path \"$_SYSROOT_LIBPATH\" " + - "\"" + sysroot_dir + binary + "\"" + - (" " + args_shell if args_shell else "") + - "\n" - ) return block def _prepare_sysroot_impl(ctx): @@ -104,20 +74,19 @@ def _prepare_sysroot_impl(ctx): sysroot_archive = ctx.files.sysroot[0] bsdtar = ctx.toolchains[_TAR_TOOLCHAIN_TYPE] out_archive = ctx.actions.declare_file(ctx.label.name + ".tar") - work = out_archive.path + ".work" tar_bin = bsdtar.tarinfo.binary.path command = ( "set -eu\n" + - "rm -rf \"" + work + "\"\n" + - _extract_and_clean(tar_bin, sysroot_archive.path, work) + - _setup_block(work, ctx.attr.host_setup_commands, ctx.attr.sysroot_setup_commands) + - tar_bin + " -cf " + out_archive.path + " -C \"" + work + "\" .\n" + - "rm -rf \"" + work + "\"\n" + "work=\"$(mktemp -d)\"\n" + + "trap 'rm -rf \"$work\"' EXIT\n" + + _extract_and_clean(tar_bin, sysroot_archive.path, "$work") + + _setup_block("$work", ctx.file._exec_wrapper.path, ctx.attr.host_setup_commands, ctx.attr.sysroot_setup_commands) + + "\"" + tar_bin + "\" -cf \"" + out_archive.path + "\" -C \"$work\" .\n" ) ctx.actions.run_shell( - inputs = [sysroot_archive], + inputs = [sysroot_archive, ctx.file._exec_wrapper], outputs = [out_archive], tools = [bsdtar.default.files], command = command, @@ -139,20 +108,31 @@ prepare_sysroot = rule( doc = "Shell lines run in the outer (host) shell after extraction while " + "the sysroot is still writable. $SYSROOT is set to the sysroot " + "directory. Use this for filesystem operations that only need the " + - "host shell (e.g. removing unwanted plugins with find/rm).", + "host shell (e.g. removing files or creating symlinks with standard " + + "shell tools such as find/rm/ln).", ), "sysroot_setup_commands": attr.string_list( default = [], - doc = "Shell lines run inside the sysroot after host_setup_commands " + - "complete. Each entry must be a space-separated ELF binary " + - "invocation with an absolute sysroot path (e.g. '/usr/bin/dot -c'). " + - "Shell metacharacters (pipes, redirects) are not supported. " + - "The binary is executed via the sysroot's own ld-linux.so with " + - "--library-path pointing at the sysroot's /usr/lib tree, giving a " + - "fully consistent single-libc environment. LD_PRELOAD=libfakechroot " + - "+ FAKECHROOT_BASE are still active so glibc-level filesystem calls " + - "(e.g. writing config6) are transparently redirected into the sysroot. " + - "Requires fakechroot and ld-linux.so to be present in the sysroot.", + doc = "ELF binary invocations run inside the sysroot after " + + "host_setup_commands complete. Each entry is word-split by the " + + "shell to separate the binary path from its arguments; entries must " + + "not contain arguments with embedded spaces, and shell metacharacters " + + "(pipes, redirects) are not supported. The binary path must be " + + "absolute within the sysroot (e.g. '/usr/bin/tool --flag'). The binary " + + "is executed via the sysroot's own ld-linux.so with --library-path " + + "pointing at the sysroot's /usr/lib tree, giving a fully consistent " + + "single-libc environment. LD_PRELOAD=libfakechroot + FAKECHROOT_BASE " + + "are still active so absolute filesystem calls are transparently " + + "redirected into the sysroot. Requires fakechroot and " + + "ld-linux.so to be present in the sysroot. NOTE: sysroot binaries are " + + "executed via the sysroot's ELF interpreter on the host kernel, so the " + + "sysroot architecture must match the host architecture.", + ), + "_exec_wrapper": attr.label( + default = Label("//bazel/rules/exec_in_sysroot:exec_in_sysroot.sh"), + allow_single_file = True, + doc = "Shared sysroot launcher reused to run sysroot_setup_commands, " + + "so build-time setup and runtime execution use one implementation.", ), }, toolchains = [_TAR_TOOLCHAIN_TYPE], @@ -160,7 +140,6 @@ prepare_sysroot = rule( Unpacks a sysroot archive, removes symlinks that break Bazel TreeArtifact validation, runs optional host/sysroot setup commands while the tree is writable, and repackages the result into a single `.tar` archive. - """, ) @@ -196,96 +175,27 @@ def _exec_in_sysroot_impl(ctx): else: sysroot_runfiles_path = ctx.workspace_name + "/" + sysroot_short_path - executable_file = ctx.executable.executable - if executable_file == None: - fail("executable must provide a runnable target") - executable_short_path = executable_file.short_path - if executable_short_path.startswith("../"): - executable_runfiles_path = executable_short_path[3:] - else: - executable_runfiles_path = ctx.workspace_name + "/" + executable_short_path - out = ctx.actions.declare_file(ctx.label.name) # Build exclude paths string - colon-separated list exclude_paths = ":".join(ctx.attr.exclude_paths) if ctx.attr.exclude_paths else "" - wrapper_script = """#!/usr/bin/env bash -set -euo pipefail - -# --- begin runfiles.bash initialization --- -if [[ ! -d "${{RUNFILES_DIR:-/dev/null}}" && ! -f "${{RUNFILES_MANIFEST_FILE:-/dev/null}}" ]]; then - if [[ -f "$0.runfiles_manifest" ]]; then - export RUNFILES_MANIFEST_FILE="$0.runfiles_manifest" - elif [[ -f "$0.runfiles/MANIFEST" ]]; then - export RUNFILES_MANIFEST_FILE="$0.runfiles/MANIFEST" - elif [[ -f "$0.runfiles/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then - export RUNFILES_DIR="$0.runfiles" - fi -fi -if [[ -f "${{RUNFILES_DIR:-/dev/null}}/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then - source "${{RUNFILES_DIR}}/bazel_tools/tools/bash/runfiles/runfiles.bash" -elif [[ -f "${{RUNFILES_MANIFEST_FILE:-/dev/null}}" ]]; then - source "$(grep -m1 '^bazel_tools/tools/bash/runfiles/runfiles.bash ' "$RUNFILES_MANIFEST_FILE" | cut -d ' ' -f 2-)" -else - echo >&2 "ERROR: cannot find @bazel_tools//tools/bash/runfiles:runfiles.bash" - exit 1 -fi -# --- end runfiles.bash initialization --- - -FAKECHROOT_WRAPPER="$(rlocation '{wrapper_short_path}')" -SYSROOT_DIR="$(rlocation '{sysroot_short_path}')" -EXECUTABLE_FILE="$(rlocation '{executable_runfiles_path}')" - -if [[ -z "${{FAKECHROOT_WRAPPER}}" || ! -x "${{FAKECHROOT_WRAPPER}}" ]]; then - echo "ERROR: could not resolve fakechroot wrapper: {wrapper_short_path}" >&2 - exit 1 -fi - -if [[ -z "${{SYSROOT_DIR}}" || ! -d "${{SYSROOT_DIR}}" ]]; then - echo "ERROR: could not resolve sysroot directory: {sysroot_short_path}" >&2 - exit 1 -fi - -if [[ ! -x "${{SYSROOT_DIR}}/usr/bin/fakechroot" ]]; then - echo "ERROR: sysroot does not provide /usr/bin/fakechroot: ${{SYSROOT_DIR}}" >&2 - exit 1 -fi - -if [[ -z "${{EXECUTABLE_FILE}}" || ! -f "${{EXECUTABLE_FILE}}" ]]; then - echo "ERROR: could not resolve executable target: {executable_runfiles_path}" >&2 - exit 1 -fi - -export SYSROOT_DIR -if [[ -n "{exclude_paths}" ]]; then - export FAKECHROOT_EXCLUDE_PATH="{exclude_paths}" -fi - -# The executable lives in host runfiles, not in the sysroot. Exclude its path -# so fakechroot does not redirect accesses to it into the sysroot. -EXECUTABLE_DIR="$(dirname "${{EXECUTABLE_FILE}}")" -if [[ -n "${{FAKECHROOT_EXCLUDE_PATH:-}}" ]]; then - export FAKECHROOT_EXCLUDE_PATH="${{EXECUTABLE_DIR}}:${{EXECUTABLE_FILE}}:${{FAKECHROOT_EXCLUDE_PATH}}" -else - export FAKECHROOT_EXCLUDE_PATH="${{EXECUTABLE_DIR}}:${{EXECUTABLE_FILE}}" -fi - -exec "${{FAKECHROOT_WRAPPER}}" "${{EXECUTABLE_FILE}}" "$@" -""".format( - wrapper_short_path = ctx.workspace_name + "/" + ctx.executable._fakechroot_wrapper.short_path, - sysroot_short_path = sysroot_runfiles_path, - executable_runfiles_path = executable_runfiles_path, - exclude_paths = exclude_paths, + ctx.actions.expand_template( + template = ctx.file._launcher_template, + output = out, + is_executable = True, + substitutions = { + "{{WRAPPER_SHORT_PATH}}": ctx.workspace_name + "/" + ctx.executable._fakechroot_wrapper.short_path, + "{{SYSROOT_SHORT_PATH}}": sysroot_runfiles_path, + "{{SYSROOT_BINARY}}": ctx.attr.sysroot_binary, + "{{EXCLUDE_PATHS}}": exclude_paths, + }, ) - ctx.actions.write(output = out, content = wrapper_script, is_executable = True) runfiles = ctx.runfiles( - files = [out, ctx.executable._fakechroot_wrapper, sysroot, executable_file] + ctx.files._bash_runfiles, + files = [ctx.executable._fakechroot_wrapper, sysroot], ) - runfiles = _merge_default_and_data_runfiles(ctx.attr.executable, runfiles) runfiles = _merge_default_and_data_runfiles(ctx.attr._fakechroot_wrapper, runfiles) - runfiles = _merge_default_and_data_runfiles(ctx.attr._bash_runfiles, runfiles) runfiles = _merge_default_and_data_runfiles(ctx.attr.sysroot, runfiles) return [DefaultInfo( @@ -298,30 +208,37 @@ exec_in_sysroot = rule( implementation = _exec_in_sysroot_impl, executable = True, attrs = { - "executable": attr.label(mandatory = True, executable = True, cfg = "exec"), - "sysroot": attr.label(mandatory = True, allow_single_file = True), + "sysroot_binary": attr.string( + mandatory = True, + doc = "Sysroot-relative path of the ELF binary to execute, e.g. '/usr/bin/tool'.", + ), + "sysroot": attr.label( + mandatory = True, + allow_single_file = True, + doc = "Prepared sysroot archive, typically the output of a prepare_sysroot rule.", + ), "exclude_paths": attr.string_list( default = [], - doc = "Paths to exclude from fakechroot path-redirection (colon-separated).", - ), - "_bash_runfiles": attr.label( - default = Label("@bazel_tools//tools/bash/runfiles"), - allow_files = True, + doc = "List of paths to exclude from fakechroot path-redirection.", ), "_fakechroot_wrapper": attr.label( default = Label("//bazel/rules/exec_in_sysroot"), executable = True, cfg = "exec", + doc = "Shared sysroot launcher (exec_in_sysroot.sh) invoked at runtime.", + ), + "_launcher_template": attr.label( + default = Label("//bazel/rules/exec_in_sysroot:exec_in_sysroot_launcher.sh.tpl"), + allow_single_file = True, ), }, toolchains = [_TAR_TOOLCHAIN_TYPE], doc = """ - Produces an executable wrapper that runs a given executable target using the - supplied sysroot archive. The archive is unpacked in-rule and the wrapped - executable runs within fakechroot via LD_PRELOAD, allowing access to sysroot - tools and libraries hermetically. + Produces an executable wrapper that runs a sysroot ELF binary via the + sysroot's own ld-linux.so (SYSROOT_INTERP) and fakechroot (LD_PRELOAD), + giving the binary a hermetic single-libc environment backed by the sysroot. - The archive is expected to be a reworked sysroot (see prepare_sysroot), which - performs plugin pruning / post-install setup once and caches the result. + The sysroot archive is expected to be prepared by a prepare_sysroot rule, + which performs filesystem preparation / post-install setup once and caches the result. """, ) diff --git a/bazel/rules/exec_in_sysroot/exec_in_sysroot.sh b/bazel/rules/exec_in_sysroot/exec_in_sysroot.sh index f4918dcd..1adbe886 100755 --- a/bazel/rules/exec_in_sysroot/exec_in_sysroot.sh +++ b/bazel/rules/exec_in_sysroot/exec_in_sysroot.sh @@ -11,27 +11,52 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -# Generic wrapper script that executes a command in fakechroot using the provided sysroot. +# Generic wrapper script that executes a command inside a sysroot using fakechroot. # -# Rather than using `fakechroot -- chroot SYSROOT COMMAND` (which requires COMMAND -# to exist inside the chroot), this script uses fakechroot's LD_PRELOAD mechanism -# directly: libfakechroot.so intercepts all file-system calls and redirects absolute -# paths to the sysroot, while FAKECHROOT_EXCLUDE_PATH allows the host-side launcher -# script to be found at its real location. +# Rather than using `fakechroot -- chroot SYSROOT COMMAND` (which relies on the +# host's dynamic linker and would cause the host libc and the sysroot libc to be +# loaded simultaneously, resulting in a segfault), this script directly invokes the +# sysroot's own ELF interpreter (ld-linux.so) with --library-path pointing at the +# sysroot's library tree. This guarantees a single-libc environment: all shared +# libraries, including libc, come exclusively from the sysroot. fakechroot is still +# loaded via LD_PRELOAD so that absolute file-system calls are transparently +# redirected into the sysroot, but the chroot simulation itself is bypassed entirely. +# +# WHY NOT `fakechroot -- chroot SYSROOT $SYSROOT_INTERP --library-path ...`? +# +# One might try to preserve the fakechroot binary invocation (in case it does more +# than just set LD_PRELOAD) while still using the sysroot's own ld-linux.so. This +# does not work cleanly for the following reason: +# +# ld-linux.so.2 is the dynamic linker itself. When invoked as a standalone +# executable it bootstraps before glibc exists and performs its own file I/O +# via raw kernel syscalls, NOT via glibc wrappers. Because fakechroot works +# exclusively through LD_PRELOAD (hooking glibc wrappers), it cannot intercept +# ld-linux.so.2's internal library lookups. The --library-path argument passed +# to ld-linux.so.2 is therefore resolved against the real host filesystem. If +# relative (chroot-relative) paths are used, ld-linux.so.2 loads host libraries +# → dual libc → segfault. If absolute sysroot paths are used instead, fakechroot +# would double-translate them (prepend SYSROOT_DIR a second time) unless every +# such path is individually listed in FAKECHROOT_EXCLUDE_PATH. +# +# The fakechroot binary itself adds little beyond setting LD_PRELOAD and +# FAKECHROOT_BASE plus substituting package-management commands (ldconfig, chown, +# etc.) that are irrelevant when running a plain ELF binary. Setting those two +# environment variables directly (as this script does) is fully equivalent for +# this use case. # # Usage: -# exec_in_sysroot.sh [arguments...] +# exec_in_sysroot.sh [arguments...] # # Environment variables (set by the generated exec_in_sysroot wrapper): # SYSROOT_DIR - Path to the extracted sysroot directory (required) -# FAKECHROOT_EXCLUDE_PATH - Colon-separated paths NOT to redirect (the host -# executable and its runfiles dir are pre-excluded) +# FAKECHROOT_EXCLUDE_PATH - Colon-separated paths NOT to redirect (optional) # BUILD_WORKSPACE_DIRECTORY - Workspace root, automatically excluded (optional) set -eu COMMAND_IN_SYSROOT="${1:?Command must be provided as first argument}" -shift || true +shift SYSROOT_DIR="${SYSROOT_DIR:?SYSROOT_DIR must be set}" @@ -42,10 +67,7 @@ elif [ -f "${SYSROOT_DIR}/usr/lib/aarch64-linux-gnu/fakechroot/libfakechroot.so" FAKECHROOT_LIB="${SYSROOT_DIR}/usr/lib/aarch64-linux-gnu/fakechroot/libfakechroot.so" fi if [ -z "${FAKECHROOT_LIB}" ]; then - FAKECHROOT_LIB="$(find "${SYSROOT_DIR}/usr/lib" -path '*/fakechroot/libfakechroot.so' -type f | head -1 || true)" -fi -if [ -z "${FAKECHROOT_LIB}" ]; then - echo "ERROR: libfakechroot.so not found in sysroot" >&2 + echo "ERROR: libfakechroot.so not found in sysroot (tried x86_64 and aarch64 multiarch paths)" >&2 exit 1 fi @@ -53,11 +75,10 @@ FAKECHROOT_LIB_DIR="$(dirname "${FAKECHROOT_LIB}")" # Determine the sysroot's ELF interpreter (ld-linux.so) and the arch-specific # library search path using well-known Debian multiarch paths rather than a -# fragile glob search. These are exported so that the wrapped executable can -# invoke sysroot ELF binaries via the sysroot's own dynamic linker — avoiding -# loading the sysroot's libc.so.6 alongside the host's already-loaded libc -# (two libc instances → segfault on systems without host graphviz installed). -# See third_party/docs_runtime/dot.sh for the usage pattern. +# fragile glob search. The sysroot binary is always executed via SYSROOT_INTERP +# so all of its dependencies (including libc.so.6) come from the sysroot — +# avoiding loading the sysroot's libc alongside the host's (two libc instances +# → segfault). if [ -f "${SYSROOT_DIR}/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2" ]; then SYSROOT_INTERP="${SYSROOT_DIR}/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2" SYSROOT_LIBPATH="${SYSROOT_DIR}/usr/lib/x86_64-linux-gnu:${SYSROOT_DIR}/usr/lib" @@ -68,7 +89,6 @@ else echo "ERROR: sysroot ELF interpreter not found (tried x86_64 and aarch64 paths)" >&2 exit 1 fi -export SYSROOT_INTERP SYSROOT_LIBPATH # Build the exclude paths list. # Always exclude BUILD_WORKSPACE_DIRECTORY so workspace operations stay on host. @@ -111,6 +131,8 @@ fi # path is listed in FAKECHROOT_EXCLUDE_PATH. export FAKECHROOT_BASE="${SYSROOT_DIR}" export LD_PRELOAD="${FAKECHROOT_LIB}${LD_PRELOAD:+:${LD_PRELOAD}}" +# Add the fakechroot library directory to LD_LIBRARY_PATH so fakechroot's own +# shared dependencies are resolved without falling back to host libraries. export LD_LIBRARY_PATH="${FAKECHROOT_LIB_DIR}${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" -exec "${COMMAND_IN_SYSROOT}" "$@" +exec "${SYSROOT_INTERP}" --library-path "${SYSROOT_LIBPATH}" "${SYSROOT_DIR}${COMMAND_IN_SYSROOT}" "$@" diff --git a/bazel/rules/exec_in_sysroot/exec_in_sysroot_launcher.sh.tpl b/bazel/rules/exec_in_sysroot/exec_in_sysroot_launcher.sh.tpl new file mode 100755 index 00000000..c526c625 --- /dev/null +++ b/bazel/rules/exec_in_sysroot/exec_in_sysroot_launcher.sh.tpl @@ -0,0 +1,86 @@ +#!/bin/sh +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +# +# Runfiles-aware launcher generated by the exec_in_sysroot rule. +# +# It resolves the extracted sysroot TreeArtifact and the shared +# exec_in_sysroot.sh wrapper from its own runfiles (via the $0.runfiles/ +# convention, independent of any RUNFILES_DIR the parent process may carry), +# then execs the wrapper with the sysroot-relative binary path. +# +# The following tokens are substituted at analysis time by ctx.actions.expand_template: +# {{WRAPPER_SHORT_PATH}} - rlocation key of exec_in_sysroot.sh (_fakechroot_wrapper) +# {{SYSROOT_SHORT_PATH}} - rlocation key of the extracted sysroot TreeArtifact +# {{SYSROOT_BINARY}} - sysroot-absolute path of the ELF binary to run +# {{EXCLUDE_PATHS}} - colon-separated FAKECHROOT_EXCLUDE_PATH entries (may be empty) +set -eu + +# --- begin POSIX sh runfiles init --- +# Prefer $0.runfiles/ (the companion directory Bazel creates for this specific +# binary) over any inherited RUNFILES_DIR from the parent process. When this +# launcher is invoked as a subprocess from another tool (e.g. Sphinx via +# sphinx.ext.graphviz or PlantUML's -graphvizdot), the parent process carries +# its own RUNFILES_DIR pointing at a different binary's runfiles tree. The +# standard bootstrap guard (if [ ! -d "${RUNFILES_DIR:-/dev/null}" ]) would +# accept the inherited value and cause rlocation to search the wrong tree, +# failing to find the sysroot. +# +# By probing $0.runfiles/ first we always use this binary's own companion +# runfiles directory when it exists (Bazel creates it whenever this binary is +# used as an action tool or via bazel run). We fall through to the inherited +# RUNFILES_DIR/RUNFILES_MANIFEST_FILE only when $0.runfiles/ does not exist, +# which happens when this binary is accessed via another target's data dep (e.g. +# the smoke test resolves it via rlocation and the path is inside the test's +# runfiles tree, where no companion .runfiles/ is created). +if [ -d "$0.runfiles" ]; then + RUNFILES_DIR="$0.runfiles"; export RUNFILES_DIR + unset RUNFILES_MANIFEST_FILE 2>/dev/null || true +elif [ -f "$0.runfiles_manifest" ]; then + RUNFILES_MANIFEST_FILE="$0.runfiles_manifest"; export RUNFILES_MANIFEST_FILE + unset RUNFILES_DIR 2>/dev/null || true +elif [ -f "$0.runfiles/MANIFEST" ]; then + RUNFILES_MANIFEST_FILE="$0.runfiles/MANIFEST"; export RUNFILES_MANIFEST_FILE + unset RUNFILES_DIR 2>/dev/null || true +elif [ ! -d "${RUNFILES_DIR:-/dev/null}" ] && [ ! -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]; then + echo >&2 "ERROR: cannot find runfiles for $0" + exit 1 +fi +rlocation() { + if [ -n "${RUNFILES_DIR:-}" ] && [ -e "${RUNFILES_DIR}/$1" ]; then + printf '%s' "${RUNFILES_DIR}/$1" + elif [ -n "${RUNFILES_MANIFEST_FILE:-}" ]; then + grep -m1 "^$1 " "${RUNFILES_MANIFEST_FILE}" | cut -d' ' -f2- + fi +} +# --- end POSIX sh runfiles init --- + +FAKECHROOT_WRAPPER="$(rlocation '{{WRAPPER_SHORT_PATH}}')" +SYSROOT_DIR="$(rlocation '{{SYSROOT_SHORT_PATH}}')" + +if [ -z "${FAKECHROOT_WRAPPER}" ] || [ ! -x "${FAKECHROOT_WRAPPER}" ]; then + echo "ERROR: could not resolve fakechroot wrapper: {{WRAPPER_SHORT_PATH}}" >&2 + exit 1 +fi + +if [ -z "${SYSROOT_DIR}" ] || [ ! -d "${SYSROOT_DIR}" ]; then + echo "ERROR: could not resolve sysroot directory: {{SYSROOT_SHORT_PATH}}" >&2 + exit 1 +fi + +export SYSROOT_DIR +if [ -n "{{EXCLUDE_PATHS}}" ]; then + export FAKECHROOT_EXCLUDE_PATH="{{EXCLUDE_PATHS}}" +fi + +exec "${FAKECHROOT_WRAPPER}" "{{SYSROOT_BINARY}}" "$@" diff --git a/bazel/rules/rules_score/docs/integration_guide.rst b/bazel/rules/rules_score/docs/integration_guide.rst index 313f7b7e..ddee0901 100644 --- a/bazel/rules/rules_score/docs/integration_guide.rst +++ b/bazel/rules/rules_score/docs/integration_guide.rst @@ -229,13 +229,12 @@ executes graphviz from the sysroot itself, so no custom ``LD_LIBRARY_PATH`` / **Resolving paths in conf.py** ``GRAPHVIZ_DOT`` is set as an *execroot-relative* path. Because Sphinx changes -the process working directory during the build, these paths would break if -used as-is. ``conf.template.py`` therefore: - -1. Captures ``_EXECROOT = Path.cwd()`` at **module import time** (cwd is still - the execroot at that point). -2. Calls ``_resolve_execroot_path()`` on the value to prepend ``_EXECROOT`` - and produce absolute paths. +the process working directory during the build, it would break if used as-is. +``conf.template.py`` converts it to a stable absolute path with a single +``os.path.abspath()`` call at **module import time**, when Bazel guarantees the +action's cwd still equals the execroot (before Sphinx performs any +``os.chdir()``). See :doc:`tooling_architecture` §"Hermetic tool path +resolution" for the full rationale. PlantUML ~~~~~~~~ @@ -244,40 +243,35 @@ PlantUML PlantUML is fetched from **Maven Central** via ``rules_jvm_external`` (declared in ``MODULE.bazel``). It is wrapped as a ``java_binary`` at -``//tools/sphinx:plantuml`` in ``tools/sphinx/BUILD``. - -The PlantUML target is added to the sphinx-build binary (``raw_build``) as a -``data`` dependency, making it a **runfile** of that binary — not an -independent action input. +``//third_party/plantuml:plantuml`` in ``third_party/plantuml/BUILD``. -**Where the file lands (runfiles-relative path)** +The ``sphinx_module`` rule passes the target as an action **tool** +(``attr.label(executable = True, cfg = "exec")``), exactly like the hermetic +graphviz dot. It is not a runfile of the sphinx-build binary. -.. code-block:: text - - {sphinx_build_binary}.runfiles/ - {repo_name}/tools/sphinx/plantuml ← wrapper script (absolute path via Runfiles API) +**Wiring into the Sphinx action** -The ``{repo_name}`` prefix depends on the Bzlmod configuration: +The Bazel rule sets one variable (mirroring ``GRAPHVIZ_DOT``): -- ``_main`` — when score_tooling is the root module (e.g.\ building within - the score-tooling repo itself) -- ``score_tooling`` / ``score_tooling+`` / ``score_tooling~`` — when - score_tooling is an external dependency of another project +.. list-table:: + :widths: 30 70 + :header-rows: 1 -**Discovering the binary in conf.py** + * - Env var + - Content + * - ``PLANTUML_BIN`` + - Execroot-relative path to the ``plantuml`` ``java_binary`` launcher -Because the repo name varies, ``conf.template.py`` uses two-stage discovery: +``PLANTUML_BIN_RLOC`` (the ``short_path`` rlocation key) is also set, but is +used only for diagnostic logging. -1. **Manifest scan (primary):** Read ``RUNFILES_MANIFEST_FILE`` and search - for any entry whose runfiles path ends in ``/tools/sphinx/plantuml``. This - requires no knowledge of the repo name prefix. -2. **Runfiles API fallback:** If no manifest file is available (directory-based - runfiles trees on some platforms), fall back to - ``Runfiles.Create().Rlocation()`` with a list of known repo-name candidates - (``_main``, ``score_tooling``, ``score_tooling+``, …). +**Resolving the path in conf.py** -The Runfiles API returns an **absolute path** directly, so no -``_resolve_execroot_path()`` is required for PlantUML. +``PLANTUML_BIN`` is an *execroot-relative* path. As with ``GRAPHVIZ_DOT``, +``conf.template.py`` converts it to an absolute path with a single +``os.path.abspath()`` call — Bazel guarantees the action's cwd equals the +execroot when ``conf.py`` is imported, before Sphinx performs any +``os.chdir()``. **Connecting PlantUML to Graphviz** @@ -289,6 +283,7 @@ command: plantuml = f"{plantuml_path} -graphvizdot {graphviz_dot}" The ``-graphvizdot`` flag makes PlantUML use the hermetic ``dot`` binary for -diagram layout instead of its bundled Java port (Smetana). This ensures that -the graphviz version is identical for both ``sphinx.ext.graphviz`` directives -and PlantUML diagrams. +diagram layout instead of its bundled Java port (Smetana). This ensures the +graphviz version is identical for both ``sphinx.ext.graphviz`` directives and +PlantUML diagrams. There is no Smetana fallback: the hermetic dot is the +single rendering path. diff --git a/bazel/rules/rules_score/docs/tooling_architecture.rst b/bazel/rules/rules_score/docs/tooling_architecture.rst index f001c7b6..d1eb04df 100644 --- a/bazel/rules/rules_score/docs/tooling_architecture.rst +++ b/bazel/rules/rules_score/docs/tooling_architecture.rst @@ -382,7 +382,7 @@ The rlocation keys (``*_RLOC``) are computed once at analysis time: else ctx.workspace_name + "/" + _gv_short ) -This matches the Bazel bash-runfiles convention used internally by the +This matches the Bazel runfiles convention used internally by the ``exec_in_sysroot`` wrapper itself (``rlocation '/third_party/docs_runtime/dot'``). @@ -428,8 +428,8 @@ Two reasons make this impractical: Passing that symlink path to a subprocess means ``$0`` is the symlink, so ``$0.runfiles/`` does not exist and the wrapper's runfiles bootstrap falls back to ``RUNFILES_DIR`` — which still points to the sphinx binary's - runfiles. The wrapper fails to find ``SYSROOT_DIR``, ``FAKECHROOT_WRAPPER``, - and ``EXECUTABLE_FILE`` and exits with an error. + runfiles. The wrapper fails to find ``SYSROOT_DIR`` and ``FAKECHROOT_WRAPPER`` + and exits with an error. The ``os.path.abspath()`` approach avoids both issues: it yields the real binary path (not a symlink in a runfiles forest), so ``$0.runfiles/`` @@ -438,36 +438,37 @@ bootstrapping in the wrapper works correctly. The exec_in_sysroot wrapper's own runfiles bootstrap ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The generated wrapper produced by ``exec_in_sysroot`` contains the standard -Bazel bash-runfiles initialisation block: +The generated wrapper produced by ``exec_in_sysroot`` is a POSIX-``sh`` script +that contains an inline runfiles initialisation block (no ``bash`` and no +``runfiles.bash`` dependency): -.. code-block:: bash +.. code-block:: sh - if [[ ! -d "${RUNFILES_DIR:-/dev/null}" && \ - ! -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then - if [[ -f "$0.runfiles_manifest" ]]; then - export RUNFILES_MANIFEST_FILE="$0.runfiles_manifest" - elif [[ -f "$0.runfiles/MANIFEST" ]]; then - export RUNFILES_MANIFEST_FILE="$0.runfiles/MANIFEST" - elif [[ -f "$0.runfiles/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then - export RUNFILES_DIR="$0.runfiles" + if [ ! -d "${RUNFILES_DIR:-/dev/null}" ] && \ + [ ! -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]; then + if [ -f "$0.runfiles_manifest" ]; then + RUNFILES_MANIFEST_FILE="$0.runfiles_manifest"; export RUNFILES_MANIFEST_FILE + elif [ -f "$0.runfiles/MANIFEST" ]; then + RUNFILES_MANIFEST_FILE="$0.runfiles/MANIFEST"; export RUNFILES_MANIFEST_FILE + elif [ -d "$0.runfiles" ]; then + RUNFILES_DIR="$0.runfiles"; export RUNFILES_DIR fi fi Because ``graphviz_dot`` is the **absolute** path to the real binary (not a symlink), ``$0`` equals that absolute path, and ``$0.runfiles/`` is the actual -companion runfiles directory Bazel created for the binary. The block finds -``bazel_tools/tools/bash/runfiles/runfiles.bash`` there and correctly -bootstraps ``RUNFILES_DIR`` to the dot wrapper's own runfiles — regardless of -what ``RUNFILES_DIR`` the parent process (Sphinx/Python) has set in its -environment. +companion runfiles directory Bazel created for the binary. The block resolves +``RUNFILES_DIR`` (or ``RUNFILES_MANIFEST_FILE``) from ``$0.runfiles/``, and a +small inline ``rlocation`` helper then looks up ``SYSROOT_DIR`` and the +fakechroot wrapper — regardless of what ``RUNFILES_DIR`` the parent process +(Sphinx/Python) has set in its environment. -PlantUML and the Smetana fallback -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +PlantUML and the hermetic dot +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``sphinxcontrib.plantuml`` invokes PlantUML via -``shlex.split(app.config.plantuml)``. When the hermetic dot is available the -``plantuml`` config value is: +``shlex.split(app.config.plantuml)``. The ``plantuml`` config value always +points at the hermetic dot (there is no fallback): .. code-block:: text diff --git a/bazel/rules/rules_score/private/sphinx_module.bzl b/bazel/rules/rules_score/private/sphinx_module.bzl index 06c3123c..345ac9ab 100644 --- a/bazel/rules/rules_score/private/sphinx_module.bzl +++ b/bazel/rules/rules_score/private/sphinx_module.bzl @@ -79,6 +79,12 @@ sphinx_rule_attrs = dict( executable = True, cfg = "exec", ), + "_fta_metamodel": attr.label( + default = Label("//plantuml:fta_metamodel"), + allow_files = True, + doc = "Directory containing fta_metamodel.puml, passed to PlantUML via " + + "-Dplantuml.include.path so FTA diagrams can resolve !include fta_metamodel.puml.", + ), }, **VERBOSITY_ATTR ) @@ -125,8 +131,10 @@ def _score_needs_impl(ctx): _graphviz_rloc = _gv_short[3:] if _gv_short.startswith("../") else ctx.workspace_name + "/" + _gv_short _pl_short = ctx.executable._plantuml.short_path _plantuml_rloc = _pl_short[3:] if _pl_short.startswith("../") else ctx.workspace_name + "/" + _pl_short + fta_metamodel_files = ctx.files._fta_metamodel + fta_metamodel_dir = fta_metamodel_files[0].dirname if fta_metamodel_files else "" ctx.actions.run( - inputs = needs_inputs, + inputs = needs_inputs + fta_metamodel_files, outputs = [needs_output], arguments = needs_args, env = { @@ -134,6 +142,7 @@ def _score_needs_impl(ctx): "PLANTUML_BIN_RLOC": _plantuml_rloc, "GRAPHVIZ_DOT": ctx.executable._graphviz.path, "GRAPHVIZ_DOT_RLOC": _graphviz_rloc, + "FTA_METAMODEL_DIR": fta_metamodel_dir, }, progress_message = "Generating needs.json for: %s" % ctx.label.name, executable = sphinx_toolchain.sphinx.files_to_run.executable, @@ -274,15 +283,18 @@ def _score_html_impl(ctx): _graphviz_rloc = _gv_short[3:] if _gv_short.startswith("../") else ctx.workspace_name + "/" + _gv_short _pl_short = ctx.executable._plantuml.short_path _plantuml_rloc = _pl_short[3:] if _pl_short.startswith("../") else ctx.workspace_name + "/" + _pl_short + fta_metamodel_files = ctx.files._fta_metamodel + fta_metamodel_dir = fta_metamodel_files[0].dirname if fta_metamodel_files else "" action_env = { "PLANTUML_BIN": ctx.executable._plantuml.path, "PLANTUML_BIN_RLOC": _plantuml_rloc, "GRAPHVIZ_DOT": ctx.executable._graphviz.path, "GRAPHVIZ_DOT_RLOC": _graphviz_rloc, + "FTA_METAMODEL_DIR": fta_metamodel_dir, } ctx.actions.run( - inputs = html_inputs, + inputs = html_inputs + fta_metamodel_files, outputs = [sphinx_html_output], arguments = html_args + [args], env = action_env, diff --git a/bazel/rules/rules_score/src/sphinx_wrapper.py b/bazel/rules/rules_score/src/sphinx_wrapper.py index 760c7c39..5cefacb6 100644 --- a/bazel/rules/rules_score/src/sphinx_wrapper.py +++ b/bazel/rules/rules_score/src/sphinx_wrapper.py @@ -289,6 +289,16 @@ def main() -> int: level=_LEVEL_MAP[args.log_level], format="%(levelname)s: %(message)s" ) validate_arguments(args) + # Resolve execroot-relative tool paths to absolute paths NOW, while cwd + # is still the execroot (Bazel guarantees cwd = execroot at action start). + # Sphinx changes its working directory to the source/staging directory + # before evaluating conf.py, so os.path.abspath() inside conf.py would + # resolve against the wrong base. Converting here is safe and means + # conf.py receives already-absolute values via the environment. + for _tool_var in ("GRAPHVIZ_DOT", "PLANTUML_BIN", "FTA_METAMODEL_DIR"): + _tool_path = os.environ.get(_tool_var) + if _tool_path and not os.path.isabs(_tool_path): + os.environ[_tool_var] = os.path.abspath(_tool_path) # Create processor instance stdout_processor = StdoutProcessor() stderr_processor = StderrProcessor() diff --git a/bazel/rules/rules_score/templates/conf.template.py b/bazel/rules/rules_score/templates/conf.template.py index 41259225..998e5509 100644 --- a/bazel/rules/rules_score/templates/conf.template.py +++ b/bazel/rules/rules_score/templates/conf.template.py @@ -21,7 +21,6 @@ import json import os import sys -from pathlib import Path from sphinx.util import logging @@ -164,59 +163,65 @@ # Graphviz (sphinx.ext.graphviz) # --------------------------------------------------------------------------- # GRAPHVIZ_DOT — execroot-relative path of //third_party/docs_runtime:dot -# (the exec_in_sysroot bash wrapper), injected by the +# (the exec_in_sysroot POSIX-sh wrapper), injected by the # sphinx_module Bazel rule. # GRAPHVIZ_DOT_RLOC — analysis-time-stable rlocation key; logged only. # # Path resolution: same os.path.abspath() rationale as PLANTUML_BIN above. # -# The exec_in_sysroot wrapper is a runfiles-aware bash script that bootstraps -# its own runfiles via the standard $0.runfiles/ Bazel convention (identical -# to the runfiles.bash init block). Passing the ABSOLUTE path ensures $0 is -# absolute, so $0.runfiles/ resolves to the correct companion directory even -# when the wrapper is called as a subprocess from inside the Sphinx Python -# process (which carries its own RUNFILES_DIR pointing at the sphinx tool's -# runfiles, not the dot wrapper's runfiles). +# The exec_in_sysroot wrapper is a runfiles-aware POSIX-sh script that +# bootstraps its own runfiles via the standard $0.runfiles/ Bazel convention. +# Passing the ABSOLUTE path ensures $0 is absolute, so $0.runfiles/ resolves to +# the correct companion directory even when the wrapper is called as a +# subprocess from inside the Sphinx Python process (which carries its own +# RUNFILES_DIR pointing at the sphinx tool's runfiles, not the dot wrapper's +# runfiles). # -# If GRAPHVIZ_DOT is absent a known-invalid sentinel is used so that -# sphinx.ext.graphviz fails loudly on any .. graphviz:: directive rather than -# silently using a host-installed dot binary. -_graphviz_dot_path = os.environ.get("GRAPHVIZ_DOT", "") -_graphviz_dot_rloc = os.environ.get("GRAPHVIZ_DOT_RLOC", "") -if _graphviz_dot_path: - graphviz_dot = os.path.abspath(_graphviz_dot_path) - graphviz_output_format = "svg" - logger.debug( - f"graphviz dot resolved: {graphviz_dot} (rloc: {_graphviz_dot_rloc or 'n/a'})" +# GRAPHVIZ_DOT is mandatory: the sphinx_module rule always provides the hermetic +# wrapper, so if it is missing conf.py fails loudly rather than silently using a +# host-installed dot binary. +_graphviz_dot_path = os.environ.get("GRAPHVIZ_DOT") +if not _graphviz_dot_path: + raise ValueError( + "GRAPHVIZ_DOT environment variable is not set. It must point at the " + "//third_party/docs_runtime:dot hermetic wrapper and is normally provided " + "by the sphinx_module Bazel rule. If you are invoking Sphinx outside that " + "rule, set GRAPHVIZ_DOT to the hermetic dot wrapper path." ) -else: - graphviz_dot = "/__hermetic_graphviz_not_configured__/dot" +_graphviz_dot_rloc = os.environ.get("GRAPHVIZ_DOT_RLOC", "") +graphviz_dot = os.path.abspath(_graphviz_dot_path) +graphviz_output_format = "svg" +logger.debug( + f"graphviz dot resolved: {graphviz_dot} (rloc: {_graphviz_dot_rloc or 'n/a'})" +) # --------------------------------------------------------------------------- -# PlantUML layout engine: hermetic dot or Smetana fallback +# PlantUML layout engine: hermetic dot + FTA metamodel include path # --------------------------------------------------------------------------- -# Wire PlantUML to the hermetic dot via -graphvizdot when the wrapper is -# accessible; otherwise fall back to PlantUML's built-in Smetana layout -# engine (a pure-Java re-implementation of Graphviz) with a warning. -# The hermetic dot is the reference rendering path; Smetana output may differ -# visually (different edge routing, node spacing). -# Note: the smetana fallback only affects PlantUML diagrams. sphinx.ext.graphviz -# directives always use graphviz_dot; they will fail if it is the sentinel. -_dot_available = ( - graphviz_dot != "/__hermetic_graphviz_not_configured__/dot" - and Path(graphviz_dot).is_file() -) -if _dot_available: - plantuml = f"{plantuml_path} -graphvizdot {graphviz_dot}" +# FTA_METAMODEL_DIR — directory containing fta_metamodel.puml, set by the +# sphinx_module rule from //plantuml:fta_metamodel. +# FTA diagrams keep ``!include fta_metamodel.puml``; +# sphinxcontrib-plantuml renders via -pipe (no source-file +# dir on the include search path) so the file must be +# listed on plantuml.include.path. The JVM flag must +# precede the program args or the java_binary launcher +# passes it to PlantUML instead of the JVM. +_fta_metamodel_dir = os.environ.get("FTA_METAMODEL_DIR", "") +if _fta_metamodel_dir: + _fta_metamodel_dir = os.path.abspath(_fta_metamodel_dir) + logger.debug(f"fta_metamodel include path: {_fta_metamodel_dir}") + _include_flag = f" --jvm_flag=-Dplantuml.include.path={_fta_metamodel_dir}" else: logger.warning( - f"Hermetic Graphviz dot is not available at {graphviz_dot!r}. " - "PlantUML is falling back to the built-in Smetana layout engine. " - "Diagrams may differ from the reference output produced by the " - "hermetic Bazel build. Ensure GRAPHVIZ_DOT / GRAPHVIZ_DOT_RLOC are " - "set correctly when invoking the sphinx_module rule." + "FTA_METAMODEL_DIR is not set; FTA diagrams using " + "!include fta_metamodel.puml will fail to render." ) - plantuml = f"{plantuml_path} -Playout=smetana" + _include_flag = "" + +# PlantUML uses the same hermetic Graphviz dot as sphinx.ext.graphviz for its +# internal layout calls, via the -graphvizdot flag. There is no fallback: the +# hermetic dot is the single reference rendering path for both. +plantuml = f"{plantuml_path}{_include_flag} -graphvizdot {graphviz_dot}" # HTML theme html_theme = "sphinx_rtd_theme" diff --git a/third_party/docs_runtime/BUILD b/third_party/docs_runtime/BUILD index 3a747b52..4b936137 100644 --- a/third_party/docs_runtime/BUILD +++ b/third_party/docs_runtime/BUILD @@ -14,7 +14,6 @@ # Hermetic doc-tool runtime rootfs. The @docs_runtime repo (rules_distroless # apt.install) is defined in //MODULE.bazel; its manifest + lock live here. -load("@rules_shell//shell:sh_binary.bzl", "sh_binary") load("//bazel/rules/exec_in_sysroot:exec_in_sysroot.bzl", "exec_in_sysroot", "prepare_sysroot") package(default_visibility = ["//visibility:public"]) @@ -24,12 +23,6 @@ exports_files([ "docs_runtime.lock.json", ]) -# Host-side launcher that resolves `/usr/bin/dot` inside the fakechroot sysroot. -sh_binary( - name = "dot_in_sysroot", - srcs = ["dot.sh"], -) - # Rework the distroless rootfs once into a cached `dot`-ready sysroot archive. # The apt graphviz package ships /usr/bin/dot as a symlink to # libgvc6-config-update; its postinst (`dot -c`) generates config6a so dot can @@ -39,7 +32,6 @@ sh_binary( # re-run when only the wrapped executable / exclude_paths change). prepare_sysroot( name = "dot_sysroot", - # fakechroot + the x86_64 sysroot binaries only run on a linux/x86_64 host. exec_compatible_with = [ "@platforms//cpu:x86_64", "@platforms//os:linux", @@ -71,11 +63,10 @@ exec_in_sysroot( # redirected into the sysroot. Graphviz-internal paths (/usr/lib/…/graphviz, # /usr/share/fonts, …) are still correctly redirected by fakechroot. exclude_paths = ["/tmp"], - # fakechroot + the x86_64 sysroot binaries only run on a linux/x86_64 host. exec_compatible_with = [ "@platforms//cpu:x86_64", "@platforms//os:linux", ], - executable = ":dot_in_sysroot", sysroot = ":dot_sysroot", + sysroot_binary = "/usr/bin/dot", ) diff --git a/third_party/docs_runtime/README.md b/third_party/docs_runtime/README.md index 6db55cdf..2949fa6c 100644 --- a/third_party/docs_runtime/README.md +++ b/third_party/docs_runtime/README.md @@ -26,8 +26,8 @@ and PlantUML `-graphvizdot`). This package makes that use hermetic: generate the plugin manifest, and repackages the result as a single cached archive. 3. `//third_party/docs_runtime:dot` (an `exec_in_sysroot` rule) extracts the - prepared archive at build time and wraps `dot.sh` so that `/usr/bin/dot` - inside the sysroot is invoked via `LD_PRELOAD=libfakechroot.so`. + prepared archive at build time and wraps `exec_in_sysroot.sh` so that + `/usr/bin/dot` inside the sysroot is invoked via `LD_PRELOAD=libfakechroot.so`. Rules that need graphviz use `//third_party/docs_runtime:dot` as the executable and receive its path via the `GRAPHVIZ_DOT` environment variable. @@ -37,18 +37,17 @@ and receive its path via the `GRAPHVIZ_DOT` environment variable. This setup is reproducible but not *fully* hermetic — two host dependencies remain: -1. **Host glibc / ld.so ABI compatibility.** `exec_in_sysroot` runs `dot` via - `LD_PRELOAD=libfakechroot.so` rather than a real chroot, so the kernel still - launches the sysroot's `dot` with the *host* ELF interpreter (`ld.so`), which - then loads the sysroot's glibc. The sysroot is pinned to Ubuntu 24.04 (glibc - 2.39); on a build host whose glibc is older than the sysroot's, `dot` can - fail to start with `GLIBC_2.xx not found`. Build hosts therefore need a glibc - at least as new as the pinned sysroot. +1. **Host kernel ABI.** `exec_in_sysroot` runs `dot` through the sysroot's own + ELF interpreter (`ld-linux.so`) with `LD_PRELOAD=libfakechroot.so` rather + than a real chroot. All shared libraries — including `libc.so.6` — are loaded + from the sysroot, so the *host* glibc version does not matter. The one host + dependency that remains is the Linux kernel: the sysroot's glibc (Ubuntu + 24.04, glibc 2.39) requires a kernel at least as new as its minimum supported + version, so build hosts need a sufficiently recent kernel. 2. **Host shell tools.** The sysroot-rework and extraction actions run under a - POSIX `sh` and use standard coreutils (`find`, `mktemp`, `chmod`, `rm`), - assumed present in the build environment. The generated `dot` launcher itself - requires `bash`, because it sources Bazel's `runfiles.bash` library (there is - no POSIX-`sh` runfiles equivalent in `@bazel_tools`). + POSIX `sh` and use standard coreutils (`mkdir`, `find`, `rm`) plus `tar`, + assumed present in the build environment. The generated `dot` launcher and + the smoke test are plain POSIX `sh` scripts with an inline runfiles lookup ## Updating packages diff --git a/third_party/docs_runtime/dot.sh b/third_party/docs_runtime/dot.sh deleted file mode 100755 index dc695147..00000000 --- a/third_party/docs_runtime/dot.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/sh -# ******************************************************************************* -# Copyright (c) 2026 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* - -set -eu - -# Use the sysroot's own ELF interpreter (exported by exec_in_sysroot.sh as -# SYSROOT_INTERP) with an explicit --library-path so that dot and all of its -# shared-library dependencies (libgvc.so.6, libcgraph.so.6, …) are loaded -# entirely from the sysroot. -# -# Running dot through the HOST's ld-linux.so and a wide LD_LIBRARY_PATH would -# cause the sysroot's libc.so.6 to load alongside the host's already-resident -# libc (two libc instances → segfault on systems without a host graphviz). -# The sysroot's own interpreter gives a single coherent libc. -# -# LD_PRELOAD=libfakechroot.so + FAKECHROOT_BASE are still active (set by -# exec_in_sysroot.sh) so glibc-level filesystem calls inside dot (e.g. -# opening the graphviz plugin directory, reading config6) are transparently -# redirected into the sysroot. -exec "${SYSROOT_INTERP}" --library-path "${SYSROOT_LIBPATH}" "${SYSROOT_DIR}/usr/bin/dot" "$@" diff --git a/third_party/docs_runtime/tests/dot_smoke_test.sh b/third_party/docs_runtime/tests/dot_smoke_test.sh index bdd60d7c..a37785db 100755 --- a/third_party/docs_runtime/tests/dot_smoke_test.sh +++ b/third_party/docs_runtime/tests/dot_smoke_test.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/sh # ******************************************************************************* # Copyright (c) 2026 Contributors to the Eclipse Foundation # @@ -17,30 +17,32 @@ # inside the docs_runtime sysroot. # If this wrapper/sysroot pairing breaks, doc builds lose graphviz rendering. -set -euo pipefail +set -eu -# --- begin runfiles.bash initialization --- -if [[ ! -d "${RUNFILES_DIR:-/dev/null}" && ! -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then - if [[ -f "$0.runfiles_manifest" ]]; then - export RUNFILES_MANIFEST_FILE="$0.runfiles_manifest" - elif [[ -f "$0.runfiles/MANIFEST" ]]; then - export RUNFILES_MANIFEST_FILE="$0.runfiles/MANIFEST" - elif [[ -f "$0.runfiles/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then - export RUNFILES_DIR="$0.runfiles" +# --- begin POSIX sh runfiles init --- +if [ ! -d "${RUNFILES_DIR:-/dev/null}" ] && [ ! -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]; then + if [ -f "$0.runfiles_manifest" ]; then + RUNFILES_MANIFEST_FILE="$0.runfiles_manifest"; export RUNFILES_MANIFEST_FILE + elif [ -f "$0.runfiles/MANIFEST" ]; then + RUNFILES_MANIFEST_FILE="$0.runfiles/MANIFEST"; export RUNFILES_MANIFEST_FILE + elif [ -d "$0.runfiles" ]; then + RUNFILES_DIR="$0.runfiles"; export RUNFILES_DIR + else + echo >&2 "ERROR: cannot find runfiles" + exit 1 fi fi -if [[ -f "${RUNFILES_DIR:-/dev/null}/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then - source "${RUNFILES_DIR}/bazel_tools/tools/bash/runfiles/runfiles.bash" -elif [[ -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then - source "$(grep -m1 '^bazel_tools/tools/bash/runfiles/runfiles.bash ' "$RUNFILES_MANIFEST_FILE" | cut -d ' ' -f 2-)" -else - echo >&2 "ERROR: cannot find @bazel_tools//tools/bash/runfiles:runfiles.bash" - exit 1 -fi -# --- end runfiles.bash initialization --- +rlocation() { + if [ -n "${RUNFILES_DIR:-}" ] && [ -e "${RUNFILES_DIR}/$1" ]; then + printf '%s' "${RUNFILES_DIR}/$1" + elif [ -n "${RUNFILES_MANIFEST_FILE:-}" ]; then + grep -m1 "^$1 " "${RUNFILES_MANIFEST_FILE}" | cut -d' ' -f2- + fi +} +# --- end POSIX sh runfiles init --- dot_wrapper="$(rlocation "${TEST_WORKSPACE}/third_party/docs_runtime/dot")" -if [[ -z "${dot_wrapper}" || ! -x "${dot_wrapper}" ]]; then +if [ -z "${dot_wrapper}" ] || [ ! -x "${dot_wrapper}" ]; then echo "ERROR: could not resolve //third_party/docs_runtime:dot from runfiles" >&2 exit 1 fi @@ -57,11 +59,15 @@ echo "=== dot -V ===" echo "=== rendering digraph to SVG ===" svg="$(printf 'digraph G { A -> B }' | "${dot_wrapper}" -Tsvg 2>&1)" -if [[ "${svg}" != *"&2 echo "--- output ---" >&2 echo "${svg}" >&2 exit 1 -fi + ;; +esac echo "ok: hermetic dot works"