From 5a981c0afb37782c02f38997bd9f47e2aefa0e42 Mon Sep 17 00:00:00 2001 From: showxu <10173746+showxu@users.noreply.github.com> Date: Thu, 8 Oct 2026 03:05:36 +0800 Subject: [PATCH] fix: preserve selected documentation traits --- README.md | 6 ++-- Scripts/build-site | 20 ++++++------- Tests/test_pipeline.py | 66 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 3161feb..c2403e8 100644 --- a/README.md +++ b/README.md @@ -38,9 +38,9 @@ Source archives are downloaded at the tag's full commit SHA and removed after conversion. No repository checkout is copied into the build cache. A DocC catalog entry may select `traits` from its tagged package, including -`default` when it needs the default APIs. Both compilation and symbol extraction -use that selection. Omitted traits keep the package defaults. Symbol extraction -includes extensions to external types so those APIs remain linkable in DocC. +`default` when it needs the default APIs. The compiler emits public symbol graphs +during that selected build. Omitted traits keep the package defaults. Symbol +graphs include extensions to external types so those APIs remain linkable in DocC. Generated breadcrumbs omit external-type containers for which DocC emits no page, while keeping the package's actual extension members. The link checker distinguishes tutorial chapter labels from navigable tutorial pages; explicit diff --git a/Scripts/build-site b/Scripts/build-site index 220f3e1..fd6a0b8 100755 --- a/Scripts/build-site +++ b/Scripts/build-site @@ -103,16 +103,16 @@ def build_documentation(config, project, tag, cache, output, toolchain, jobs, co swift_options += documentation_traits(manifest, project["documentation"].get("traits")) if jobs is not None: swift_options += ["--jobs", str(jobs)] - # SwiftPM's symbol extraction includes generated test-runner modules. - command(["swift", "build", *swift_options, "--build-tests"], cwd=source) - command(["swift", "package", *swift_options, "dump-symbol-graph", - "--minimum-access-level", "public", "--emit-extension-block-symbols"], cwd=source) - graph_directories = [path for path in scratch.glob("*/symbolgraph") if path.is_dir()] - if not graph_directories: - graph_directories = [path for path in scratch.glob("*/*/symbolgraph") if path.is_dir()] - if len(graph_directories) != 1: - raise SiteError(f"{repo}: expected one symbol graph directory, got {len(graph_directories)}") - all_graphs = list(graph_directories[0].glob("*.symbols.json")) + # Compiler emission preserves the selected traits; SwiftPM's separate + # symbol-graph command enables every trait when it creates its build plan. + graph_directory = work / "symbol-graphs" + graph_directory.mkdir(parents=True, exist_ok=True) + command(["swift", "build", *swift_options, + "-Xswiftc", "-emit-symbol-graph", + "-Xswiftc", "-emit-symbol-graph-dir", "-Xswiftc", str(graph_directory), + "-Xswiftc", "-symbol-graph-minimum-access-level", "-Xswiftc", "public", + "-Xswiftc", "-emit-extension-block-symbols"], cwd=source) + all_graphs = list(graph_directory.glob("*.symbols.json")) module_graphs = {} for path in all_graphs: module = json.loads(path.read_text())["module"]["name"] diff --git a/Tests/test_pipeline.py b/Tests/test_pipeline.py index 36f56b9..8202cb1 100644 --- a/Tests/test_pipeline.py +++ b/Tests/test_pipeline.py @@ -3,11 +3,13 @@ import importlib.machinery import importlib.util +from contextlib import nullcontext import json from pathlib import Path import sys import tempfile import unittest +from unittest.mock import patch SCRIPTS = Path(__file__).resolve().parent.parent / "Scripts" sys.path.insert(0, str(SCRIPTS)) @@ -184,6 +186,70 @@ def test_documentation_targets_follow_the_tagged_public_products(self): with self.assertRaises(build_site.SiteError): build_site.library_targets(manifest, ["CLI"]) + def test_selected_build_supplies_public_and_extension_graphs(self): + fixtures = SCRIPTS.parent / ".build/test-fixtures" + fixtures.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(dir=fixtures) as directory: + root = Path(directory) + source = root / "source" + source.mkdir() + (source / "Package.swift").write_text("// fixture manifest\n") + manifest = { + "traits": [{"name": "Preview"}], + "products": [{"type": {"library": ["automatic"]}, "targets": ["Core"]}], + "targets": [{"name": "Core"}], + } + project = {"repository": "example", "documentation": { + "targets": "library-products", "traits": ["Preview", "default"], + }} + builds = [] + converted = [] + + def command(arguments, cwd=None, capture=False): + if arguments[:3] == ["swift", "package", "dump-package"]: + return json.dumps(manifest) + if arguments[:2] == ["swift", "build"]: + builds.append(arguments) + self.assertEqual(arguments[arguments.index("--traits") + 1], "Preview,default") + self.assertIn("-emit-symbol-graph", arguments) + self.assertIn("-emit-extension-block-symbols", arguments) + level = arguments.index("-symbol-graph-minimum-access-level") + self.assertEqual(arguments[level + 2], "public") + destination = Path(arguments[arguments.index("-emit-symbol-graph-dir") + 2]) + for name, module in [("Core", "Core"), ("Core@Foundation", "Core"), + ("Dependency", "Dependency")]: + (destination / (name + ".symbols.json")).write_text( + json.dumps({"module": {"name": module}})) + return "" + if arguments[:3] == ["xcrun", "docc", "convert"]: + symbols = Path(arguments[arguments.index("--additional-symbol-graph-dir") + 1]) + converted.extend(path.name for path in symbols.glob("*.symbols.json")) + Path(arguments[arguments.index("--output-path") + 1]).mkdir(parents=True) + return "" + if arguments[:4] == ["xcrun", "docc", "process-archive", "transform-for-static-hosting"]: + destination = Path(arguments[arguments.index("--output-path") + 1]) + landing = destination / "documentation/core/index.html" + landing.parent.mkdir(parents=True) + landing.write_text("Core documentation") + return "" + self.fail(f"Unexpected external command: {arguments}") + + output = root / "output" + output.mkdir() + with patch.object(build_site, "archive_source", return_value=nullcontext(source)), \ + patch.object(build_site, "command", side_effect=command), \ + patch.object(build_site, "add_identity"), \ + patch.object(build_site, "normalize_extension_hierarchy"), \ + patch.object(build_site, "identity_records", return_value=[]): + result = build_site.build_documentation( + {"organization": "sample"}, project, + {"name": "v1.0.0", "commit": {"sha": "a" * 40}}, + root / "cache", output, "fixture toolchain", 2, "blue", b"icon") + self.assertEqual(len(builds), 1) + self.assertEqual(set(converted), {"Core.symbols.json", "Core@Foundation.symbols.json"}) + self.assertEqual(result["modules"], ["Core"]) + self.assertTrue((output / "example/documentation/core/index.html").is_file()) + if __name__ == "__main__": unittest.main()