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.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
.build
|
||||
public
|
||||
|
||||
@@ -47,6 +47,7 @@ let package = Package(
|
||||
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
||||
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
|
||||
.product(name: "Logging", package: "swift-log"),
|
||||
.product(name: "_NIOFileSystem", package: "swift-nio"),
|
||||
.product(name: "Subprocess", package: "swift-subprocess"),
|
||||
.product(name: "Noora", package: "Noora")
|
||||
]
|
||||
|
||||
@@ -4,6 +4,6 @@ import ArgumentParser
|
||||
struct Command: AsyncParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
abstract: "Project tasks of Astzweig's Swift Inotify project.",
|
||||
subcommands: [TestCommand.self]
|
||||
subcommands: [TestCommand.self, GenerateDocumentationCommand.self]
|
||||
)
|
||||
}
|
||||
|
||||
37
Sources/TaskCLI/DoccFinder.swift
Normal file
37
Sources/TaskCLI/DoccFinder.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
180
Sources/TaskCLI/GenerateDocumentation Command.swift
Normal file
180
Sources/TaskCLI/GenerateDocumentation Command.swift
Normal file
@@ -0,0 +1,180 @@
|
||||
import ArgumentParser
|
||||
import AsyncAlgorithms
|
||||
import Foundation
|
||||
import Logging
|
||||
import Noora
|
||||
import Subprocess
|
||||
|
||||
struct GenerateDocumentationCommand: AsyncParsableCommand {
|
||||
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 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)"])
|
||||
let dockerResult = try await Subprocess.run(
|
||||
.name("docker"),
|
||||
arguments: [
|
||||
"run", "--rm",
|
||||
"-v", "\(tempDirectory.path(percentEncoded: false)):/code",
|
||||
"--platform", "linux/arm64",
|
||||
"-w", "/code",
|
||||
"swift:latest",
|
||||
"/bin/bash", "-c", script,
|
||||
],
|
||||
preferredBufferSize: 10,
|
||||
) { execution, standardInput, standardOutput, standardError in
|
||||
print("")
|
||||
let stdout = standardOutput.lines()
|
||||
let stderr = standardError.lines()
|
||||
for try await line in merge(stdout, stderr) {
|
||||
noora.passthrough("\(line)")
|
||||
}
|
||||
print("")
|
||||
}
|
||||
|
||||
guard dockerResult.terminationStatus.isSuccess else {
|
||||
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="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
|
||||
|
||||
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 result = try await Subprocess.run(
|
||||
.name("swift"),
|
||||
arguments: [
|
||||
"package", "--package-path", directory.path(percentEncoded: false),
|
||||
"add-dependency", "--from", Self.doccPluginMinVersion, Self.doccPluginURL
|
||||
],
|
||||
) { _ in }
|
||||
|
||||
guard result.terminationStatus.isSuccess else {
|
||||
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."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ The build tool for the Swift Inotify project.
|
||||
|
||||
## Overview
|
||||
|
||||
`TaskCLI` is a small command-line executable (exposed as `task` in `Package.swift`) that automates project-level workflows. Its primary purpose is running the integration test suite inside a Linux Docker container, so you can validate the inotify-dependent code on the correct platform even when developing on macOS.
|
||||
`TaskCLI` is a small command-line executable (exposed as `task` in `Package.swift`) that automates project-level workflows. Its primary purpose is running integration tests and generating documentation inside Linux Docker containers, so you can validate inotify-dependent code on the correct platform even when developing on macOS.
|
||||
|
||||
### Running the Tests
|
||||
|
||||
@@ -19,6 +19,16 @@ This launches a `swift:latest` Docker container with the repository mounted at `
|
||||
|
||||
The container is started with `--security-opt systempaths=unconfined` so that the limit tests can write to `/proc/sys/fs/inotify/*`.
|
||||
|
||||
### Generating Documentation
|
||||
|
||||
```bash
|
||||
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.
|
||||
|
||||
The working tree is never modified — all changes happen in the temporary copy, which is cleaned up automatically.
|
||||
|
||||
### Verbosity
|
||||
|
||||
Pass one or more `-v` flags to increase log output:
|
||||
@@ -40,7 +50,12 @@ Docker must be installed and running on the host machine. The container uses the
|
||||
|
||||
- ``Command``
|
||||
- ``TestCommand``
|
||||
- ``GenerateDocumentationCommand``
|
||||
|
||||
### Configuration
|
||||
|
||||
- ``GlobalOptions``
|
||||
|
||||
### Errors
|
||||
|
||||
- ``GenerateDocumentationError``
|
||||
|
||||
Reference in New Issue
Block a user