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
15 changes: 7 additions & 8 deletions .github/workflows/build_documentation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,16 @@ jobs:
if: github.event_name != 'schedule' || github.repository == 'swiftlang/docs'
runs-on: ubuntu-latest
container:
# image: swiftlang/swift:nightly-main-jammy
image: swift:6.3
image: swiftlang/swift:nightly-main-jammy
steps:
- uses: actions/checkout@v7

- uses: actions/cache@v6
with:
path: .workspace
key: docs-workspace-${{ hashFiles('scripts/sources.json') }}
restore-keys: |
docs-workspace-
# - uses: actions/cache@v6
# with:
# path: .workspace
# key: docs-workspace-${{ hashFiles('scripts/sources.json') }}
# restore-keys: |
# docs-workspace-

- name: Install Python
run: apt-get update && apt-get install -y python3
Expand Down
71 changes: 68 additions & 3 deletions scripts/build_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class ArchiveFetchError(Exception):
DOCC_BUILD_FLAGS = [
"--experimental-enable-custom-templates",
"--enable-mentioned-in",
"--enable-experimental-external-link-support",
# "--enable-experimental-external-link-support",
# Emit a markdown copy of every rendered document (plus a manifest) under
# data/documentation/. These sidecar files survive docc merge and
# process-archive transform-for-static-hosting, so they reach the published
Expand Down Expand Up @@ -458,12 +458,44 @@ def find_doccarchive(search_dir, target):
return None


def _docc_plugin_resolves(source_dir, swift_cmd):
"""Check whether swift-docc-plugin is in the resolved dependency graph.

Uses `swift package describe --type json`, which evaluates Package.swift
under the real build environment — unlike a text search, this catches a
dependency that was added into an inactive branch of a conditional
expression (e.g. a manifest with two 'dependencies:' arrays gated by an
env-var flag).
"""
try:
result = subprocess.run(
swift_cmd + ["package", "describe", "--type", "json"],
cwd=str(source_dir), capture_output=True, text=True, check=True,
)
pkg_info = json.loads(result.stdout)
except (subprocess.CalledProcessError, json.JSONDecodeError):
return False
return any(
"swift-docc-plugin" in dep.get("url", "")
for dep in pkg_info.get("dependencies", [])
)


def add_docc_plugin(source_dir, swift_cmd):
"""Inject swift-docc-plugin dependency if not already present."""
"""Inject swift-docc-plugin dependency if not already present.

Verifies the dependency actually resolves after `add-dependency` runs.
Some manifests have more than one 'dependencies:' array (e.g. a ternary
gated on a local-dependencies flag); `swift package add-dependency` can
add the entry to a branch that isn't active in this build, in which case
the entry is relocated into the first 'dependencies:' array instead.
"""
package_swift = source_dir / "Package.swift"
if "swift-docc-plugin" in package_swift.read_text():
before = package_swift.read_text()
if "swift-docc-plugin" in before:
print("swift-docc-plugin dependency already present, skipping.")
return

print("Adding swift-docc-plugin dependency...")
subprocess.run(
swift_cmd + [
Expand All @@ -475,6 +507,39 @@ def add_docc_plugin(source_dir, swift_cmd):
check=True,
)

if _docc_plugin_resolves(source_dir, swift_cmd):
return

print(
" swift-docc-plugin did not resolve after being added — the "
"manifest likely has more than one 'dependencies:' array; "
"relocating it into the first one..."
)
after = package_swift.read_text()
added_lines = [
line for line in after.splitlines(keepends=True)
if "swift-docc-plugin" in line
]
if len(added_lines) != 1:
raise RuntimeError(
"could not isolate the swift-docc-plugin line added by "
"'swift package add-dependency' in Package.swift"
)
plugin_line = added_lines[0]

without_line = after.replace(plugin_line, "", 1)
marker = without_line.index("dependencies:")
bracket = without_line.index("[", marker) + 1
fixed = without_line[:bracket] + "\n" + plugin_line.strip() + without_line[bracket:]
package_swift.write_text(fixed)

if not _docc_plugin_resolves(source_dir, swift_cmd):
raise RuntimeError(
"swift-docc-plugin still does not resolve after relocating it "
"into the first 'dependencies:' array in Package.swift"
)
print(" Relocated swift-docc-plugin into the active dependencies array.")


def _build_archive_source(source, workspace, temp_archive_dir):
"""Fetch an archive source, copy it into the staging dir, optionally strip.
Expand Down
4 changes: 2 additions & 2 deletions scripts/sources.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,14 @@
"id": "swiftpm",
"type": "git",
"repo": "https://github.com/swiftlang/swift-package-manager.git",
"ref": "release/6.3",
"ref": "main",
"targets": ["PackageManagerDocs", "PackageDescription", "PackagePlugin"]
},
{
"id": "swift-testing",
"type": "git",
"repo": "https://github.com/swiftlang/swift-testing.git",
"ref": "release/6.3",
"ref": "main",
"targets": ["Testing"],
"add_docc_plugin": true
},
Expand Down
180 changes: 180 additions & 0 deletions scripts/test_build_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,186 @@ def fake_run(cmd, **kw):
self.assertEqual(commit, "unknown")


class DoccPluginResolves(unittest.TestCase):
def test_true_when_dependency_present(self):
def fake_run(cmd, **kw):
payload = json.dumps({
"dependencies": [
{"url": "https://github.com/swiftlang/swift-docc-plugin"},
]
})
return subprocess.CompletedProcess(cmd, 0, stdout=payload, stderr="")

with mock.patch.object(build_docs.subprocess, "run", side_effect=fake_run):
self.assertTrue(
build_docs._docc_plugin_resolves(Path("/tmp/x"), ["swift"])
)

def test_false_when_dependency_absent(self):
def fake_run(cmd, **kw):
payload = json.dumps({
"dependencies": [
{"url": "https://github.com/swiftlang/swift-syntax.git"},
]
})
return subprocess.CompletedProcess(cmd, 0, stdout=payload, stderr="")

with mock.patch.object(build_docs.subprocess, "run", side_effect=fake_run):
self.assertFalse(
build_docs._docc_plugin_resolves(Path("/tmp/x"), ["swift"])
)

def test_false_when_describe_fails(self):
def fake_run(cmd, **kw):
raise subprocess.CalledProcessError(1, cmd)

with mock.patch.object(build_docs.subprocess, "run", side_effect=fake_run):
self.assertFalse(
build_docs._docc_plugin_resolves(Path("/tmp/x"), ["swift"])
)

def test_false_when_describe_output_is_not_json(self):
def fake_run(cmd, **kw):
return subprocess.CompletedProcess(cmd, 0, stdout="not json", stderr="")

with mock.patch.object(build_docs.subprocess, "run", side_effect=fake_run):
self.assertFalse(
build_docs._docc_plugin_resolves(Path("/tmp/x"), ["swift"])
)


class AddDoccPlugin(unittest.TestCase):
def _describe_result(self, cmd, has_plugin):
deps = [{"url": "https://github.com/swiftlang/swift-syntax.git"}]
if has_plugin:
deps.append({"url": "https://github.com/swiftlang/swift-docc-plugin"})
return subprocess.CompletedProcess(
cmd, 0, stdout=json.dumps({"dependencies": deps}), stderr=""
)

def test_skips_when_already_present(self):
with tempfile.TemporaryDirectory() as tmp:
source_dir = Path(tmp)
(source_dir / "Package.swift").write_text(
'dependencies: [\n .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.0"),\n],\n'
)
with mock.patch.object(build_docs.subprocess, "run") as run:
build_docs.add_docc_plugin(source_dir, ["swift"])
run.assert_not_called()

def test_no_relocation_needed_when_plugin_resolves(self):
with tempfile.TemporaryDirectory() as tmp:
source_dir = Path(tmp)
package_swift = source_dir / "Package.swift"
package_swift.write_text(
'dependencies: [\n'
' .package(url: "https://example.com/a.git", from: "1.0.0"),\n'
'],\n'
)

def fake_run(cmd, **kw):
if "add-dependency" in cmd:
text = package_swift.read_text()
text = text.replace(
'.package(url: "https://example.com/a.git", from: "1.0.0"),',
'.package(url: "https://example.com/a.git", from: "1.0.0"),\n'
' .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.0"),',
)
package_swift.write_text(text)
return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
return self._describe_result(cmd, has_plugin=True)

with mock.patch.object(build_docs.subprocess, "run", side_effect=fake_run):
build_docs.add_docc_plugin(source_dir, ["swift"])

final = package_swift.read_text()
self.assertEqual(final.count("swift-docc-plugin"), 1)

def test_relocates_when_added_into_inactive_branch(self):
with tempfile.TemporaryDirectory() as tmp:
source_dir = Path(tmp)
package_swift = source_dir / "Package.swift"
package_swift.write_text(
'dependencies: cond ? [\n'
' .package(url: "https://example.com/active.git", from: "1.0.0"),\n'
'] : [\n'
' .package(path: "../active"),\n'
'],\n'
)
describe_results = iter([False, True])

def fake_run(cmd, **kw):
if "add-dependency" in cmd:
text = package_swift.read_text()
text = text.replace(
'.package(path: "../active"),',
'.package(path: "../active"),\n'
' .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.0"),',
)
package_swift.write_text(text)
return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
return self._describe_result(cmd, has_plugin=next(describe_results))

with mock.patch.object(build_docs.subprocess, "run", side_effect=fake_run):
build_docs.add_docc_plugin(source_dir, ["swift"])

final = package_swift.read_text()
self.assertEqual(final.count("swift-docc-plugin"), 1)
self.assertLess(
final.index("swift-docc-plugin"), final.index("../active"),
"plugin dependency should have been relocated into the first array",
)

def test_raises_when_still_unresolved_after_relocation(self):
with tempfile.TemporaryDirectory() as tmp:
source_dir = Path(tmp)
package_swift = source_dir / "Package.swift"
package_swift.write_text(
'dependencies: cond ? [\n'
' .package(url: "https://example.com/active.git", from: "1.0.0"),\n'
'] : [\n'
' .package(path: "../active"),\n'
'],\n'
)

def fake_run(cmd, **kw):
if "add-dependency" in cmd:
text = package_swift.read_text()
text = text.replace(
'.package(path: "../active"),',
'.package(path: "../active"),\n'
' .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.0"),',
)
package_swift.write_text(text)
return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
return self._describe_result(cmd, has_plugin=False)

with mock.patch.object(build_docs.subprocess, "run", side_effect=fake_run):
with self.assertRaises(RuntimeError):
build_docs.add_docc_plugin(source_dir, ["swift"])

def test_raises_when_added_line_not_isolatable(self):
with tempfile.TemporaryDirectory() as tmp:
source_dir = Path(tmp)
package_swift = source_dir / "Package.swift"
package_swift.write_text('dependencies: [\n],\n')

def fake_run(cmd, **kw):
if "add-dependency" in cmd:
package_swift.write_text(
'dependencies: [\n'
' .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.0"),\n'
' // swift-docc-plugin duplicate mention\n'
'],\n'
)
return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
return self._describe_result(cmd, has_plugin=False)

with mock.patch.object(build_docs.subprocess, "run", side_effect=fake_run):
with self.assertRaises(RuntimeError):
build_docs.add_docc_plugin(source_dir, ["swift"])


class MergeArchives(unittest.TestCase):
def _capture_merge_cmd(self, version):
"""Run merge_archives with a stubbed subprocess and return the cmd."""
Expand Down