Compare commits

..

4 Commits

Author SHA1 Message Date
T. R. Bernstein
e153f15d43 Add SwiftPackageIndex configuration file
Some checks failed
Docs / docs (push) Has been cancelled
Docs / deploy (push) Has been cancelled
2026-03-15 20:06:26 +01:00
T. R. Bernstein
430713c741 Add GitHub workflow file to generate docs 2026-03-15 20:06:26 +01:00
T. R. Bernstein
4efc27dd3a Update README section about docs generation
Show how to generate the Docc docs using task CLI instead of faning out
to swift package docc plugin.
2026-03-15 20:06:26 +01:00
T. R. Bernstein
239374704a Add generate-docs command to build task
The Swift Docc has to run in a Linux container to be able to build the
documentation, as it needs access to the inotify.h header files.
2026-03-15 20:06:21 +01:00
6 changed files with 95 additions and 52 deletions

View File

@@ -2,3 +2,4 @@ version: 1
builder: builder:
configs: configs:
- documentation_targets: [Inotify, TaskCLI] - documentation_targets: [Inotify, TaskCLI]
platform: Linux

View File

@@ -47,6 +47,7 @@ let package = Package(
.product(name: "ArgumentParser", package: "swift-argument-parser"), .product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
.product(name: "Logging", package: "swift-log"), .product(name: "Logging", package: "swift-log"),
.product(name: "_NIOFileSystem", package: "swift-nio"),
.product(name: "Subprocess", package: "swift-subprocess"), .product(name: "Subprocess", package: "swift-subprocess"),
.product(name: "Noora", package: "Noora") .product(name: "Noora", package: "Noora")
] ]

View File

@@ -4,6 +4,6 @@ import ArgumentParser
struct Command: AsyncParsableCommand { struct Command: AsyncParsableCommand {
static let configuration = CommandConfiguration( static let configuration = CommandConfiguration(
abstract: "Project tasks of Astzweig's Swift Inotify project.", abstract: "Project tasks of Astzweig's Swift Inotify project.",
subcommands: [TestCommand.self, GenerateDocsCommand.self] subcommands: [TestCommand.self, GenerateDocumentationCommand.self]
) )
} }

View File

@@ -0,0 +1,37 @@
import _NIOFileSystem
public struct DoccFinder {
static let fileManager = FileSystem.shared
public static func getTargetsWithDocumentation(at paths: String...) async throws -> [String] {
try await Self.getTargetsWithDocumentation(at: paths)
}
static func getTargetsWithDocumentation(at paths: [String]) async throws -> [String] {
var resolved: [String] = []
for path in paths {
let itemPath = FilePath(path)
try await withSubdirectories(at: itemPath) { targetPath in
print("Target path is", targetPath.description)
try await withSubdirectories(at: targetPath) { subdirectory in
guard subdirectory.description.hasSuffix(".docc") else { return }
guard let target = targetPath.lastComponent?.description else { return }
resolved.append(target)
}
}
}
return resolved
}
private static func withSubdirectories(at path: FilePath, body: (FilePath) async throws -> Void) async throws {
let directoryHandle = try await fileManager.openDirectory(atPath: path)
for try await childContent in directoryHandle.listContents() {
guard childContent.type == .directory else { continue }
try await body(childContent.path)
}
try await directoryHandle.close()
}
}

View File

@@ -5,10 +5,10 @@ import Logging
import Noora import Noora
import Subprocess import Subprocess
struct GenerateDocsCommand: AsyncParsableCommand { struct GenerateDocumentationCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration( static let configuration = CommandConfiguration(
commandName: "generate-docs", commandName: "generate-documentation",
abstract: "Generate DocC documentation inside a Linux container.", abstract: "Generate DocC documentation of all targets inside a Linux container.",
aliases: ["gd"], aliases: ["gd"],
) )
@@ -16,20 +16,20 @@ struct GenerateDocsCommand: AsyncParsableCommand {
private static let doccPluginURL = "https://github.com/apple/swift-docc-plugin.git" private static let doccPluginURL = "https://github.com/apple/swift-docc-plugin.git"
private static let doccPluginMinVersion = "1.4.0" private static let doccPluginMinVersion = "1.4.0"
private static let targets = ["Inotify", "TaskCLI"]
private static let hostingBasePath = "swift-inotify"
private static let skipItems: Set<String> = [".git", ".build", ".swiftpm", "public"] private static let skipItems: Set<String> = [".git", ".build", ".swiftpm", "public"]
// MARK: - Run // MARK: - Run
func run() async throws { func run() async throws {
let noora = Noora() let noora = Noora()
let logger = global.makeLogger(labeled: "swift-inotify.cli.task.generate-docs") let logger = global.makeLogger(labeled: "swift-inotify.cli.task.generate-documentation")
let fileManager = FileManager.default let fileManager = FileManager.default
let projectDirectory = URL(fileURLWithPath: fileManager.currentDirectoryPath) let projectDirectory = URL(fileURLWithPath: fileManager.currentDirectoryPath)
let targets = try await Self.targets(for: projectDirectory)
noora.info("Generating DocC documentation on Linux.") noora.info("Generating DocC documentation on Linux.")
logger.debug("Current directory", metadata: ["current-directory": "\(projectDirectory.path(percentEncoded: false))"]) logger.debug("Current directory", metadata: ["current-directory": "\(projectDirectory.path(percentEncoded: false))", "targets": "\(targets.joined(separator: ", "))"])
let tempDirectory = try copyProject(from: projectDirectory) let tempDirectory = try copyProject(from: projectDirectory)
logger.info("Copied project to temporary directory.", metadata: ["path": "\(tempDirectory.path(percentEncoded: false))"]) logger.info("Copied project to temporary directory.", metadata: ["path": "\(tempDirectory.path(percentEncoded: false))"])
@@ -40,7 +40,7 @@ struct GenerateDocsCommand: AsyncParsableCommand {
} }
try await injectDoccPluginDependency(in: tempDirectory, logger: logger) try await injectDoccPluginDependency(in: tempDirectory, logger: logger)
let script = Self.makeRunScript() let script = Self.makeRunScript(for: targets)
logger.debug("Container script", metadata: ["script": "\(script)"]) logger.debug("Container script", metadata: ["script": "\(script)"])
let dockerResult = try await Subprocess.run( let dockerResult = try await Subprocess.run(
@@ -70,45 +70,58 @@ struct GenerateDocsCommand: AsyncParsableCommand {
} }
try copyResults(from: tempDirectory, to: projectDirectory) 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( noora.success(
.alert("Documentation generated successfully.", .alert("Documentation generated successfully.",
takeaways: Self.targets.map { takeaways: targets.map {
"./public/\($0.lowercased())/" "./public/\($0.lowercased())/"
} }
) )
) )
} }
private static func generateDocsCommand() -> String { private static func generateIndexHTML(templateURL: URL, outputURL: URL) throws {
Self.targets.map { var content = try String(contentsOf: templateURL, encoding: .utf8)
"swift package generate-documentation --target \($0)"
}.joined(separator: " && ")
}
private static func transformDocsForStaticHostingCommand() -> String {
Self.targets.map { target in
let lowercased = target.lowercased()
return "docc process-archive transform-for-static-hosting" +
" $OUTPUTS/\(target).doccarchive" +
" --output-path ./public/\(lowercased)" +
" --hosting-base-path \(Self.hostingBasePath)/\(lowercased)"
}.joined(separator: " && ")
}
private static func outputPathsForDocTargets() -> String {
Self.targets.map { "./public/\($0.lowercased())" }.joined(separator: " ")
}
private static func makeRunScript() -> String {
let generateDocs = Self.generateDocsCommand()
let transformDocs = Self.transformDocsForStaticHostingCommand()
let outputDirs = Self.outputPathsForDocTargets()
return "set -euo pipefail && \(generateDocs)" + let replacements: [(String, String)] = [
" && OUTPUTS=.build/plugins/Swift-DocC/outputs" + ("{{project.name}}", "Swift Inotify"),
" && mkdir -p \(outputDirs)" + ("{{project.tagline}}", "🗂️ Monitor filesystem events on Linux using modern Swift concurrency"),
" && \(transformDocs)" ("{{project.links}}", """
<li><a href="inotify/documentation/inotify/">Inotify</a>: The actual library.</li>\
<li><a href="taskcli/documentation/taskcli/">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 sourcesDirectory = projectDirectory.appending(path: "Sources").path
let testsDirectory = projectDirectory.appending(path: "Tests").path
return try await DoccFinder.getTargetsWithDocumentation(at: sourcesDirectory, testsDirectory)
}
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 // MARK: - Project Copy
@@ -141,32 +154,23 @@ struct GenerateDocsCommand: AsyncParsableCommand {
// MARK: - Dependency Injection // MARK: - Dependency Injection
private func injectDoccPluginDependency(in directory: URL, logger: Logger) async throws { private func injectDoccPluginDependency(in directory: URL, logger: Logger) async throws {
let packageSwiftURL = directory.appending(path: "Package.swift")
let contents = try String(contentsOf: packageSwiftURL, encoding: .utf8)
guard !contents.contains("swift-docc-plugin") else {
logger.info("swift-docc-plugin dependency already present.")
return
}
let result = try await Subprocess.run( let result = try await Subprocess.run(
.name("swift"), .name("swift"),
arguments: [ arguments: [
"package", "--package-path", directory.path(percentEncoded: false), "package", "--package-path", directory.path(percentEncoded: false),
"add-dependency", Self.doccPluginURL, "add-dependency", "--from", Self.doccPluginMinVersion, Self.doccPluginURL
"--from", Self.doccPluginMinVersion,
], ],
) { _ in } ) { _ in }
guard result.terminationStatus.isSuccess else { guard result.terminationStatus.isSuccess else {
throw GenerateDocsError.dependencyInjectionFailed throw GenerateDocumentationError.dependencyInjectionFailed
} }
logger.info("Injected swift-docc-plugin dependency.") logger.info("Injected swift-docc-plugin dependency.")
} }
} }
enum GenerateDocsError: Error, CustomStringConvertible { enum GenerateDocumentationError: Error, CustomStringConvertible {
case dependencyInjectionFailed case dependencyInjectionFailed
var description: String { var description: String {

View File

@@ -22,7 +22,7 @@ The container is started with `--security-opt systempaths=unconfined` so that th
### Generating Documentation ### Generating Documentation
```bash ```bash
swift run task generate-docs swift run task generate-documentation
``` ```
This copies the project to a temporary directory, injects the `swift-docc-plugin` dependency via `swift package add-dependency` (if absent), and runs documentation generation inside a `swift:latest` Docker container. The resulting static sites are written to `./public/inotify/` and `./public/taskcli/`, ready for deployment to GitHub Pages. This copies the project to a temporary directory, injects the `swift-docc-plugin` dependency via `swift package add-dependency` (if absent), and runs documentation generation inside a `swift:latest` Docker container. The resulting static sites are written to `./public/inotify/` and `./public/taskcli/`, ready for deployment to GitHub Pages.
@@ -50,7 +50,7 @@ Docker must be installed and running on the host machine. The container uses the
- ``Command`` - ``Command``
- ``TestCommand`` - ``TestCommand``
- ``GenerateDocsCommand`` - ``GenerateDocumentationCommand``
### Configuration ### Configuration