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: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ Package.resolved
.build-output/
**/*.docc/header.html
**/*.docc/footer.html
**/*.docc/favicon.ico
scripts/__pycache__/
scripts/.coverage
2 changes: 2 additions & 0 deletions .licenseignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
**/*.gitkeep
**/*.html
**/*.ico
**/*.svg
**/Package.swift
common/README
Binary file added common/favicon.ico
Binary file not shown.
1 change: 1 addition & 0 deletions common/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ python3 -m http.server 8123 --directory .build-output
Then in another terminal:

```bash
open http://localhost:8123/latest/documentation/
open http://localhost:8123/main/documentation/
```

Serve from `.build-output` (the parent), not `.build-output/latest`: the build
Expand Down
49 changes: 46 additions & 3 deletions scripts/build_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,19 @@ class ArchiveFetchError(Exception):
# Common template files to copy into each .docc catalog before building
TEMPLATE_FILES = ["header.html", "footer.html"]

# Shared favicon copied verbatim (no placeholder substitution) into each .docc
# catalog root before building. DocC recognizes a favicon.ico at the catalog
# root as a custom favicon and copies it into the output, overriding its own
# default icon (see swift-docc's DocumentationBundleFileTypes.isCustomFavicon).
FAVICON_FILE = "favicon.ico"

# Shared mask-icon SVG (Safari pinned-tab icon). Unlike favicon.ico, DocC has
# no catalog-level override for this file — a loose favicon.svg dropped into
# a .docc catalog is silently discarded by `docc convert`. The only place
# this can be fixed is the post-transform restore in
# _finalize_combined_archive (see restore_custom_favicon).
FAVICON_SVG_FILE = "favicon.svg"


def parse_args():
parser = argparse.ArgumentParser(
Expand Down Expand Up @@ -477,7 +490,8 @@ def render_common_template(text, year=None):


def install_templates(catalog_dir, common_dir, source_id):
"""Copy common template files (header.html, footer.html) into a .docc catalog."""
"""Copy common template files (header.html, footer.html) and the shared
favicon.ico into a .docc catalog."""
for tmpl in TEMPLATE_FILES:
src = common_dir / tmpl
dst = catalog_dir / tmpl
Expand All @@ -486,6 +500,13 @@ def install_templates(catalog_dir, common_dir, source_id):
dst.write_text(render_common_template(src.read_text()))
print(f" Installed {tmpl} -> {catalog_dir}/")

favicon_src = common_dir / FAVICON_FILE
favicon_dst = catalog_dir / FAVICON_FILE
if favicon_dst.exists():
print(f" WARNING: overwriting existing {FAVICON_FILE} in {source_id} catalog")
shutil.copyfile(str(favicon_src), str(favicon_dst))
print(f" Installed {FAVICON_FILE} -> {catalog_dir}/")


def find_doccarchive(search_dir, target):
"""Find a .doccarchive directory produced by swift package generate-documentation."""
Expand Down Expand Up @@ -892,6 +913,22 @@ def inject_custom_templates_into_stubs(archive_path, common_dir):
return patched


def restore_custom_favicon(archive_path, common_dir, filename=FAVICON_FILE):
"""Reinstate a shared favicon asset after the static-hosting transform.

Workaround for `docc process-archive transform-for-static-hosting`
unconditionally overwriting favicon.ico/favicon.svg with its own bundled
defaults (swift-docc's TransformForStaticHostingAction copies every file
from its HTML template directory over the output, with no exclusion for
either file). Since this transform always runs last, this is the only
point where the custom favicon needs to be reapplied — drop this when
swift-docc excludes these files from that copy.
"""
favicon_src = Path(common_dir) / filename
favicon_dst = Path(archive_path) / filename
shutil.copyfile(str(favicon_src), str(favicon_dst))


def _finalize_combined_archive(all_archives, output_dir, version_slug, docc_cmd, prior_failed, common_dir=None, navigation=None, hosting_base_path=None, canonical_base_url=None):
"""Merge per-source archives and apply the static-hosting transform.

Expand Down Expand Up @@ -982,6 +1019,12 @@ def _finalize_combined_archive(all_archives, output_dir, version_slug, docc_cmd,
patched = inject_custom_templates_into_stubs(combined_output, common_dir)
print(f"Patched custom-header/footer into {patched} per-route stub(s).")

# Workaround: transform-for-static-hosting overwrites the favicons
# with DocC's own defaults (see restore_custom_favicon docstring).
restore_custom_favicon(combined_output, common_dir)
restore_custom_favicon(combined_output, common_dir, FAVICON_SVG_FILE)
print("Restored shared favicon.ico/favicon.svg after static-hosting transform.")

prior_steps.append("static-hosting-transform")

if canonical_base_url:
Expand Down Expand Up @@ -1047,8 +1090,8 @@ def main():
check_prerequisites()
tools = discover_tools()

# Validate common template files exist
for tmpl in TEMPLATE_FILES:
# Validate common template files and the shared favicon exist
for tmpl in TEMPLATE_FILES + [FAVICON_FILE, FAVICON_SVG_FILE]:
tmpl_path = common_dir / tmpl
if not tmpl_path.is_file():
print(f"Error: common template '{tmpl}' not found at {tmpl_path}")
Expand Down
119 changes: 119 additions & 0 deletions scripts/test_build_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2028,6 +2028,52 @@ def test_curation_success_records_navigator_curation(self):
)
self.assertEqual(failed, [])

def test_favicon_survives_static_hosting_transform(self):
"""Reproduces docc process-archive transform-for-static-hosting
overwriting favicon.ico/favicon.svg with its own bundled defaults,
and verifies build_docs.py restores the shared ones afterward."""
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
common_dir = tmp_path / "common"
common_dir.mkdir()
(common_dir / "favicon.ico").write_bytes(b"SWIFT-FAVICON")
(common_dir / "favicon.svg").write_bytes(b"SWIFT-FAVICON-SVG")
(common_dir / "header.html").write_text("HDR")
(common_dir / "footer.html").write_text("FTR")
archive = tmp_path / "a.doccarchive"
archive.mkdir()
(archive / "index.html").write_text("ok")

def fake_run(cmd, **kw):
out_idx = cmd.index("--output-path") + 1
out = Path(cmd[out_idx])
out.mkdir(parents=True, exist_ok=True)
(out / "index.html").write_text("stub")
if "merge" in cmd:
(out / "data").mkdir(parents=True, exist_ok=True)
(out / "favicon.ico").write_bytes(b"SWIFT-FAVICON")
(out / "favicon.svg").write_bytes(b"DOCC-DEFAULT-GLOBE-SVG")
else:
# Simulate transform-for-static-hosting clobbering the
# favicons with DocC's own bundled defaults.
(out / "favicon.ico").write_bytes(b"DOCC-DEFAULT-GLOBE")
(out / "favicon.svg").write_bytes(b"DOCC-DEFAULT-GLOBE-SVG")
return subprocess.CompletedProcess(cmd, 0)

with mock.patch.object(build_docs.subprocess, "run", side_effect=fake_run):
succeeded, failed = build_docs._finalize_combined_archive(
[archive], tmp_path, "main", ["docc"], prior_failed=[],
common_dir=common_dir,
)
self.assertEqual(failed, [])
combined_output = tmp_path / "main"
self.assertEqual(
(combined_output / "favicon.ico").read_bytes(), b"SWIFT-FAVICON"
)
self.assertEqual(
(combined_output / "favicon.svg").read_bytes(), b"SWIFT-FAVICON-SVG"
)


class RenderCommonTemplate(unittest.TestCase):
def test_substitutes_copyright_year_placeholder(self):
Expand Down Expand Up @@ -2065,6 +2111,7 @@ def test_substitutes_copyright_year_in_installed_footer(self):
common.mkdir()
(common / "header.html").write_text("HDR")
(common / "footer.html").write_text("Copyright {{COPYRIGHT_YEAR}}")
(common / "favicon.ico").write_bytes(b"ICO")
catalog = root / "Foo.docc"
catalog.mkdir()
build_docs.install_templates(catalog, common, "foo")
Expand All @@ -2080,12 +2127,43 @@ def test_warns_and_overwrites_existing_template(self):
common.mkdir()
(common / "header.html").write_text("HDR")
(common / "footer.html").write_text("FTR")
(common / "favicon.ico").write_bytes(b"ICO")
catalog = root / "Foo.docc"
catalog.mkdir()
(catalog / "footer.html").write_text("stale")
build_docs.install_templates(catalog, common, "foo")
self.assertEqual((catalog / "footer.html").read_text(), "FTR")

def _write_common_templates(self, common):
(common / "header.html").write_text("HDR")
(common / "footer.html").write_text("FTR")

def test_copies_favicon_byte_for_byte(self):
favicon_bytes = bytes(range(256))
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
common = root / "common"
common.mkdir()
self._write_common_templates(common)
(common / "favicon.ico").write_bytes(favicon_bytes)
catalog = root / "Foo.docc"
catalog.mkdir()
build_docs.install_templates(catalog, common, "foo")
self.assertEqual((catalog / "favicon.ico").read_bytes(), favicon_bytes)

def test_warns_and_overwrites_existing_favicon(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
common = root / "common"
common.mkdir()
self._write_common_templates(common)
(common / "favicon.ico").write_bytes(b"NEW")
catalog = root / "Foo.docc"
catalog.mkdir()
(catalog / "favicon.ico").write_bytes(b"stale")
build_docs.install_templates(catalog, common, "foo")
self.assertEqual((catalog / "favicon.ico").read_bytes(), b"NEW")


class InjectCustomTemplatesIntoStubs(unittest.TestCase):
"""Workaround for swiftlang/swift-docc#1532: see build_docs function docstring."""
Expand Down Expand Up @@ -2213,6 +2291,47 @@ def test_template_ordering_matches_docc_convert(self):
self.assertLess(footer_pos, header_pos)


class RestoreCustomFavicon(unittest.TestCase):
"""Workaround for `docc process-archive transform-for-static-hosting`
unconditionally overwriting favicon.ico with its own bundled default
(swift-docc's TransformForStaticHostingAction copies every file from its
HTML template directory over the output, with no exclusion for
favicon.ico)."""

def test_overwrites_transformed_favicon_with_shared_one(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
common = root / "common"
common.mkdir()
(common / "favicon.ico").write_bytes(b"SWIFT-FAVICON")
archive = root / "main"
archive.mkdir()
(archive / "favicon.ico").write_bytes(b"DOCC-DEFAULT-GLOBE")
build_docs.restore_custom_favicon(archive, common)
self.assertEqual(
(archive / "favicon.ico").read_bytes(), b"SWIFT-FAVICON"
)

def test_restores_favicon_svg_when_filename_given(self):
"""favicon.svg has no DocC catalog-level override at all (unlike
favicon.ico), so this post-transform restore is the only place it
can ever be fixed."""
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
common = root / "common"
common.mkdir()
(common / "favicon.svg").write_bytes(b"SWIFT-FAVICON-SVG")
archive = root / "main"
archive.mkdir()
(archive / "favicon.svg").write_bytes(b"DOCC-DEFAULT-GLOBE-SVG")
build_docs.restore_custom_favicon(
archive, common, filename=build_docs.FAVICON_SVG_FILE
)
self.assertEqual(
(archive / "favicon.svg").read_bytes(), b"SWIFT-FAVICON-SVG"
)


class CleanPackageBuildDirs(unittest.TestCase):
def test_removes_build_dir_for_local_source(self):
with tempfile.TemporaryDirectory() as tmp:
Expand Down