Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .swiftpm/xcode/xcshareddata/xcschemes/spmgraph-Package.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,16 @@
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "SPMGraphConfigSetupTests"
BuildableName = "SPMGraphConfigSetupTests"
BlueprintName = "SPMGraphConfigSetupTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
Expand Down
11 changes: 1 addition & 10 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 9 additions & 8 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,6 @@ let package = Package(
url: "https://github.com/apple/swift-package-manager",
revision: "swift-6.2-RELEASE"
),
.package(
url: "https://github.com/aus-der-Technik/FileMonitor",
revision: "1.2.0"
),
],
targets: [
// MARK: - Functionality
Expand Down Expand Up @@ -93,10 +89,6 @@ let package = Package(
name: "SPMGraphConfigSetup",
dependencies: [
.target(name: "Core"),
.product(
name: "FileMonitor",
package: "FileMonitor"
),
],
resources: [
.copy("Resources")
Expand Down Expand Up @@ -168,6 +160,15 @@ let package = Package(
.enableExperimentalFeature("StrictConcurrency")
]
),
.testTarget(
name: "SPMGraphConfigSetupTests",
dependencies: [
.target(name: "SPMGraphConfigSetup"),
],
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency")
]
),
.testTarget(
name: "SPMGraphDescriptionInterfaceTests",
dependencies: [
Expand Down
163 changes: 156 additions & 7 deletions Sources/FixtureSupport/Package+Fixture.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ import Basics
import Foundation
import PackageModel
import Testing
import Workspace

public extension AbsolutePath {
static var fixturePackagePath: AbsolutePath {
do {
Expand All @@ -38,11 +36,162 @@ public extension AbsolutePath {
}

public func loadFixturePackage() async throws -> Package {
let observability = ObservabilitySystem { print("\($0): \($1)") }
let workspace = try Workspace(forRootPackage: .fixturePackagePath)
let packagePath = AbsolutePath.fixturePackagePath
let manifestPath = packagePath.appending(component: Manifest.filename)
let manifest = Manifest(
displayName: "LintFixturePackage",
packageIdentity: .plain("LintFixturePackage"),
path: manifestPath,
packageKind: .root(packagePath),
packageLocation: packagePath.pathString,
defaultLocalization: nil,
platforms: [],
version: nil,
revision: nil,
toolsVersion: .v6_0,
pkgConfig: nil,
providers: nil,
cLanguageStandard: nil,
cxxLanguageStandard: nil,
swiftLanguageVersions: nil,
dependencies: [],
products: [],
targets: [],
traits: []
)

let modules = try makeFixtureModules(at: packagePath)

return Package(
identity: manifest.packageIdentity,
manifest: manifest,
path: packagePath,
targets: modules,
products: [],
targetSearchPath: packagePath.appending(component: "Sources"),
testTargetSearchPath: packagePath.appending(component: "Tests")
)
}

private func makeFixtureModules(at packagePath: AbsolutePath) throws -> [Module] {
let baseModule = try makeSwiftModule(
name: "BaseModule",
path: packagePath.appending(components: "Sources", "BaseModule"),
type: .library,
dependencies: []
)

let interfaceModule = try makeSwiftModule(
name: "InterfaceModule",
path: packagePath.appending(components: "Sources", "InterfaceModule"),
type: .library,
dependencies: [
.module(baseModule, conditions: [])
]
)

let networkingLiveModule = try makeSwiftModule(
name: "NetworkingLive",
path: packagePath.appending(components: "Sources", "NetworkingLive"),
type: .library,
dependencies: [
.module(interfaceModule, conditions: [])
]
)

let featureModule = try makeSwiftModule(
name: "FeatureModule",
path: packagePath.appending(components: "Sources", "FeatureModule"),
type: .library,
dependencies: [
.module(interfaceModule, conditions: []),
.module(networkingLiveModule, conditions: [])
]
)

let storageLiveModule = try makeSwiftModule(
name: "StorageLive",
path: packagePath.appending(components: "Sources", "StorageLive"),
type: .library,
dependencies: [
.module(networkingLiveModule, conditions: [])
]
)

return try await workspace.loadRootPackage(
at: .fixturePackagePath,
observabilityScope: observability.topScope
let moduleWithUnusedDep = try makeSwiftModule(
name: "ModuleWithUnusedDep",
path: packagePath.appending(components: "Sources", "ModuleWithUnusedDep"),
type: .library,
dependencies: [
.module(baseModule, conditions: []),
.module(interfaceModule, conditions: [])
]
)

let baseModuleTests = try makeSwiftModule(
name: "BaseModuleTests",
path: packagePath.appending(components: "Tests", "BaseModuleTests"),
type: .test,
dependencies: [
.module(baseModule, conditions: [])
]
)

let moduleWithUnusedDepTests = try makeSwiftModule(
name: "ModuleWithUnusedDepTests",
path: packagePath.appending(components: "Tests", "ModuleWithUnusedDepTests"),
type: .test,
dependencies: [
.module(moduleWithUnusedDep, conditions: [])
]
)

return [
baseModule,
interfaceModule,
featureModule,
networkingLiveModule,
storageLiveModule,
moduleWithUnusedDep,
baseModuleTests,
moduleWithUnusedDepTests
]
}

private func makeSwiftModule(
name: String,
path: AbsolutePath,
type: Module.Kind,
dependencies: [Module.Dependency]
) throws -> SwiftModule {
let sources = Sources(
paths: swiftSourceFiles(in: path),
root: path
)
return SwiftModule(
name: name,
type: type,
path: path,
sources: sources,
dependencies: dependencies,
packageAccess: false,
usesUnsafeFlags: false,
implicit: false
)
}

private func swiftSourceFiles(in path: AbsolutePath) -> [AbsolutePath] {
guard let enumerator = FileManager.default.enumerator(at: path.asURL, includingPropertiesForKeys: nil) else {
return []
}

var files: [AbsolutePath] = []
while let url = enumerator.nextObject() as? URL {
guard url.pathExtension == "swift" else { continue }
if let filePath = try? AbsolutePath(validating: url.path) {
files.append(filePath)
}
}

return files
}
132 changes: 132 additions & 0 deletions Sources/SPMGraphConfigSetup/FileMonitor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import Foundation

enum FileChange: Sendable {
case changed(URL)
case added(URL)
case deleted(URL)
}

enum FileMonitorError: Error {
case invalidDirectory(URL)
}

final class FileMonitor {
private struct FileState: Equatable {
let modificationDate: Date?
let fileSize: Int?
}

private let directory: URL
private let pollInterval: DispatchTimeInterval
private var snapshot: [String: FileState]
private var continuation: AsyncStream<FileChange>.Continuation?
private let queue: DispatchQueue
private var timer: DispatchSourceTimer?

let stream: AsyncStream<FileChange>

init(directory: URL, pollInterval: DispatchTimeInterval = .milliseconds(250)) throws {
guard directory.isFileURL else {
throw FileMonitorError.invalidDirectory(directory)
}

var isDirectory: ObjCBool = false
guard
FileManager.default.fileExists(atPath: directory.path, isDirectory: &isDirectory),
isDirectory.boolValue
else {
throw FileMonitorError.invalidDirectory(directory)
}

self.directory = directory
self.pollInterval = pollInterval
self.snapshot = try Self.snapshot(directory: directory)
self.queue = DispatchQueue(label: "spmgraph.filemonitor")

var continuation: AsyncStream<FileChange>.Continuation?
self.stream = AsyncStream { continuation = $0 }
self.continuation = continuation
}

func start() throws {
guard timer == nil else {
return
}

let timer = DispatchSource.makeTimerSource(queue: queue)
timer.schedule(deadline: .now() + pollInterval, repeating: pollInterval)
timer.setEventHandler { [weak self] in
guard let self else {
return
}

do {
let currentSnapshot = try Self.snapshot(directory: self.directory)
Self.emitChanges(from: self.snapshot, to: currentSnapshot, continuation: self.continuation)
self.snapshot = currentSnapshot
} catch {
self.continuation?.finish()
timer.cancel()
}
}

self.timer = timer
timer.resume()
}

deinit {
timer?.cancel()
continuation?.finish()
}
}

private extension FileMonitor {
private static func snapshot(directory: URL) throws -> [String: FileState] {
let contents = try FileManager.default.contentsOfDirectory(
at: directory,
includingPropertiesForKeys: [
.contentModificationDateKey,
.fileSizeKey,
.isRegularFileKey
],
options: [.skipsSubdirectoryDescendants]
)

var snapshot: [String: FileState] = [:]
snapshot.reserveCapacity(contents.count)

for url in contents {
let values = try url.resourceValues(forKeys: [.contentModificationDateKey, .fileSizeKey, .isRegularFileKey])
guard values.isRegularFile == true else {
continue
}

snapshot[url.path] = FileState(
modificationDate: values.contentModificationDate,
fileSize: values.fileSize
)
}

return snapshot
}

private static func emitChanges(
from previousSnapshot: [String: FileState],
to currentSnapshot: [String: FileState],
continuation: AsyncStream<FileChange>.Continuation?
) {
for (path, state) in currentSnapshot {
if let previousState = previousSnapshot[path] {
if previousState != state {
continuation?.yield(.changed(URL(fileURLWithPath: path)))
}
} else {
continuation?.yield(.added(URL(fileURLWithPath: path)))
}
}

for path in previousSnapshot.keys where currentSnapshot[path] == nil {
continuation?.yield(.deleted(URL(fileURLWithPath: path)))
}
}
}
Loading
Loading