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
7 changed files with 29 additions and 77 deletions

View File

@@ -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 {
let directoryHandle = try await fileManager.openDirectory(atPath: path) guard let fileInfo = try await fileManager.info(forFileAt: path) else {
for try await childContent in directoryHandle.listContents() { throw DirectoryResolverError.pathNotFound(path)
guard childContent.type == .directory else { continue }
try await body(childContent.path)
if recursive {
try await withSubdirectories(at: childContent.path, recursive: recursive, body: body)
} }
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)
for try await childContent in directoryHandle.listContents(recursive: true) {
guard childContent.type == .directory else { continue }
result.append(childContent.path)
} }
try await directoryHandle.close() try await directoryHandle.close()
return result
} }
} }

View File

@@ -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)

View File

@@ -77,7 +77,9 @@ struct GenerateDocumentationCommand: AsyncParsableCommand {
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: targets.map {
"./public/\($0.lowercased())/"
}
) )
) )
} }

View File

@@ -58,4 +58,4 @@ Docker must be installed and running on the host machine. The container uses the
### Errors ### Errors
- ``GenerateDocumentationError`` - ``GenerateDocsError``

View File

@@ -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])
}
}
}

View File

@@ -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")
} }

View File

@@ -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)