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
5 changes: 5 additions & 0 deletions BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ docs(
"bundle": "//src/extensions/score_mounts/docs:internals",
"mount_at": "internals/extensions/mounts",
},
{
"bundle": "//src/extensions/score_metamodel/docs:metamodel",
"mount_at": "reference/metamodel",
"attach_to": "reference/index",
},
],
scan_code = [
"//scripts_bazel:sources",
Expand Down
38 changes: 34 additions & 4 deletions bzl/bundle_rules.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ DocsBundleInfo = provider(
"entries": "Ordered entries, one per source directory, including its final documentation-tree location.",
"sourcelinks": "Source-code-link JSON files together with their owning repository.",
"external_runfiles": "Documentation source files from external repositories needed in runfiles.",
"data": "Non-source-tree files (e.g. genrule outputs) needed for Sphinx resolution.",
Comment thread
a-zw marked this conversation as resolved.
},
)

Expand Down Expand Up @@ -144,7 +145,7 @@ def _bundle_execroot_path(runtime_path):
return "external/" + runtime_path[3:]
return runtime_path

def _rebase_bundle_entry(entry, mount_at, attach_to):
def _rebase_bundle_entry(entry, mount_at, attach_to, data):
"""Place a bundle entry below a requested documentation-tree location.

A bundle's own root has no ``mount_at`` yet. For that root, an omitted
Expand All @@ -165,6 +166,7 @@ def _rebase_bundle_entry(entry, mount_at, attach_to):
entry_doc = entry.entry_doc,
external = entry.external,
repository = entry.repository,
data = data,
)

def _entries_visible_through(ctx, child):
Expand Down Expand Up @@ -210,6 +212,7 @@ def _docs_bundle_impl(ctx):
entries = []
own_source_files = []
own_external_runfiles = []
own_data = depset(direct = ctx.files.data)

if ctx.files.srcs:
runtime_path = _bundle_runtime_path(ctx)
Expand All @@ -225,12 +228,25 @@ def _docs_bundle_impl(ctx):
entry_doc = ctx.attr.entry_doc,
external = external,
repository = ctx.label.workspace_name,
data = own_data,
))
own_source_files.extend(ctx.files.srcs)
# Local sources are read directly from the workspace by ``bazel run``.
# Only sources from external repositories must be staged in runfiles.
if external:
own_external_runfiles.extend(ctx.files.srcs)
elif own_data:
# Pure data bundle: create an entry so the data files appear in the manifest.
entries.append(struct(
runtime_path = "",
src_root = "",
mount_at = "",
attach_to = "",
entry_doc = ctx.attr.entry_doc,
external = False,
repository = ctx.label.workspace_name,
data = own_data,
))

child_source_files = []
child_external_runfiles = []
Expand All @@ -239,11 +255,13 @@ def _docs_bundle_impl(ctx):
for source_link in ctx.files.sourcelinks
]
for index, child in enumerate(ctx.attr.bundles):
child_data = child[DocsBundleInfo].data
entries.extend([
_rebase_bundle_entry(
entry,
ctx.attr.bundle_mount_ats[index],
ctx.attr.bundle_attach_tos[index],
child_data,
)
for entry in _entries_visible_through(ctx, child)
])
Expand All @@ -260,12 +278,19 @@ def _docs_bundle_impl(ctx):
direct = own_external_runfiles,
transitive = child_external_runfiles,
)
all_data = depset(
transitive = [own_data] + [
child[DocsBundleInfo].data
for child in ctx.attr.bundles
],
)
return [
DefaultInfo(files = all_source_files),
DefaultInfo(files = depset(transitive = [all_source_files, all_data])),
DocsBundleInfo(
entries = entries,
sourcelinks = sourcelinks,
external_runfiles = external_runfiles,
data = all_data,
),
]

Expand All @@ -279,11 +304,12 @@ _docs_bundle = rule(
"bundles": attr.label_list(providers = [DocsBundleInfo]),
"bundle_mount_ats": attr.string_list(),
"bundle_attach_tos": attr.string_list(),
"data": attr.label_list(allow_files = True),
},
doc = "Internal rule that carries bundle files and their documentation-tree locations.",
)

def create_bundle(name, bundles, srcs = [], sourcelinks = [], strip_prefix = "", entry_doc = "index", visibility = None, **kwargs):
def create_bundle(name, bundles, srcs = [], sourcelinks = [], strip_prefix = "", entry_doc = "index", data = [], visibility = None, **kwargs):
"""Create a reusable documentation bundle from files and child declarations."""
parsed_bundles = [_parse_bundle_declaration(declaration) for declaration in bundles]
_docs_bundle(
Expand All @@ -295,14 +321,18 @@ def create_bundle(name, bundles, srcs = [], sourcelinks = [], strip_prefix = "",
bundles = [bundle.bundle for bundle in parsed_bundles],
bundle_mount_ats = [bundle.mount_at for bundle in parsed_bundles],
bundle_attach_tos = [bundle.attach_to for bundle in parsed_bundles],
data = data,
visibility = visibility,
**kwargs
)
return ":" + name

def _external_docs_runfiles_impl(ctx):
"""Expose external documentation sources needed under ``bazel run``."""
return [DefaultInfo(files = ctx.attr.bundle[DocsBundleInfo].external_runfiles)]
bundle = ctx.attr.bundle[DocsBundleInfo]
return [DefaultInfo(files = depset(
transitive = [bundle.external_runfiles, bundle.data],
))]

_external_docs_runfiles = rule(
implementation = _external_docs_runfiles_impl,
Expand Down
4 changes: 3 additions & 1 deletion bzl/mount_rules.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ load("@score_docs_as_code//:bzl/bundle_rules.bzl", "DocsBundleInfo")

def _mounts_manifest_impl(ctx):
"""Generate the canonical Sphinx mount manifest."""
entries = ctx.attr.bundle[DocsBundleInfo].entries
bundle_info = ctx.attr.bundle[DocsBundleInfo]
entries = bundle_info.entries

json_mounts = []
for entry in entries:
Expand All @@ -29,6 +30,7 @@ def _mounts_manifest_impl(ctx):
"attach_to": entry.attach_to,
"entry_doc": entry.entry_doc,
"external": entry.external,
"data": [f.path for f in entry.data.to_list()],
})
Comment thread
a-zw marked this conversation as resolved.

out = ctx.actions.declare_file(ctx.label.name + ".json")
Expand Down
6 changes: 5 additions & 1 deletion docs.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,17 @@ load(
"create_mounts_manifest",
)

def docs_bundle(name, source_dir = None, entry_doc = "index", bundles = [], scan_code = [], code_targets = [], visibility = None, **kwargs):
def docs_bundle(name, source_dir = None, data = [], entry_doc = "index", bundles = [], scan_code = [], code_targets = [], visibility = None, **kwargs):
"""A docs bundle, optionally composed of others.

Args:
name: target name.
source_dir: optional directory holding this bundle's own doc sources. It is
globbed like `docs()` (same file kinds) and the contents are stored after
stripping the `source_dir` prefix. Leave it unset for a pure aggregator.
data:
Additional data dependencies for this target.
Useful for generated rst sources.
entry_doc: bundle-relative docname attached when this bundle is mounted.
Defaults to `index`.
bundles: nested bundles to compose, each a dict
Expand Down Expand Up @@ -113,6 +116,7 @@ def docs_bundle(name, source_dir = None, entry_doc = "index", bundles = [], scan
strip_prefix = strip_prefix,
entry_doc = entry_doc,
bundles = bundles,
data = data,
visibility = visibility,
**kwargs
)
Expand Down
42 changes: 42 additions & 0 deletions src/extensions/score_metamodel/docs/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# *******************************************************************************
# 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("//:docs.bzl", "docs_bundle")
load("@aspect_rules_py//py:defs.bzl", "py_binary")
load("@docs_as_code_hub_env//:requirements.bzl", "all_requirements")

genrule(
name = "generate_metamodel_rst",
srcs = [
"//src/extensions/score_metamodel:metamodel_yaml",
],
outs = ["generated/index.rst", "generated/metamodel_classes.mmd"],
cmd = "$(location :generate_metamodel_rst_bin) --rst-output $(location generated/index.rst) --mmd-output $(location generated/metamodel_classes.mmd) $(location //src/extensions/score_metamodel:metamodel_yaml)",
tools = [":generate_metamodel_rst_bin"],
Comment thread
a-zw marked this conversation as resolved.
visibility = ["//visibility:private"],
)

py_binary(
name = "generate_metamodel_rst_bin",
srcs = ["generate_metamodel_rst.py"],
main = "generate_metamodel_rst.py",
deps = all_requirements,
visibility = ["//visibility:private"],
)

docs_bundle(
name = "metamodel",
data = [":generate_metamodel_rst"],
entry_doc = "index",
visibility = ["//visibility:public"],
)
140 changes: 140 additions & 0 deletions src/extensions/score_metamodel/docs/generate_metamodel_rst.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
#!/usr/bin/env python3
# *******************************************************************************
# 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
# *******************************************************************************
"""Generate an RST file with a list-table and Mermaid class diagram from metamodel.yaml.

Usage:
generate_metamodel_rst.py --rst-output FILE --mmd-output FILE [METAMODEL_YAML]
"""

from __future__ import annotations

import argparse
import sys
from pathlib import Path

import ruamel.yaml


def _parse_yaml(path: Path) -> dict:
yaml = ruamel.yaml.YAML()
yaml.preserve_quotes = True
with Path(path).open(encoding="utf-8") as fh:
return yaml.load(fh) # type: ignore[no-any-return]


def _build_table(types: dict) -> list[str]:
lines: list[str] = []
lines.append(".. list-table:: Need Types")
lines.append(" :header-rows: 1")
lines.append("")
lines.append(" * - Type")
lines.append(" - Title")
lines.append(" - Mandatory Options")
lines.append(" - Links")
for name, ty in sorted(types.items()):
title = ty.get("title", name)
mandatory = (
", ".join(sorted(ty.get("mandatory_options", {}).keys())) or "\u2014"
)
optional_links = ", ".join(sorted(ty.get("optional_links", {}).keys()))
mandatory_links = ", ".join(sorted(ty.get("mandatory_links", {}).keys()))
if mandatory_links:
links = f"{optional_links} | mandatory: {mandatory_links}"
else:
links = optional_links or "\u2014"
lines.append(" * - " + name)
lines.append(" - " + title)
lines.append(" - " + mandatory)
lines.append(" - " + links)
return lines


def _build_mermaid(types: dict) -> list[str]:
lines: list[str] = []
seen: set[tuple[str, str, str]] = set()
for name, ty in sorted(types.items()):
for link_name, targets in sorted(
list(ty.get("mandatory_links", {}).items())
+ list(ty.get("optional_links", {}).items())
):
target_types = targets.split(", ") if targets != "ANY" else []
for target in target_types:
target = target.strip()
if target and target in types:
key = (name, target, link_name)
if key not in seen:
seen.add(key)
lines.append(f"{name} --> {target} : {link_name}")
return lines


def main() -> int:
parser = argparse.ArgumentParser(description="Generate RST from metamodel.yaml")
parser.add_argument("--rst-output", type=Path, required=True)
parser.add_argument("--mmd-output", type=Path, required=True)
parser.add_argument("metamodel", nargs="?", default=None)
args = parser.parse_args()

meta_path = (
Path(args.metamodel)
if args.metamodel
else Path(__file__).parent / "metamodel.yaml"
)
if not meta_path.is_file():
print(f"Error: metamodel.yaml not found at {meta_path}", file=sys.stderr)
return 1

try:
data = _parse_yaml(meta_path)
except Exception as exc:
print(f"Error parsing YAML: {exc}", file=sys.stderr)
return 1

types = data.get("needs_types", {})
if not types:
print(
"Error: 'needs_types' section not found in metamodel.yaml", file=sys.stderr
)
return 1

table = _build_table(types)
mermaid_lines = ["classDiagram"] + _build_mermaid(types)
args.mmd_output.write_text("\n".join(mermaid_lines) + "\n", encoding="utf-8")
output = "\n".join(
[
"..",
" # (generated \u2014 do not edit)",
" # SPDX-License-Identifier: Apache-2.0",
"",
".. _metamodel-types-visualization:",
"",
"Metamodel Types Visualization",
"==============================",
"",
f".. mermaid:: {args.mmd_output.name}",
"",
"Need Types",
"----------",
"",
]
+ table
+ [""]
)

args.rst_output.write_text(output, encoding="utf-8")
return 0


if __name__ == "__main__":
sys.exit(main())
Loading
Loading