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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ module(name = "score_docs_as_code")
#
###############################################################################
bazel_dep(name = "rules_python", version = "1.8.5")
bazel_dep(name = "sphinxdocs", version = "2.2.0")

PYTHON_VERSION = "3.12"

Expand Down
3 changes: 0 additions & 3 deletions MODULE.bazel.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

92 changes: 5 additions & 87 deletions bzl/bundle_rules.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,8 @@
# *******************************************************************************
"""Internal Bazel support for composing reusable documentation bundles."""

# `docs_bundle` and `sphinx_docs_library` operate at a similar architectural level:
# both describe reusable, transitively composable collections of documentation sources
# that are later assembled into a Sphinx source tree.

# However, their data models and responsibilities differ significantly.

# `sphinx_docs_library` primarily models file placement. Each library contributes files
# together with a `strip_prefix` and a `prefix`, allowing the final Sphinx rule to map
# every source file to a new location in the generated source tree.

# `docs_bundle` instead models documentation structure at the bundle level. In
# addition to the source files, it propagates information such as:
# `docs_bundle` models documentation structure at the bundle level. In addition to
# source files, it propagates information such as:

# * where a bundle is mounted, * which document it is attached to, * which document acts
# as its entry point, * which repository owns its sources, * whether it is an internal
Expand All @@ -34,30 +24,13 @@
# is therefore not just a set of files with path transformations, but a structured
# documentation component with composition semantics.

# Using `sphinx_docs_library` directly would not preserve the metadata required by this
# model. We would need a second provider alongside it and would still have to implement
# most of the bundle traversal, rebasing, validation, and composition logic ourselves.

# Extending `sphinx_docs_library` is also not a good fit. Its provider represents
# individual file mappings, while our provider represents complete mounted bundles. Adding
# the required metadata would therefore not be a small extension of the existing
# abstraction; it would change its propagated unit and its semantics. It would also couple
# SCORE-specific composition rules to the generic `rules_sphinxdocs` implementation.

# We therefore reimplement the relatively small overlapping part—transitive source
# collection—while keeping the richer bundle model explicit and independent.

# The name `docs_bundle` reflects that relationship: it fills the same general role
# as `sphinx_docs_library`, but uses a SCORE-specific data model for composing structured
# documentation bundles.
# The provider is consumed directly by the repository-owned Needs action, while
# its entries are consumed by the mounts manifest action. Keeping both consumers
# on this provider ensures that source ownership and runtime placement agree.



load("@score_docs_as_code//:bzl/basics.bzl", "join_path")
load(
"@sphinxdocs//sphinxdocs/private:sphinx_docs_library_info.bzl",
"SphinxDocsLibraryInfo",
)

# Internal data passed between bundle targets and eventually consumed by an
# adapter such as the Sphinx mounts manifest. Users configure bundles through
Expand Down Expand Up @@ -513,61 +486,6 @@ def bundle_source_files(name, bundle, visibility = None, tags = None):
)
return ":" + name

def _bundle_sphinx_source_files_impl(ctx):
"""Expose direct bundle sources with a Sphinx-specific path mapping."""
bundle = ctx.attr.bundle[DocsBundleInfo]
source_files = tuple(bundle.own_source_files.to_list())
if not source_files:
fail("bundle %s has no direct documentation sources" % ctx.attr.bundle)

# Directory-discovered sources already carry a stable bundle-relative root
# in the provider. Explicit source targets instead use the output path
# Bazel gives to Sphinx. Deriving that parent from ``short_path`` handles
# both workspace files and generated outputs (whose paths include
# ``bazel-out``) without making the macro guess a configuration-dependent
# output directory.
if bundle.own_source_is_explicit:
source_path = source_files[0].short_path
separator = source_path.rfind("/")
strip_prefix = source_path[:separator + 1] if separator >= 0 else ""
else:
strip_prefix = bundle.own_source_root
if strip_prefix and not strip_prefix.endswith("/"):
strip_prefix += "/"

entry = struct(
strip_prefix = strip_prefix,
prefix = "",
files = source_files,
)
return [
DefaultInfo(files = depset(source_files)),
SphinxDocsLibraryInfo(
strip_prefix = strip_prefix,
prefix = "",
files = source_files,
transitive = depset(direct = [entry]),
),
]

_bundle_sphinx_source_files = rule(
implementation = _bundle_sphinx_source_files_impl,
attrs = {
"bundle": attr.label(providers = [DocsBundleInfo]),
},
doc = "Exposes direct bundle sources with paths rooted for a Sphinx build.",
)

def bundle_sphinx_source_files(name, bundle, visibility = None, tags = None):
"""Create a Sphinx library containing only a bundle's direct sources."""
_bundle_sphinx_source_files(
name = name,
bundle = bundle,
visibility = visibility,
tags = tags,
)
return ":" + name

def _external_docs_runfiles_impl(ctx):
"""Expose external documentation sources needed under ``bazel run``."""
bundle = ctx.attr.bundle[DocsBundleInfo]
Expand Down
85 changes: 85 additions & 0 deletions bzl/needs_rules.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# *******************************************************************************
# 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 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************
"""Private Bazel action for building the Needs output in place.

The SCORE mount extension exposes bundle inputs directly from the Bazel
execution root. This action therefore provides Sphinx with the project's
primary source directory and the mount manifest. Bundle targets remain
explicit action inputs; a manifest by itself does not make the files named by
that manifest available inside a sandbox.
"""

load("@score_docs_as_code//:bzl/bundle_rules.bzl", "DocsBundleInfo")

def _sphinx_docs_impl(ctx):
"""Run Sphinx against the Bazel execution-root source tree."""
output = ctx.actions.declare_directory(ctx.label.name + "/_build/needs")

bundle = ctx.attr.bundle[DocsBundleInfo]
# The bundle owns both the direct inputs and their root. Nested sources
# are provided separately for score_mounts, so local exports retain their
# bundle ownership. Generated roots already use execution-root paths;
# external source roots use runfiles spelling and need this translation.
if not bundle.own_source_files.to_list():
fail("Sphinx requires a bundle with direct documentation sources")
source_dir = bundle.own_source_root
if source_dir.startswith("../"):
source_dir = "external/" + source_dir[3:]

# Expand file labels at analysis time, then encode the argument list as
# JSON so spaces, quotes and '=' in Sphinx options survive the environment
# transport unchanged. The launcher adds these after its default options.
# ``config`` is transported separately because the launcher derives
# Sphinx's ``-c`` directory from its path; it is not just another data file.
env = {
"ACTION": "build_needs_json",
"SOURCE_DIRECTORY": source_dir or ".",
"OUTPUT_DIRECTORY": output.path,
"SPHINX_CONFIG_FILE": ctx.file.config.path,
"DATA": "[]",
"SPHINX_EXTRA_OPTS": json.encode([
ctx.expand_location(option, targets = ctx.attr.tools)
for option in ctx.attr.extra_opts
]),
}

# Data and mounted sources must be present at their execution-root paths.
# The executable separately carries these labels in its Python runfiles
# for extensions that locate external inventories through Bazel labels.
ctx.actions.run(
executable = ctx.executable.sphinx,
env = env,
inputs = depset(
[ctx.file.config] + ctx.files.data + ctx.files.tools,
transitive = [bundle.own_source_files],
),
outputs = [output],
mnemonic = "ScoreNeedsBuild",
progress_message = "Building Needs inventory for %s" % ctx.label,
)

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

sphinx_docs = rule(
implementation = _sphinx_docs_impl,
attrs = {
"config": attr.label(allow_single_file = True, mandatory = True),
"bundle": attr.label(providers = [DocsBundleInfo], mandatory = True),
"data": attr.label_list(allow_files = True),
"tools": attr.label_list(allow_files = True),
"extra_opts": attr.string_list(),
# The launcher runs on the build host and carries extension runfiles.
"sphinx": attr.label(cfg = "exec", executable = True, mandatory = True),
},
doc = "Private action that builds Needs from declared execution-root inputs.",
)
Loading
Loading