From 691007dfdb6e2352723ba542006e06a69d6f8b23 Mon Sep 17 00:00:00 2001 From: Joe Heck Date: Tue, 8 Sep 2026 12:54:26 -0700 Subject: [PATCH 1/5] updating favicon.ico to use from swift.org, and copy into docc catalogs prior to building/merging --- common/favicon.ico | Bin 0 -> 16958 bytes scripts/build_docs.py | 20 +++++++++++++++++--- scripts/test_build_docs.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 common/favicon.ico diff --git a/common/favicon.ico b/common/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..ffb5e7b288288718974fe9ac468ab3f0b99cd1dd GIT binary patch literal 16958 zcmeHOS&$S(5FO-$PyhTg^pgnv@P}Yo4_3r8LfBmp1V!B7P>@3qK?OO~;1CZq06_$m z1x3ODmGA&T7hDL0<-PJ?uuIP*oqo=eMPN% zdqs_>nV%MZ!u-Vep&~zM6K#Ws(;83U%{vyn+DB;c#Z0=TdyTuUqS~{!$k?3Al6eO2 zF2-Ph%rdL8+I4=w^TpxyI$HFJ2pE z_zBx4*Sb3Bce0J7*>y#?->&Z%Vmndv0bUpC%W%E3kgvO5!rxeE{C$S|=#BXS`t<7% zZQ2&4-3Qv}$cY%8Y>v^vV=>ykr;XPA5~2A^gEVZ4pDJr}>yNz#duhX#hy`}k?QTrr zPxvwl-`L}6IOACRV}#C}Z6~|r_Qq+$<_OhI^4sF;yG;=a1mh~-oR;vzhf?{*vdjYm zJhWg%C@0KjGTL{fjYdst&DwSk?k}s>hZ9vqqLj645^IZuAAY3DH>PFo?eC$vp9JYr zi+x?2;nwFL`YT57^7wdawC~#ZczB4Hrpyh{;p4Gu#nE>Z@PsA&j{Ns&?*-`JbA`iw z4Vd)O#&Cn7xRjH6MSoGiACvH_l+P&B`x!57*}d|*A0 zF=l(Wga5NN>DXJ`aa#R-m`1nf8_{LnKYIDC zl>a(-yx*u;M&6g-s`>oIzz_R=2m3=tw2}3s_a#KRE!(5?h|Yhh?B}6S*jQHLJERlY zpOCSsycbIU*n`y*{3ZB+lX_z6ji`5uHeOOynFw12Mfq%*B%<;NySCk$b z>Lu)lha>5_Jar~UgU0!?^1it-yS;=Mf{aV`J>U3eEeNEk9aym^41W6ao~_=`fgM=& zw6)%h`jgzy3f_}Z#VnW0C;lK1+UU{e(&sVkd5S%9toL%ecHXi6 zWB*A{828wlvQwA7tP}i*Wyg8QWA4?*sNn<4BSKu+>RYS7 z=ZYVDmRQRW_tUolSs=EL*jz3)nZ7>6TJ!|l8UD1stli%de&zTl=5yG#W^wstYeEUx zAU8fA!~?qlV@RfXOPq}- zi7z?+t#tkYHny3a4Zj6@5H&B@U*9C2(&V7`eb;YvKIWyij<-H|WYaKdCw}#|qtJopWO2nDZr_e@onJNwshOO>xRBn$hAJb6gT{{0* zO6Ok(yyPR;ySc>kl(F+%C!c?X9Z(AApV7||TgDkWJ6LR&)t!F^{u}-LJMILq9BV!Z literal 0 HcmV?d00001 diff --git a/scripts/build_docs.py b/scripts/build_docs.py index dc3afabb..cecb6976 100755 --- a/scripts/build_docs.py +++ b/scripts/build_docs.py @@ -66,6 +66,12 @@ 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" + def parse_args(): parser = argparse.ArgumentParser( @@ -477,7 +483,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 @@ -486,6 +493,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.""" @@ -1047,8 +1061,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]: tmpl_path = common_dir / tmpl if not tmpl_path.is_file(): print(f"Error: common template '{tmpl}' not found at {tmpl_path}") diff --git a/scripts/test_build_docs.py b/scripts/test_build_docs.py index ea261434..f9b354fd 100644 --- a/scripts/test_build_docs.py +++ b/scripts/test_build_docs.py @@ -2065,6 +2065,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") @@ -2080,12 +2081,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.""" From 639e0fa4f23a6e4fb995ed75c92dac916e5b9158 Mon Sep 17 00:00:00 2001 From: Joe Heck Date: Tue, 8 Sep 2026 13:13:26 -0700 Subject: [PATCH 2/5] final transform overwrites all the HTML - and the favicon - so we at the moment we need to post-process it back into place --- scripts/README.md | 2 +- scripts/build_docs.py | 21 +++++++++++++ scripts/test_build_docs.py | 62 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 1 deletion(-) diff --git a/scripts/README.md b/scripts/README.md index a7f6529a..d6c78f1e 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -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 diff --git a/scripts/build_docs.py b/scripts/build_docs.py index cecb6976..bd98d4ab 100755 --- a/scripts/build_docs.py +++ b/scripts/build_docs.py @@ -906,6 +906,22 @@ def inject_custom_templates_into_stubs(archive_path, common_dir): return patched +def restore_custom_favicon(archive_path, common_dir): + """Reinstate the shared favicon.ico after the static-hosting transform. + + Workaround for `docc process-archive transform-for-static-hosting` + unconditionally overwriting the archive's 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). 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 favicon.ico from that copy. + """ + favicon_src = Path(common_dir) / FAVICON_FILE + favicon_dst = Path(archive_path) / FAVICON_FILE + 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. @@ -996,6 +1012,11 @@ 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 favicon.ico + # with DocC's own default (see restore_custom_favicon docstring). + restore_custom_favicon(combined_output, common_dir) + print("Restored shared favicon.ico after static-hosting transform.") + prior_steps.append("static-hosting-transform") if canonical_base_url: diff --git a/scripts/test_build_docs.py b/scripts/test_build_docs.py index f9b354fd..90ff81b2 100644 --- a/scripts/test_build_docs.py +++ b/scripts/test_build_docs.py @@ -2028,6 +2028,46 @@ 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 with its own bundled default, and verifies + build_docs.py restores the shared one 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 / "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") + else: + # Simulate transform-for-static-hosting clobbering the + # favicon with DocC's own bundled default. + (out / "favicon.ico").write_bytes(b"DOCC-DEFAULT-GLOBE") + 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" + ) + class RenderCommonTemplate(unittest.TestCase): def test_substitutes_copyright_year_placeholder(self): @@ -2245,6 +2285,28 @@ 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" + ) + + class CleanPackageBuildDirs(unittest.TestCase): def test_removes_build_dir_for_local_source(self): with tempfile.TemporaryDirectory() as tmp: From 2de7539aeff6b869c4427449adb92985999bacfa Mon Sep 17 00:00:00 2001 From: Joe Heck Date: Tue, 8 Sep 2026 13:37:56 -0700 Subject: [PATCH 3/5] making sure favicon.svg is the Swift logo as well - although v. different from favicon.ico --- common/favicon.svg | 1 + scripts/build_docs.py | 36 ++++++++++++++++++++++-------------- scripts/test_build_docs.py | 31 ++++++++++++++++++++++++++++--- 3 files changed, 51 insertions(+), 17 deletions(-) create mode 100644 common/favicon.svg diff --git a/common/favicon.svg b/common/favicon.svg new file mode 100644 index 00000000..637a1e96 --- /dev/null +++ b/common/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scripts/build_docs.py b/scripts/build_docs.py index bd98d4ab..3fa4976f 100755 --- a/scripts/build_docs.py +++ b/scripts/build_docs.py @@ -72,6 +72,13 @@ class ArchiveFetchError(Exception): # 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( @@ -906,19 +913,19 @@ def inject_custom_templates_into_stubs(archive_path, common_dir): return patched -def restore_custom_favicon(archive_path, common_dir): - """Reinstate the shared favicon.ico after the static-hosting transform. +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 the archive's 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). 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 favicon.ico from that copy. + 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) / FAVICON_FILE - favicon_dst = Path(archive_path) / FAVICON_FILE + favicon_src = Path(common_dir) / filename + favicon_dst = Path(archive_path) / filename shutil.copyfile(str(favicon_src), str(favicon_dst)) @@ -1012,10 +1019,11 @@ 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 favicon.ico - # with DocC's own default (see restore_custom_favicon docstring). + # 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) - print("Restored shared favicon.ico after static-hosting transform.") + 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") @@ -1083,7 +1091,7 @@ def main(): tools = discover_tools() # Validate common template files and the shared favicon exist - for tmpl in TEMPLATE_FILES + [FAVICON_FILE]: + 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}") diff --git a/scripts/test_build_docs.py b/scripts/test_build_docs.py index 90ff81b2..841d9fba 100644 --- a/scripts/test_build_docs.py +++ b/scripts/test_build_docs.py @@ -2030,13 +2030,14 @@ def test_curation_success_records_navigator_curation(self): def test_favicon_survives_static_hosting_transform(self): """Reproduces docc process-archive transform-for-static-hosting - overwriting favicon.ico with its own bundled default, and verifies - build_docs.py restores the shared one afterward.""" + 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" @@ -2051,10 +2052,12 @@ def fake_run(cmd, **kw): 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 - # favicon with DocC's own bundled default. + # 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): @@ -2067,6 +2070,9 @@ def fake_run(cmd, **kw): 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): @@ -2306,6 +2312,25 @@ def test_overwrites_transformed_favicon_with_shared_one(self): (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): From 43a04bb670ae4df45658ef1fa192d2822cfaa485 Mon Sep 17 00:00:00 2001 From: Joe Heck Date: Tue, 8 Sep 2026 13:38:38 -0700 Subject: [PATCH 4/5] updating gitignore for the favicon.ico replicating --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ed340052..64546233 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,6 @@ Package.resolved .build-output/ **/*.docc/header.html **/*.docc/footer.html +**/*.docc/favicon.ico scripts/__pycache__/ scripts/.coverage From 59d9176d078397217dc47eba9278c621b80b8392 Mon Sep 17 00:00:00 2001 From: Joe Heck Date: Tue, 8 Sep 2026 13:48:30 -0700 Subject: [PATCH 5/5] escaping .ico and .svg from license header checks --- .licenseignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.licenseignore b/.licenseignore index 1a6520c1..01e5dd75 100644 --- a/.licenseignore +++ b/.licenseignore @@ -1,4 +1,6 @@ **/*.gitkeep **/*.html +**/*.ico +**/*.svg **/Package.swift common/README