Compare commits
5 Commits
1.0.0
...
29e35ae386
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29e35ae386 | ||
|
|
672c4b85f2 | ||
|
|
295b701be1 | ||
|
|
984b0dd29c | ||
|
|
39fe5c9237 |
1
.spi.yml
1
.spi.yml
@@ -2,4 +2,3 @@ version: 1
|
|||||||
builder:
|
builder:
|
||||||
configs:
|
configs:
|
||||||
- documentation_targets: [Inotify, TaskCLI]
|
- documentation_targets: [Inotify, TaskCLI]
|
||||||
platform: Linux
|
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ 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")
|
||||||
]
|
]
|
||||||
|
|||||||
22
README.md
22
README.md
@@ -75,24 +75,6 @@ try await inotify.addWatchWithAutomaticSubtreeWatching(
|
|||||||
|
|
||||||
This is the most convenient option when you need full coverage of a growing directory tree.
|
This is the most convenient option when you need full coverage of a growing directory tree.
|
||||||
|
|
||||||
## Excluding Items
|
|
||||||
|
|
||||||
You can tell the `Inotify` actor to ignore certain file or directory names. Excluded names are skipped during recursive directory resolution (so no watch is installed on them) and silently dropped from the event stream:
|
|
||||||
|
|
||||||
```swift
|
|
||||||
let inotify = try Inotify()
|
|
||||||
|
|
||||||
// Ignore version-control and build directories
|
|
||||||
await inotify.exclude(names: ".git", "node_modules", ".build")
|
|
||||||
|
|
||||||
try await inotify.addWatchWithAutomaticSubtreeWatching(
|
|
||||||
forDirectory: "/home/user/project",
|
|
||||||
mask: [.create, .modify, .delete]
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Use `isExcluded(_:)` to check whether a name is currently on the exclusion list.
|
|
||||||
|
|
||||||
## Event Masks
|
## Event Masks
|
||||||
|
|
||||||
`InotifyEventMask` is an `OptionSet` that mirrors the native inotify flags. You can combine them freely.
|
`InotifyEventMask` is an `OptionSet` that mirrors the native inotify flags. You can combine them freely.
|
||||||
@@ -131,9 +113,7 @@ try inotify.removeWatch(wd)
|
|||||||
|
|
||||||
## Build Tool
|
## Build Tool
|
||||||
|
|
||||||
The package ships with a `task` executable (the `TaskCLI` target) that serves as the project's build tool. It automates running tests and generating documentation inside Linux Docker containers, so you can validate everything on the correct platform even when developing on macOS.
|
The package ships with a `task` executable (the `TaskCLI` target) that serves as the project's build tool. It spins up a Docker container running `swift:latest` on Linux and executes the full test suite inside it, so you can validate everything on the correct platform even when developing on macOS.
|
||||||
|
|
||||||
### Tests
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
swift run task test
|
swift run task test
|
||||||
|
|||||||
@@ -3,35 +3,42 @@ import _NIOFileSystem
|
|||||||
public struct DirectoryResolver {
|
public struct DirectoryResolver {
|
||||||
static let fileManager = FileSystem.shared
|
static let fileManager = FileSystem.shared
|
||||||
|
|
||||||
public static func resolve(_ paths: String..., excluding itemNames: Set<String> = []) async throws -> [FilePath] {
|
public static func resolve(_ paths: String...) async throws -> [FilePath] {
|
||||||
try await Self.resolve(paths, excluding: itemNames)
|
try await Self.resolve(paths)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func resolve(_ paths: [String], excluding itemNames: Set<String> = []) async throws -> [FilePath] {
|
static func resolve(_ paths: [String]) async throws -> [FilePath] {
|
||||||
var resolved: [FilePath] = []
|
var resolved: [FilePath] = []
|
||||||
|
|
||||||
for path in paths {
|
for path in paths {
|
||||||
let path = FilePath(path)
|
let itemPath = FilePath(path)
|
||||||
resolved.append(path)
|
try await Self.ensure(itemPath, is: .directory)
|
||||||
try await withSubdirectories(at: path, recursive: true) { subdirectoryPath in
|
|
||||||
guard let basename = subdirectoryPath.lastComponent?.description else { return }
|
let allDirectoriesIncludingSelf = try await getAllSubdirectoriesAndSelf(at: itemPath)
|
||||||
guard !itemNames.contains(basename) else { return }
|
resolved.append(contentsOf: allDirectoriesIncludingSelf)
|
||||||
resolved.append(subdirectoryPath)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolved
|
return resolved
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func withSubdirectories(at path: FilePath, recursive: Bool = false, body: (FilePath) async throws -> Void) async throws {
|
private static func ensure(_ path: FilePath, is fileType: FileType) async throws {
|
||||||
|
guard let fileInfo = try await fileManager.info(forFileAt: path) else {
|
||||||
|
throw DirectoryResolverError.pathNotFound(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard fileInfo.type == fileType else {
|
||||||
|
throw DirectoryResolverError.pathIsNoDirectory(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func getAllSubdirectoriesAndSelf(at path: FilePath) async throws -> [FilePath] {
|
||||||
|
var result: [FilePath] = []
|
||||||
let directoryHandle = try await fileManager.openDirectory(atPath: path)
|
let directoryHandle = try await fileManager.openDirectory(atPath: path)
|
||||||
for try await childContent in directoryHandle.listContents() {
|
for try await childContent in directoryHandle.listContents(recursive: true) {
|
||||||
guard childContent.type == .directory else { continue }
|
guard childContent.type == .directory else { continue }
|
||||||
try await body(childContent.path)
|
result.append(childContent.path)
|
||||||
if recursive {
|
|
||||||
try await withSubdirectories(at: childContent.path, recursive: recursive, body: body)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
try await directoryHandle.close()
|
try await directoryHandle.close()
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ Beyond single-directory watches, the library provides two higher-level methods f
|
|||||||
- ``Inotify/Inotify/addRecursiveWatch(forDirectory:mask:)`` installs watches on every existing subdirectory at setup time.
|
- ``Inotify/Inotify/addRecursiveWatch(forDirectory:mask:)`` installs watches on every existing subdirectory at setup time.
|
||||||
- ``Inotify/Inotify/addWatchWithAutomaticSubtreeWatching(forDirectory:mask:)`` does the same **and** automatically watches subdirectories that are created after setup.
|
- ``Inotify/Inotify/addWatchWithAutomaticSubtreeWatching(forDirectory:mask:)`` does the same **and** automatically watches subdirectories that are created after setup.
|
||||||
|
|
||||||
You can also exclude certain file or directory names so that they are skipped during directory resolution and silently dropped from the event stream. See ``Inotify/Inotify/exclude(names:)`` and <doc:WatchingDirectoryTrees> for details.
|
|
||||||
|
|
||||||
All public types conform to `Sendable`, so they can be safely passed across concurrency boundaries.
|
All public types conform to `Sendable`, so they can be safely passed across concurrency boundaries.
|
||||||
|
|
||||||
## Topics
|
## Topics
|
||||||
@@ -32,10 +30,6 @@ All public types conform to `Sendable`, so they can be safely passed across conc
|
|||||||
- ``InotifyEvent``
|
- ``InotifyEvent``
|
||||||
- ``InotifyEventMask``
|
- ``InotifyEventMask``
|
||||||
|
|
||||||
### Articles
|
|
||||||
|
|
||||||
- <doc:WatchingDirectoryTrees>
|
|
||||||
|
|
||||||
### Errors
|
### Errors
|
||||||
|
|
||||||
- ``InotifyError``
|
- ``InotifyError``
|
||||||
|
|||||||
@@ -33,22 +33,6 @@ let descriptors = try await inotify.addWatchWithAutomaticSubtreeWatching(
|
|||||||
|
|
||||||
Internally this listens for `CREATE` events carrying the ``InotifyEventMask/isDir`` flag and installs a new watch with the same mask whenever a subdirectory appears.
|
Internally this listens for `CREATE` events carrying the ``InotifyEventMask/isDir`` flag and installs a new watch with the same mask whenever a subdirectory appears.
|
||||||
|
|
||||||
### Excluding Directories
|
|
||||||
|
|
||||||
When watching large trees you often want to skip certain subdirectories entirely — version-control metadata, build artefacts, dependency caches, and so on. Call ``Inotify/Inotify/exclude(names:)`` **before** adding a recursive or automatic-subtree watch:
|
|
||||||
|
|
||||||
```swift
|
|
||||||
let inotify = try Inotify()
|
|
||||||
await inotify.exclude(names: ".git", "node_modules", ".build")
|
|
||||||
|
|
||||||
try await inotify.addWatchWithAutomaticSubtreeWatching(
|
|
||||||
forDirectory: "/home/user/project",
|
|
||||||
mask: .allEvents
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Excluded names are matched against the last path component of each directory during resolution and are also filtered from the event stream, so you never receive events for items whose name is on the exclusion list.
|
|
||||||
|
|
||||||
### Choosing the Right Method
|
### Choosing the Right Method
|
||||||
|
|
||||||
| Method | Covers existing subdirectories | Covers new subdirectories |
|
| Method | Covers existing subdirectories | Covers new subdirectories |
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import CInotify
|
|||||||
|
|
||||||
public actor Inotify {
|
public actor Inotify {
|
||||||
private let fd: CInt
|
private let fd: CInt
|
||||||
private var excludedItemNames: Set<String> = []
|
|
||||||
private var watches = InotifyWatchManager()
|
private var watches = InotifyWatchManager()
|
||||||
private var eventReader: any DispatchSourceRead
|
private var eventReader: any DispatchSourceRead
|
||||||
private var eventStream: AsyncStream<RawInotifyEvent>
|
private var eventStream: AsyncStream<RawInotifyEvent>
|
||||||
@@ -19,24 +18,6 @@ public actor Inotify {
|
|||||||
(self.eventReader, self.eventStream) = Self.createEventReader(forFileDescriptor: fd)
|
(self.eventReader, self.eventStream) = Self.createEventReader(forFileDescriptor: fd)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func isExcluded(_ name: String) -> Bool {
|
|
||||||
self.excludedItemNames.contains(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func exclude(name: String) {
|
|
||||||
self.excludedItemNames.insert(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func exclude(names: String...) {
|
|
||||||
self.exclude(names: names)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func exclude(names: [String]) {
|
|
||||||
for name in names {
|
|
||||||
self.excludedItemNames.insert(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
public func addWatch(path: String, mask: InotifyEventMask) throws -> CInt {
|
public func addWatch(path: String, mask: InotifyEventMask) throws -> CInt {
|
||||||
let wd = inotify_add_watch(self.fd, path, mask.rawValue)
|
let wd = inotify_add_watch(self.fd, path, mask.rawValue)
|
||||||
@@ -49,7 +30,7 @@ public actor Inotify {
|
|||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
public func addRecursiveWatch(forDirectory path: String, mask: InotifyEventMask) async throws -> [CInt] {
|
public func addRecursiveWatch(forDirectory path: String, mask: InotifyEventMask) async throws -> [CInt] {
|
||||||
let directoryPaths = try await DirectoryResolver.resolve(path, excluding: self.excludedItemNames)
|
let directoryPaths = try await DirectoryResolver.resolve(path)
|
||||||
var result: [CInt] = []
|
var result: [CInt] = []
|
||||||
for path in directoryPaths {
|
for path in directoryPaths {
|
||||||
let wd = try self.addWatch(path: path.string, mask: mask)
|
let wd = try self.addWatch(path: path.string, mask: mask)
|
||||||
@@ -78,7 +59,6 @@ public actor Inotify {
|
|||||||
|
|
||||||
private func transform(_ rawEvent: RawInotifyEvent) async -> InotifyEvent? {
|
private func transform(_ rawEvent: RawInotifyEvent) async -> InotifyEvent? {
|
||||||
guard let path = self.watches.path(forId: rawEvent.watchDescriptor) else { return nil }
|
guard let path = self.watches.path(forId: rawEvent.watchDescriptor) else { return nil }
|
||||||
guard !self.excludedItemNames.contains(rawEvent.name) else { return nil }
|
|
||||||
let event = InotifyEvent.init(from: rawEvent, inDirectory: path)
|
let event = InotifyEvent.init(from: rawEvent, inDirectory: path)
|
||||||
await self.addWatchInCaseOfAutomaticSubtreeWatching(event)
|
await self.addWatchInCaseOfAutomaticSubtreeWatching(event)
|
||||||
return InotifyEvent.init(from: rawEvent, inDirectory: path)
|
return InotifyEvent.init(from: rawEvent, inDirectory: path)
|
||||||
|
|||||||
@@ -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, GenerateDocumentationCommand.self]
|
subcommands: [TestCommand.self, GenerateDocsCommand.self]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,10 +5,10 @@ import Logging
|
|||||||
import Noora
|
import Noora
|
||||||
import Subprocess
|
import Subprocess
|
||||||
|
|
||||||
struct GenerateDocumentationCommand: AsyncParsableCommand {
|
struct GenerateDocsCommand: AsyncParsableCommand {
|
||||||
static let configuration = CommandConfiguration(
|
static let configuration = CommandConfiguration(
|
||||||
commandName: "generate-documentation",
|
commandName: "generate-docs",
|
||||||
abstract: "Generate DocC documentation of all targets inside a Linux container.",
|
abstract: "Generate DocC documentation inside a Linux container.",
|
||||||
aliases: ["gd"],
|
aliases: ["gd"],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,20 +16,20 @@ struct GenerateDocumentationCommand: 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-documentation")
|
let logger = global.makeLogger(labeled: "swift-inotify.cli.task.generate-docs")
|
||||||
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))", "targets": "\(targets.joined(separator: ", "))"])
|
logger.debug("Current directory", metadata: ["current-directory": "\(projectDirectory.path(percentEncoded: false))"])
|
||||||
|
|
||||||
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 GenerateDocumentationCommand: AsyncParsableCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try await injectDoccPluginDependency(in: tempDirectory, logger: logger)
|
try await injectDoccPluginDependency(in: tempDirectory, logger: logger)
|
||||||
let script = Self.makeRunScript(for: targets)
|
let script = Self.makeRunScript()
|
||||||
|
|
||||||
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,58 +70,47 @@ struct GenerateDocumentationCommand: 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: ["Start a local web server with ./public as document root, i.e. with python3 -m http.server to browse the documentation."]
|
takeaways: Self.targets.map {
|
||||||
|
"./public/\($0.lowercased())/"
|
||||||
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func generateIndexHTML(templateURL: URL, outputURL: URL) throws {
|
private static func generateDocsCommand() -> String {
|
||||||
var content = try String(contentsOf: templateURL, encoding: .utf8)
|
Self.targets.map {
|
||||||
|
"swift package generate-documentation --target \($0)"
|
||||||
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: " && ")
|
}.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)" +
|
||||||
|
" && OUTPUTS=.build/plugins/Swift-DocC/outputs" +
|
||||||
|
" && mkdir -p \(outputDirs)" +
|
||||||
|
" && \(transformDocs)"
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Project Copy
|
// MARK: - Project Copy
|
||||||
|
|
||||||
private func copyProject(from source: URL) throws -> URL {
|
private func copyProject(from source: URL) throws -> URL {
|
||||||
@@ -152,23 +141,32 @@ struct GenerateDocumentationCommand: 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", "--from", Self.doccPluginMinVersion, Self.doccPluginURL
|
"add-dependency", Self.doccPluginURL,
|
||||||
|
"--from", Self.doccPluginMinVersion,
|
||||||
],
|
],
|
||||||
) { _ in }
|
) { _ in }
|
||||||
|
|
||||||
guard result.terminationStatus.isSuccess else {
|
guard result.terminationStatus.isSuccess else {
|
||||||
throw GenerateDocumentationError.dependencyInjectionFailed
|
throw GenerateDocsError.dependencyInjectionFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Injected swift-docc-plugin dependency.")
|
logger.info("Injected swift-docc-plugin dependency.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum GenerateDocumentationError: Error, CustomStringConvertible {
|
enum GenerateDocsError: Error, CustomStringConvertible {
|
||||||
case dependencyInjectionFailed
|
case dependencyInjectionFailed
|
||||||
|
|
||||||
var description: String {
|
var description: String {
|
||||||
@@ -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-documentation
|
swift run task generate-docs
|
||||||
```
|
```
|
||||||
|
|
||||||
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``
|
||||||
- ``GenerateDocumentationCommand``
|
- ``GenerateDocsCommand``
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
@@ -58,4 +58,4 @@ Docker must be installed and running on the host machine. The container uses the
|
|||||||
|
|
||||||
### Errors
|
### Errors
|
||||||
|
|
||||||
- ``GenerateDocumentationError``
|
- ``GenerateDocsError``
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import Testing
|
|
||||||
@testable import Inotify
|
|
||||||
|
|
||||||
@Suite("Directory Resolver")
|
|
||||||
struct DirectoryResolverTests {
|
|
||||||
@Test func listsDirectoryTree() async throws {
|
|
||||||
try await withTempDir { dir in
|
|
||||||
let subDirectory = "\(dir)/Subfolder/Folder 01"
|
|
||||||
try FileManager.default.createDirectory(atPath: subDirectory, withIntermediateDirectories: true)
|
|
||||||
let directories = try await DirectoryResolver.resolve(dir)
|
|
||||||
|
|
||||||
#expect(directories.count == 3)
|
|
||||||
#expect(directories.map { $0.description } == [dir, "\(dir)/Subfolder", subDirectory])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,24 +21,6 @@ struct RecursiveEventTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func ignoresFileCreationInIgnoredSubfolder() async throws {
|
|
||||||
try await withTempDir { dir in
|
|
||||||
let subDirectory = "\(dir)/Subfolder"
|
|
||||||
let filepath = "\(subDirectory)/modify-target.txt"
|
|
||||||
try FileManager.default.createDirectory(atPath: subDirectory, withIntermediateDirectories: true)
|
|
||||||
|
|
||||||
let events = try await getEventsForTrigger(
|
|
||||||
in: dir,
|
|
||||||
mask: [.create],
|
|
||||||
recursive: .recursive,
|
|
||||||
exclude: ["Subfolder"]
|
|
||||||
) { _ in try createFile(at: "\(filepath)", contents: "hello") }
|
|
||||||
|
|
||||||
let createEvent = events.first { $0.mask.contains(.create) && $0.path.string == filepath }
|
|
||||||
#expect(createEvent == nil, "Did not expect CREATE for '\(filepath)', got: \(events)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test func newSubfoldersOfRecursiveWatchAreAutomaticallyWatchedToo() async throws {
|
@Test func newSubfoldersOfRecursiveWatchAreAutomaticallyWatchedToo() async throws {
|
||||||
try await withTempDir { dir in
|
try await withTempDir { dir in
|
||||||
let subDirectory = "\(dir)/Subfolder"
|
let subDirectory = "\(dir)/Subfolder"
|
||||||
@@ -50,7 +32,7 @@ struct RecursiveEventTests {
|
|||||||
recursive: .withAutomaticSubtreeWatching
|
recursive: .withAutomaticSubtreeWatching
|
||||||
) { _ in
|
) { _ in
|
||||||
try FileManager.default.createDirectory(atPath: subDirectory, withIntermediateDirectories: true)
|
try FileManager.default.createDirectory(atPath: subDirectory, withIntermediateDirectories: true)
|
||||||
try await Task.sleep(for: .milliseconds(400))
|
try await Task.sleep(for: .milliseconds(200))
|
||||||
try createFile(at: "\(filepath)", contents: "hello")
|
try createFile(at: "\(filepath)", contents: "hello")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,9 @@ func getEventsForTrigger(
|
|||||||
in dir: String,
|
in dir: String,
|
||||||
mask: InotifyEventMask,
|
mask: InotifyEventMask,
|
||||||
recursive: RecursivKind = .nonrecursive,
|
recursive: RecursivKind = .nonrecursive,
|
||||||
exclude: [String] = [],
|
|
||||||
trigger: @escaping (String) async throws -> Void,
|
trigger: @escaping (String) async throws -> Void,
|
||||||
) async throws -> [InotifyEvent] {
|
) async throws -> [InotifyEvent] {
|
||||||
let watcher = try Inotify()
|
let watcher = try Inotify()
|
||||||
await watcher.exclude(names: exclude)
|
|
||||||
switch recursive {
|
switch recursive {
|
||||||
case .nonrecursive:
|
case .nonrecursive:
|
||||||
try await watcher.addWatch(path: dir, mask: mask)
|
try await watcher.addWatch(path: dir, mask: mask)
|
||||||
|
|||||||
Reference in New Issue
Block a user