Skip to content

Commit

Permalink
refactor: migrate used subset of swift-glob to FileSystem (#82)
Browse files Browse the repository at this point in the history
  • Loading branch information
fortmarek authored Nov 4, 2024
1 parent 10220ee commit c23b377
Show file tree
Hide file tree
Showing 13 changed files with 1,453 additions and 55 deletions.
9 changes: 0 additions & 9 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,6 @@
"version" : "1.1.0"
}
},
{
"identity" : "swift-glob",
"kind" : "remoteSourceControl",
"location" : "https://github.com/tuist/swift-glob",
"state" : {
"revision" : "6e828ca92cb8b31a15f31bdd4edccc6d30017ee3",
"version" : "0.3.9"
}
},
{
"identity" : "swift-log",
"kind" : "remoteSourceControl",
Expand Down
19 changes: 14 additions & 5 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// swift-tools-version: 5.8.1
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription
@preconcurrency import PackageDescription

#if TUIST
import ProjectDescription
Expand Down Expand Up @@ -29,17 +29,14 @@ let package = Package(
.package(url: "https://github.com/apple/swift-nio", .upToNextMajor(from: "2.76.1")),
.package(url: "https://github.com/apple/swift-log", .upToNextMajor(from: "1.6.1")),
.package(url: "https://github.com/weichsel/ZIPFoundation", .upToNextMajor(from: "0.9.19")),
// We are depending on a fork as swift-glob currently can't handle some scenario that we need in tuist/tuist.
// For example, the package currently goes through all directories regradless of whether that's necessary.'
.package(url: "https://github.com/tuist/swift-glob", .upToNextMajor(from: "0.3.9")),
],
targets: [
.target(
name: "FileSystem",
dependencies: [
"Glob",
.product(name: "_NIOFileSystem", package: "swift-nio"),
.product(name: "Path", package: "Path"),
.product(name: "Glob", package: "swift-glob"),
.product(name: "Logging", package: "swift-log"),
.product(name: "ZIPFoundation", package: "ZIPFoundation"),
],
Expand All @@ -53,5 +50,17 @@ let package = Package(
"FileSystem",
]
),
.target(
name: "Glob",
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency"),
]
),
.testTarget(
name: "GlobTests",
dependencies: [
"Glob",
]
),
]
)
114 changes: 73 additions & 41 deletions Project.swift
Original file line number Diff line number Diff line change
@@ -1,44 +1,76 @@
import ProjectDescription

let project = Project(name: "FileSystem", settings: .settings(base: ["SWIFT_STRICT_CONCURRENCY": "complete"]), targets: [
.target(
name: "FileSystem",
destinations: .macOS,
product: .staticFramework,
bundleId: "io.tuist.FileSystem",
deploymentTargets: .macOS("13.0"),
sources: [
"Sources/FileSystem/**/*.swift",
],
dependencies: [
.external(name: "Glob"),
.external(name: "_NIOFileSystem"),
.external(name: "Logging"),
.external(name: "Path"),
.external(name: "ZIPFoundation"),
],
settings: .settings(
base: ["GENERATE_MASTER_OBJECT_FILE": "YES", "OTHER_LDFLAGS": "$(inherited) -ObjC"],
configurations: [
.debug(name: .debug, settings: [
"SWIFT_ACTIVE_COMPILATION_CONDITIONS": "$(inherited) MOCKING",
]),
.release(name: .release, settings: [:]),
let project = Project(
name: "FileSystem",
settings: .settings(base: ["SWIFT_STRICT_CONCURRENCY": "complete"]),
targets: [
.target(
name: "FileSystem",
destinations: .macOS,
product: .staticFramework,
bundleId: "io.tuist.FileSystem",
deploymentTargets: .macOS("13.0"),
sources: [
"Sources/FileSystem/**/*.swift",
],
dependencies: [
.target(name: "Glob"),
.external(name: "_NIOFileSystem"),
.external(name: "Logging"),
.external(name: "Path"),
.external(name: "ZIPFoundation"),
],
settings: .settings(
base: ["GENERATE_MASTER_OBJECT_FILE": "YES", "OTHER_LDFLAGS": "$(inherited) -ObjC"],
configurations: [
.debug(name: .debug, settings: [
"SWIFT_ACTIVE_COMPILATION_CONDITIONS": "$(inherited) MOCKING",
]),
.release(name: .release, settings: [:]),
]
)
),
.target(
name: "FileSystemTests",
destinations: .macOS,
product: .unitTests,
bundleId: "io.tuist.FileSystemTests",
deploymentTargets: .macOS("13.0"),
sources: [
"Tests/FileSystemTests/**/*.swift",
],
dependencies: [
.target(name: "FileSystem"),
],
settings: .settings(base: ["GENERATE_MASTER_OBJECT_FILE": "YES", "OTHER_LDFLAGS": "$(inherited) -ObjC"])
),
.target(
name: "Glob",
destinations: .macOS,
product: .staticFramework,
bundleId: "io.tuist.Glob",
deploymentTargets: .macOS("13.0"),
sources: [
"Sources/Glob/**/*.swift",
],
settings: .settings(base: [
"GENERATE_MASTER_OBJECT_FILE": "YES",
"OTHER_LDFLAGS": "$(inherited) -ObjC",
"SWIFT_STRICT_CONCURRENCY": "complete",
])
),
.target(
name: "GlobTests",
destinations: .macOS,
product: .unitTests,
bundleId: "io.tuist.FileSystemTests",
deploymentTargets: .macOS("13.0"),
sources: [
"Tests/GlobTests/**/*.swift",
],
dependencies: [
.target(name: "Glob"),
]
)
),
.target(
name: "FileSystemTests",
destinations: .macOS,
product: .unitTests,
bundleId: "io.tuist.FileSystemTests",
deploymentTargets: .macOS("13.0"),
sources: [
"Tests/FileSystemTests/**/*.swift",
],
dependencies: [
.target(name: "FileSystem"),
],
settings: .settings(base: ["GENERATE_MASTER_OBJECT_FILE": "YES", "OTHER_LDFLAGS": "$(inherited) -ObjC"])
),
])
),
]
)
206 changes: 206 additions & 0 deletions Sources/Glob/GlobSearch.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import Foundation

/// The result of a custom matcher for searching directory components
public struct MatchResult {
/// When true, the url will be added to the output
var matches: Bool
/// When true, the descendents of a directory will be skipped entirely
///
/// This has no effect if the url is not a directory.
var skipDescendents: Bool
}

/// Recursively search the contents of a directory, filtering by the provided patterns
///
/// Searching is done asynchronously, with each subdirectory searched in parallel. Results are emitted as they are found.
///
/// The results are returned as they are matched and do not have a consistent order to them. If you need the results sorted, wait
/// for the entire search to complete and then sort the results.
///
/// - Parameters:
/// - baseURL: The directory to search, defaults to the current working directory.
/// - include: When provided, only includes results that match these patterns.
/// - exclude: When provided, ignore results that match these patterns. If a directory matches an exclude pattern, none of it's
/// descendents will be matched.
/// - keys: An array of keys that identify the properties that you want pre-fetched for each returned url. The values for these
/// keys are cached in the corresponding URL objects. You may specify nil for this parameter. For a list of keys you can specify,
/// see [Common File System Resource
/// Keys](https://developer.apple.com/documentation/corefoundation/cfurl/common_file_system_resource_keys).
/// - skipHiddenFiles: When true, hidden files will not be returned.
/// - Returns: An async collection of urls.
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
public func search(
// swiftformat:disable unusedArguments
directory baseURL: URL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath),
include: [Pattern] = [],
exclude: [Pattern] = [],
includingPropertiesForKeys keys: [URLResourceKey] = [],
skipHiddenFiles: Bool = true
) -> AsyncThrowingStream<URL, any Error> {
AsyncThrowingStream(bufferingPolicy: .unbounded) { continuation in
let task = Task {
do {
for include in include {
let (baseURL, include) = switch include.sections.first {
case let .constant(constant):
if constant.hasSuffix("/") {
(
baseURL.appending(path: constant.dropLast()),
Pattern(sections: Array(include.sections.dropFirst()), options: include.options)
)
} else if include.sections.count == 1 {
(
baseURL.appending(path: constant),
Pattern(sections: Array(include.sections.dropFirst()), options: include.options)
)
} else if case .componentWildcard = include.sections[1] {
(
baseURL.appending(path: constant.components(separatedBy: "/").dropLast().joined(separator: "/")),
Pattern(
sections: [.constant(constant.components(separatedBy: "/").last ?? "")] +
Array(include.sections.dropFirst()),
options: include.options
)
)
} else {
(
baseURL.appending(path: constant),
Pattern(sections: Array(include.sections.dropFirst()), options: include.options)
)
}
default:
(baseURL, include)
}

if include.sections.isEmpty {
if FileManager.default
.fileExists(atPath: baseURL.absoluteString.removingPercentEncoding ?? baseURL.absoluteString)
{
continuation.yield(baseURL)
}
continue
}

var isDirectory: ObjCBool = false
guard FileManager.default.fileExists(
atPath: baseURL.absoluteString.removingPercentEncoding ?? baseURL.absoluteString,
isDirectory: &isDirectory
),
isDirectory.boolValue
else { continue }

try await search(
directory: baseURL,
symbolicLinkDestination: nil,
matching: { _, relativePath in
guard include.match(relativePath) else {
// for patterns like `**/*.swift`, parent folders won't be matched but we don't want to skip those
// folder's descendents or we won't find the files that do match
let skipDescendents = !include.sections.enumerated().contains(where: { index, element in
switch element {
case .pathWildcard:
return true
case .componentWildcard:
return index != include.sections.endIndex - 1
default:
return false
}
})
return .init(matches: false, skipDescendents: skipDescendents)
}

for pattern in exclude {
if pattern.match(relativePath) {
return .init(matches: false, skipDescendents: true)
}
}

return .init(matches: true, skipDescendents: false)
},
includingPropertiesForKeys: keys,
skipHiddenFiles: skipHiddenFiles,
relativePath: "",
continuation: continuation
)
}

continuation.finish()
} catch {
continuation.finish(throwing: error)
}
}

continuation.onTermination = { _ in
task.cancel()
}
}
}

@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
private func search(
directory: URL,
symbolicLinkDestination: URL?,
matching: @escaping @Sendable (_ url: URL, _ relativePath: String) throws -> MatchResult,
includingPropertiesForKeys keys: [URLResourceKey],
skipHiddenFiles: Bool,
relativePath relativeDirectoryPath: String,
continuation: AsyncThrowingStream<URL, any Error>.Continuation
) async throws {
var options: FileManager.DirectoryEnumerationOptions = [
.producesRelativePathURLs,
]
if skipHiddenFiles {
options.insert(.skipsHiddenFiles)
}
let contents = try FileManager.default.contentsOfDirectory(
at: symbolicLinkDestination ?? directory,
includingPropertiesForKeys: keys + [.isDirectoryKey],
options: options
)

try await withThrowingTaskGroup(of: Void.self) { group in
for url in contents {
let relativePath = relativeDirectoryPath + url.lastPathComponent

let matchResult = try matching(url, relativePath)

let foundPath = directory.appending(path: url.relativePath)

if matchResult.matches {
continuation.yield(foundPath)
}

guard !matchResult.skipDescendents else { continue }

let resourceValues = try url.resourceValues(forKeys: [.isDirectoryKey, .isSymbolicLinkKey])
let isDirectory: Bool
let symbolicLinkDestination: URL?
if resourceValues.isDirectory == true {
isDirectory = true
symbolicLinkDestination = nil
} else if resourceValues.isSymbolicLink == true {
let resourceValues = try url.resolvingSymlinksInPath().resourceValues(forKeys: [.isDirectoryKey])
isDirectory = resourceValues.isDirectory == true
symbolicLinkDestination = url.resolvingSymlinksInPath()
} else {
isDirectory = false
symbolicLinkDestination = nil
}
if isDirectory {
group.addTask {
try await search(
directory: foundPath,
symbolicLinkDestination: symbolicLinkDestination,
matching: matching,
includingPropertiesForKeys: keys,
skipHiddenFiles: skipHiddenFiles,
relativePath: relativePath + "/",
continuation: continuation
)
}
}
}

try await group.waitForAll()
}
}
Loading

0 comments on commit c23b377

Please sign in to comment.