Files
swift-inotify/Sources/TaskCLI/GenerateDocumentation Command.swift
T. R. Bernstein 31ed16c828 Cache build directory of linux containers
SwiftPM uses caches heavily to reduce compilation and download time.
Before this change, we through these caches away with each container.
2026-03-22 17:51:24 +01:00

190 lines
6.6 KiB
Swift

import Foundation
import Logging
import Script
import Noora
struct GenerateDocumentationCommand: Script {
static let configuration = CommandConfiguration(
commandName: "generate-documentation",
abstract: "Generate DocC documentation of all targets inside a Linux container.",
aliases: ["gd"],
)
@OptionGroup var global: GlobalOptions
private static let doccPluginURL = "https://github.com/apple/swift-docc-plugin.git"
private static let doccPluginMinVersion = "1.4.0"
private static let skipItems: Set<String> = [".git", ".build", ".swiftpm", "public"]
// MARK: - Run
func run() async throws {
let noora = Noora()
let logger = global.makeLogger(labeled: "swift-inotify.cli.task.generate-documentation")
let fileManager = FileManager.default
let projectDirectory = URL(fileURLWithPath: fileManager.currentDirectoryPath)
let docker = try await executable(named: "docker")
let targets = try await Self.targets(for: projectDirectory)
noora.info("Generating DocC documentation on Linux.")
logger.debug("Current directory", metadata: ["current-directory": "\(projectDirectory.path(percentEncoded: false))", "targets": "\(targets.joined(separator: ", "))"])
let tempDirectory = try copyProject(from: projectDirectory)
logger.info("Copied project to temporary directory.", metadata: ["path": "\(tempDirectory.path(percentEncoded: false))"])
defer {
try? fileManager.removeItem(at: tempDirectory)
logger.info("Cleaned up temporary directory.")
}
try await injectDoccPluginDependency(in: tempDirectory, logger: logger)
let script = Self.makeRunScript(for: targets)
logger.debug("Container script", metadata: ["script": "\(script)"])
do {
try await docker(
"run", "--rm",
"-v", "\(tempDirectory.path):/code",
"-v", "swift-inotify-build-cache:/code/.build",
"--platform", Docker.getLinuxPlatformStringWithHostArchitecture(),
"-w", "/code",
"swift:latest",
"/bin/bash", "-c", script,
)
} catch {
noora.error("Documentation generation failed.")
return
}
try copyResults(from: tempDirectory, to: projectDirectory)
try Self.generateIndexHTML(
templateURL: projectDirectory.appending(path: ".github/workflows/index.tpl.html"),
outputURL: projectDirectory.appending(path: "public/index.html")
)
noora.success(
.alert("Documentation generated successfully.",
takeaways: ["Start a local web server with ./public as document root, i.e. with python3 -m http.server to browse the documentation."]
)
)
}
private static func generateIndexHTML(templateURL: URL, outputURL: URL) throws {
var content = try String(contentsOf: templateURL, encoding: .utf8)
let replacements: [(String, String)] = [
("{{project.name}}", "Swift Inotify"),
("{{project.tagline}}", "🗂️ Monitor filesystem events on Linux using modern Swift concurrency"),
("{{project.links}}", """
<li><a href="inotify/documentation/inotify/">Inotify</a>: The actual library.</li>\
<li><a href="inotifytaskcli/documentation/inotifytaskcli/">TaskCLI</a>: The project build command.</li>
"""),
]
for (placeholder, value) in replacements {
content = content.replacingOccurrences(of: placeholder, with: value)
}
try FileManager.default.createDirectory(
at: outputURL.deletingLastPathComponent(),
withIntermediateDirectories: true
)
try content.write(to: outputURL, atomically: true, encoding: .utf8)
}
private static func targets(for projectDirectory: URL) async throws -> [String] {
let packages = try await Self.packageTargets()
var packagesWithDoccFolder: [(name: String, path: String)] = []
for package in packages {
guard try await DoccFinder.hasDoccFolder(at: package.path) else { continue }
packagesWithDoccFolder.append(package)
}
return packagesWithDoccFolder.map { $0.name }
}
private static func packageTargets() async throws -> [(name: String, path: String)] {
let swift = try await executable(named: "swift")
let packageDescriptionOutput = try await outputOf {
try await swift("package", "describe", "--type", "json")
}
struct PackageDescription: Codable {
let targets: [Target]
}
struct Target: Codable {
let name: String
let path: String
}
let data = Data(packageDescriptionOutput.utf8)
let package = try JSONDecoder().decode(PackageDescription.self, from: data)
return package.targets.map { ($0.name, $0.path) }
}
private static func makeRunScript(for targets: [String]) -> String {
targets.map {
"mkdir -p \"./public/\($0.localizedLowercase)\" && " +
"swift package --allow-writing-to-directory \"\($0.localizedLowercase)\" " +
"generate-documentation --disable-indexing --transform-for-static-hosting " +
"--target \"\($0)\" " +
"--hosting-base-path \"\($0.localizedLowercase)\" " +
"--output-path \"./public/\($0.localizedLowercase)\""
}.joined(separator: " && ")
}
// MARK: - Project Copy
private func copyProject(from source: URL) throws -> URL {
let fileManager = FileManager.default
let tempDirectory = fileManager.temporaryDirectory.appending(path: "swift-inotify-docs-\(UUID().uuidString)")
try fileManager.createDirectory(at: tempDirectory, withIntermediateDirectories: true)
let contents = try fileManager.contentsOfDirectory(at: source, includingPropertiesForKeys: nil)
for item in contents {
guard !Self.skipItems.contains(item.lastPathComponent) else { continue }
try fileManager.copyItem(at: item, to: tempDirectory.appending(path: item.lastPathComponent))
}
return tempDirectory
}
private func copyResults(from tempDirectory: URL, to projectDirectory: URL) throws {
let fileManager = FileManager.default
let source = tempDirectory.appending(path: "public")
let destination = projectDirectory.appending(path: "public")
if fileManager.fileExists(atPath: destination.path(percentEncoded: false)) {
try fileManager.removeItem(at: destination)
}
try fileManager.copyItem(at: source, to: destination)
}
// MARK: - Dependency Injection
private func injectDoccPluginDependency(in directory: URL, logger: Logger) async throws {
let swift = try await executable(named: "swift")
do {
try await swift(
"package", "--package-path", directory.path(percentEncoded: false),
"add-dependency", "--from", Self.doccPluginMinVersion, Self.doccPluginURL
)
} catch {
throw GenerateDocumentationError.dependencyInjectionFailed
}
logger.info("Injected swift-docc-plugin dependency.")
}
}
enum GenerateDocumentationError: Error, CustomStringConvertible {
case dependencyInjectionFailed
var description: String {
switch self {
case .dependencyInjectionFailed:
"Failed to add swift-docc-plugin dependency to Package.swift."
}
}
}