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
14 changes: 14 additions & 0 deletions bazel/aspects/rules_score/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# *******************************************************************************
# 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
# *******************************************************************************

exports_files(["dependable_element_export.bzl"])
75 changes: 75 additions & 0 deletions bazel/aspects/rules_score/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<!-- ----------------------------------------------------------------------------
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
----------------------------------------------------------------------------- -->

# `dependable_element_export` aspect

Collects the doc/report artifacts of every `dependable_element()` (rules_score SEooC) reached by
a build and republishes them under a uniform `_dependable_element_export/` path segment, as a
dedicated, explicitly requestable output group.

It is designed to be added to an **existing** build invocation without changing what that build
produces for any other target: the aspect no-ops for every target that does not carry the
`DependableElementInfo` provider, and its outputs are only built when you ask for the
`dependable_element_export` output group.

## Usage

```sh
bazel build //<your_target(s)> \
--aspects=@score_tooling//bazel/aspects/rules_score:dependable_element_export.bzl%dependable_element_export_aspect \
--output_groups=+dependable_element_export \
--remote_download_regex=".*/_dependable_element_export/.*"
```

The three flags are independent and all required:

| Flag | Why |
| --- | --- |
| `--aspects=` | Attaches the aspect. Note the value is `<label of the .bzl file>%<aspect symbol>` — **not** a target label. |
| `--output_groups=+dependable_element_export` | The leading `+` *adds* to the default output groups instead of replacing them, so the original build still produces everything it normally would. |
| `--remote_download_regex=` | Only needed with remote execution (`--config=rbe`). Without it the exported artifacts stay on the remote worker and never materialize locally. |

Safe to apply to a wide pattern such as `//...` — packages without a `dependable_element()` cost
nothing.

## Where the output lands

```text
bazel-bin/<package-path>/_dependable_element_export/
```

To find every exported element across a whole workspace after the build:

```sh
find -L bazel-bin -type d -name _dependable_element_export
```

## Why the artifacts are copied rather than just re-exposed

A naive aspect would simply re-advertise each `dependable_element`'s existing default outputs.
That is not enough for the CI use case, because those files live at unpredictable,
rule-internal paths that differ per element, which makes them hard to glob, hard to filter with
`--remote_download_regex`, and hard to upload under a stable directory structure.

So the aspect declares new outputs under `_EXPORT_DIR` and populates them with explicit copy
actions. Two details matter:

- **Copies, not symlinks.** `ctx.actions.symlink()` was tried first and broke under remote
execution — the symlinks were not materialized usefully on the local side.
- **Tree artifacts are handled separately.** Some `dependable_element` outputs are directories
(e.g. generated Sphinx HTML), so the implementation branches on `File.is_directory` and uses
`declare_directory()` + a recursive copy for those.

The package prefix is stripped from each file's `short_path` before relocation, so paths are not
redundantly nested inside themselves.

102 changes: 102 additions & 0 deletions bazel/aspects/rules_score/dependable_element_export.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# *******************************************************************************
# 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
# *******************************************************************************

"""CLI-level aspect exporting dependable_element (rules_score SEooC) artifacts.

The aspect exposes every dependable_element (rules_score SEooC)'s own doc/report
artifacts as a dedicated output group. It no-ops for any target that isn't a
dependable_element, so it is safe to apply unconditionally on //...
Every `dependable_element`'s own doc/report artifacts become an explicitly requestable output group of that *same* build invocation - without changing what is built for any other target.

Rather than merely re-exposing each dependable_element's existing output files as-is, this
aspect *relocates/copies* them under a synthetic `_dependable_element_export/` path segment to simplify downloading it from remote-execution or uploading it in CI.

Usage:

bazel build //<your_target(s)> \
--aspects=@score_tooling//bazel/aspects/rules_score:dependable_element_export.bzl%dependable_element_export_aspect \
--output_groups=+dependable_element_export \
--remote_download_regex=".*/_dependable_element_export/.*"

See README.md next to this file for details.
"""

load("@score_tooling//bazel/rules/rules_score:providers.bzl", "DependableElementInfo")

_EXPORT_DIR = "_dependable_element_export"

def _dependable_element_export_aspect_impl(target, ctx):
"""Re-export a dependable_element's own default outputs under a uniform path segment.

No-ops for any target that isn't a dependable_element (identified via the
`DependableElementInfo` provider that `dependable_element()` forwards for exactly this kind
of cross-target lookup - see rules_score's own use of it for integrity-level checks).
"""
if DependableElementInfo not in target:
return []

# Own package prefix of the file's short_path, if present, is stripped so the relocated
# path doesn't redundantly nest the package path inside itself a second time.
own_package_prefix = ctx.label.package + "/"

exported_files = []
for f in target[DefaultInfo].files.to_list():
short_path = f.short_path
if short_path.startswith("../"):
# Drop external repo prefix.
relative_path = short_path.split("/", 2)[2]
elif short_path.startswith(own_package_prefix):
relative_path = short_path[len(own_package_prefix):]
else:
relative_path = short_path

# Some dependable_element outputs are tree artifacts (e.g. a Sphinx HTML directory),
# not plain files - handled with a directory copy instead of a single-file one.
if f.is_directory:
exported = ctx.actions.declare_directory(_EXPORT_DIR + "/" + relative_path)
ctx.actions.run_shell(
inputs = [f],
outputs = [exported],
command = "mkdir -p \"$2\" && cp -r \"$1\"/. \"$2\"/",
arguments = [f.path, exported.path],
mnemonic = "DependableElementExportCopyDir",
progress_message = "Exporting dependable_element directory " + f.short_path,
)
else:
exported = ctx.actions.declare_file(_EXPORT_DIR + "/" + relative_path)
ctx.actions.run_shell(
inputs = [f],
outputs = [exported],
command = "mkdir -p \"$(dirname \"$2\")\" && cp \"$1\" \"$2\"",
arguments = [f.path, exported.path],
mnemonic = "DependableElementExportCopyFile",
progress_message = "Exporting dependable_element file " + f.short_path,
)
exported_files.append(exported)

return [
OutputGroupInfo(
dependable_element_export = depset(exported_files),
),
]

dependable_element_export_aspect = aspect(
implementation = _dependable_element_export_aspect_impl,
doc = (
"Exports dependable_element (rules_score SEooC) default outputs (doc/report " +
"artifacts), relocated (via copy actions) under a uniform " +
"'_dependable_element_export/' path segment, as a dedicated " +
"'dependable_element_export' output group - for use as an additive `--aspects=` flag " +
"on an otherwise unrelated build."
),
)
Loading