diff --git a/Sources/Workspace/Workspace+BinaryArtifacts.swift b/Sources/Workspace/Workspace+BinaryArtifacts.swift index c7e7d09f380..c1536b5dffb 100644 --- a/Sources/Workspace/Workspace+BinaryArtifacts.swift +++ b/Sources/Workspace/Workspace+BinaryArtifacts.swift @@ -16,8 +16,8 @@ import PackageModel import SPMBuildCore import TSCBasic -import enum TSCUtility.Diagnostics import PackageLoading +import enum TSCUtility.Diagnostics extension Workspace { // marked public for testing @@ -61,10 +61,14 @@ extension Workspace { self.delegate = delegate } - func parseArtifacts(from manifests: DependencyManifests, observabilityScope: ObservabilityScope) throws -> (local: [ManagedArtifact], remote: [RemoteArtifact]) { + func parseArtifacts( + from manifests: DependencyManifests, + observabilityScope: ObservabilityScope + ) throws -> (local: [ManagedArtifact], remote: [RemoteArtifact]) { let packageAndManifests: [(reference: PackageReference, manifest: Manifest)] = manifests.root.packages.values + // Root package and manifests. - manifests.dependencies.map({ manifest, managed, _, _ in (managed.packageRef, manifest) }) // Dependency package and manifests. + manifests.dependencies + .map { manifest, managed, _, _ in (managed.packageRef, manifest) } // Dependency package and manifests. var localArtifacts: [ManagedArtifact] = [] var remoteArtifacts: [RemoteArtifact] = [] @@ -84,8 +88,13 @@ extension Workspace { ) ) } else { - guard let (artifactPath, artifactKind) = try Self.deriveBinaryArtifact(fileSystem: self.fileSystem, path: absolutePath, observabilityScope: observabilityScope) else { - observabilityScope.emit(.localArtifactNotFound(artifactPath: absolutePath, targetName: target.name)) + guard let (artifactPath, artifactKind) = try Self.deriveBinaryArtifact( + fileSystem: self.fileSystem, + path: absolutePath, + observabilityScope: observabilityScope + ) else { + observabilityScope + .emit(.localArtifactNotFound(artifactPath: absolutePath, targetName: target.name)) continue } localArtifacts.append( @@ -103,7 +112,8 @@ extension Workspace { packageRef: packageReference, targetName: target.name, url: url, - checksum: checksum) + checksum: checksum + ) ) } else { throw StringError("a binary target should have either a path or a URL and a checksum") @@ -114,7 +124,11 @@ extension Workspace { return (local: localArtifacts, remote: remoteArtifacts) } - func download(_ artifacts: [RemoteArtifact], artifactsDirectory: AbsolutePath, observabilityScope: ObservabilityScope) throws -> [ManagedArtifact] { + func download( + _ artifacts: [RemoteArtifact], + artifactsDirectory: AbsolutePath, + observabilityScope: ObservabilityScope + ) throws -> [ManagedArtifact] { let group = DispatchGroup() let result = ThreadSafeArrayStore() @@ -146,27 +160,40 @@ extension Workspace { throw StringError("Body is empty") } // FIXME: would be nice if checksumAlgorithm.hash took Data directly - let bodyChecksum = self.checksumAlgorithm.hash(ByteString(body)).hexadecimalRepresentation + let bodyChecksum = self.checksumAlgorithm.hash(ByteString(body)) + .hexadecimalRepresentation guard bodyChecksum == indexFile.checksum else { - throw StringError("checksum of downloaded artifact of binary target '\(indexFile.targetName)' (\(bodyChecksum)) does not match checksum specified by the manifest (\(indexFile.checksum ))") + throw StringError( + "checksum of downloaded artifact of binary target '\(indexFile.targetName)' (\(bodyChecksum)) does not match checksum specified by the manifest (\(indexFile.checksum))" + ) } let metadata = try jsonDecoder.decode(ArchiveIndexFile.self, from: body) // FIXME: this filter needs to become more sophisticated - guard let supportedArchive = metadata.archives.first(where: { $0.fileName.lowercased().hasSuffix(".zip") && $0.supportedTriples.contains(self.hostToolchain.triple) }) else { - throw StringError("No supported archive was found for '\(self.hostToolchain.triple.tripleString)'") + guard let supportedArchive = metadata.archives + .first(where: { + $0.fileName.lowercased().hasSuffix(".zip") && $0.supportedTriples + .contains(self.hostToolchain.triple) + }) + else { + throw StringError( + "No supported archive was found for '\(self.hostToolchain.triple.tripleString)'" + ) } // add relevant archive zipArtifacts.append( RemoteArtifact( packageRef: indexFile.packageRef, targetName: indexFile.targetName, - url: indexFile.url.deletingLastPathComponent().appendingPathComponent(supportedArchive.fileName), - checksum: supportedArchive.checksum) + url: indexFile.url.deletingLastPathComponent() + .appendingPathComponent(supportedArchive.fileName), + checksum: supportedArchive.checksum + ) ) } } catch { errors.append(error) - observabilityScope.emit(error: "failed retrieving '\(indexFile.url)': \(error.interpolationDescription)") + observabilityScope + .emit(error: "failed retrieving '\(indexFile.url)': \(error.interpolationDescription)") } } } @@ -182,14 +209,17 @@ extension Workspace { // finally download zip files, if any for artifact in zipArtifacts.get() { - let destinationDirectory = artifactsDirectory.appending(components: [artifact.packageRef.identity.description, artifact.targetName]) - guard observabilityScope.trap ({ try fileSystem.createDirectory(destinationDirectory, recursive: true) }) else { + let destinationDirectory = artifactsDirectory + .appending(components: [artifact.packageRef.identity.description, artifact.targetName]) + guard observabilityScope + .trap({ try fileSystem.createDirectory(destinationDirectory, recursive: true) }) + else { continue } let archivePath = destinationDirectory.appending(component: artifact.url.lastPathComponent) if self.fileSystem.exists(archivePath) { - guard observabilityScope.trap ({ try self.fileSystem.removeFileTree(archivePath) }) else { + guard observabilityScope.trap({ try self.fileSystem.removeFileTree(archivePath) }) else { continue } } @@ -197,7 +227,12 @@ extension Workspace { group.enter() var headers = HTTPClientHeaders() headers.add(name: "Accept", value: "application/octet-stream") - var request = LegacyHTTPClient.Request.download(url: artifact.url, headers: headers, fileSystem: self.fileSystem, destination: archivePath) + var request = LegacyHTTPClient.Request.download( + url: artifact.url, + headers: headers, + fileSystem: self.fileSystem, + destination: archivePath + ) request.options.authorizationProvider = self.authorizationProvider?.httpAuthorizationHeader(for:) request.options.retryStrategy = .exponentialBackoff(maxAttempts: 3, baseDelay: .milliseconds(50)) request.options.validResponseCodes = [200] @@ -211,7 +246,8 @@ extension Workspace { self.delegate?.downloadingBinaryArtifact( from: artifact.url.absoluteString, bytesDownloaded: bytesDownloaded, - totalBytesToDownload: totalBytesToDownload) + totalBytesToDownload: totalBytesToDownload + ) }, completion: { downloadResult in defer { group.leave() } @@ -228,21 +264,36 @@ extension Workspace { switch validationResult { case .success(let valid): guard valid else { - observabilityScope.emit(.artifactInvalidArchive(artifactURL: artifact.url, targetName: artifact.targetName)) + observabilityScope + .emit(.artifactInvalidArchive( + artifactURL: artifact.url, + targetName: artifact.targetName + )) return } - guard let archiveChecksum = observabilityScope.trap ({ try self.checksum(forBinaryArtifactAt: archivePath) }) else { + guard let archiveChecksum = observabilityScope + .trap({ try self.checksum(forBinaryArtifactAt: archivePath) }) + else { return } guard archiveChecksum == artifact.checksum else { - observabilityScope.emit(.artifactInvalidChecksum(targetName: artifact.targetName, expectedChecksum: artifact.checksum, actualChecksum: archiveChecksum)) + observabilityScope.emit(.artifactInvalidChecksum( + targetName: artifact.targetName, + expectedChecksum: artifact.checksum, + actualChecksum: archiveChecksum + )) observabilityScope.trap { try self.fileSystem.removeFileTree(archivePath) } return } guard let tempExtractionDirectory = observabilityScope.trap({ () -> AbsolutePath in - let path = artifactsDirectory.appending(components: "extract", artifact.packageRef.identity.description, artifact.targetName, UUID().uuidString) + let path = artifactsDirectory.appending( + components: "extract", + artifact.packageRef.identity.description, + artifact.targetName, + UUID().uuidString + ) try self.fileSystem.forceCreateDirectory(at: path) return path }) else { @@ -250,70 +301,129 @@ extension Workspace { } group.enter() - observabilityScope.emit(debug: "extracting \(archivePath) to \(tempExtractionDirectory)") - self.archiver.extract(from: archivePath, to: tempExtractionDirectory, completion: { extractResult in - defer { group.leave() } - - switch extractResult { - case .success: - observabilityScope.trap { - try self.fileSystem.withLock(on: destinationDirectory, type: .exclusive) { - // strip first level component if needed - if try self.fileSystem.shouldStripFirstLevel(archiveDirectory: tempExtractionDirectory, acceptableExtensions: BinaryTarget.Kind.allCases.map({ $0.fileExtension })) { - observabilityScope.emit(debug: "stripping first level component from \(tempExtractionDirectory)") - try self.fileSystem.stripFirstLevel(of: tempExtractionDirectory) - } else { - observabilityScope.emit(debug: "no first level component stripping needed for \(tempExtractionDirectory)") - } - let content = try self.fileSystem.getDirectoryContents(tempExtractionDirectory) - // copy from temp location to actual location - for file in content { - let source = tempExtractionDirectory.appending(component: file) - let destination = destinationDirectory.appending(component: file) - if self.fileSystem.exists(destination) { - try self.fileSystem.removeFileTree(destination) + observabilityScope + .emit(debug: "extracting \(archivePath) to \(tempExtractionDirectory)") + self.archiver.extract( + from: archivePath, + to: tempExtractionDirectory, + completion: { extractResult in + defer { group.leave() } + + switch extractResult { + case .success: + observabilityScope.trap { + try self.fileSystem.withLock( + on: destinationDirectory, + type: .exclusive + ) { + // strip first level component if needed + if try self.fileSystem.shouldStripFirstLevel( + archiveDirectory: tempExtractionDirectory, + acceptableExtensions: BinaryTarget.Kind.allCases + .map(\.fileExtension) + ) { + observabilityScope + .emit( + debug: "stripping first level component from \(tempExtractionDirectory)" + ) + try self.fileSystem + .stripFirstLevel(of: tempExtractionDirectory) + } else { + observabilityScope + .emit( + debug: "no first level component stripping needed for \(tempExtractionDirectory)" + ) + } + let content = try self.fileSystem + .getDirectoryContents(tempExtractionDirectory) + // copy from temp location to actual location + for file in content { + let source = tempExtractionDirectory + .appending(component: file) + let destination = destinationDirectory + .appending(component: file) + if self.fileSystem.exists(destination) { + try self.fileSystem.removeFileTree(destination) + } + try self.fileSystem.copy(from: source, to: destination) } - try self.fileSystem.copy(from: source, to: destination) } + // remove temp location + try self.fileSystem.removeFileTree(tempExtractionDirectory) } - // remove temp location - try self.fileSystem.removeFileTree(tempExtractionDirectory) - } - // derive concrete artifact path and type - guard let (artifactPath, artifactKind) = try? Self.deriveBinaryArtifact(fileSystem: self.fileSystem, path: destinationDirectory, observabilityScope: observabilityScope) else { - return observabilityScope.emit(.remoteArtifactNotFound(artifactURL: artifact.url, targetName: artifact.targetName)) - } + // derive concrete artifact path and type + guard let (artifactPath, artifactKind) = try? Self.deriveBinaryArtifact( + fileSystem: self.fileSystem, + path: destinationDirectory, + observabilityScope: observabilityScope + ) else { + return observabilityScope + .emit(.remoteArtifactNotFound( + artifactURL: artifact.url, + targetName: artifact.targetName + )) + } - result.append( - .remote( - packageRef: artifact.packageRef, + result.append( + .remote( + packageRef: artifact.packageRef, + targetName: artifact.targetName, + url: artifact.url.absoluteString, + checksum: artifact.checksum, + path: artifactPath, + kind: artifactKind + ) + ) + self.delegate?.didDownloadBinaryArtifact( + from: artifact.url.absoluteString, + result: .success(artifactPath), + duration: downloadStart.distance(to: .now()) + ) + case .failure(let error): + observabilityScope.emit(.remoteArtifactFailedExtraction( + artifactURL: artifact.url, targetName: artifact.targetName, - url: artifact.url.absoluteString, - checksum: artifact.checksum, - path: artifactPath, - kind: artifactKind + reason: error.interpolationDescription + )) + self.delegate?.didDownloadBinaryArtifact( + from: artifact.url.absoluteString, + result: .failure(error), + duration: downloadStart.distance(to: .now()) ) - ) - self.delegate?.didDownloadBinaryArtifact(from: artifact.url.absoluteString, result: .success(artifactPath), duration: downloadStart.distance(to: .now())) - case .failure(let error): - observabilityScope.emit(.remoteArtifactFailedExtraction(artifactURL: artifact.url, targetName: artifact.targetName, reason: error.interpolationDescription)) - self.delegate?.didDownloadBinaryArtifact(from: artifact.url.absoluteString, result: .failure(error), duration: downloadStart.distance(to: .now())) - } + } - observabilityScope.trap { try self.fileSystem.removeFileTree(archivePath) } - }) + observabilityScope.trap { try self.fileSystem.removeFileTree(archivePath) } + } + ) case .failure(let error): - observabilityScope.emit(.artifactFailedValidation(artifactURL: artifact.url, targetName: artifact.targetName, reason: error.interpolationDescription)) - self.delegate?.didDownloadBinaryArtifact(from: artifact.url.absoluteString, result: .failure(error), duration: downloadStart.distance(to: .now())) + observabilityScope.emit(.artifactFailedValidation( + artifactURL: artifact.url, + targetName: artifact.targetName, + reason: error.interpolationDescription + )) + self.delegate?.didDownloadBinaryArtifact( + from: artifact.url.absoluteString, + result: .failure(error), + duration: downloadStart.distance(to: .now()) + ) } }) case .failure(let error): - observabilityScope.trap ({ try self.fileSystem.removeFileTree(archivePath) }) - observabilityScope.emit(.artifactFailedDownload(artifactURL: artifact.url, targetName: artifact.targetName, reason: error.interpolationDescription)) - self.delegate?.didDownloadBinaryArtifact(from: artifact.url.absoluteString, result: .failure(error), duration: downloadStart.distance(to: .now())) + observabilityScope.trap { try self.fileSystem.removeFileTree(archivePath) } + observabilityScope.emit(.artifactFailedDownload( + artifactURL: artifact.url, + targetName: artifact.targetName, + reason: error.interpolationDescription + )) + self.delegate?.didDownloadBinaryArtifact( + from: artifact.url.absoluteString, + result: .failure(error), + duration: downloadStart.distance(to: .now()) + ) } - }) + } + ) } group.wait() @@ -325,15 +435,25 @@ extension Workspace { return result.get() } - func extract(_ artifacts: [ManagedArtifact], artifactsDirectory: AbsolutePath, observabilityScope: ObservabilityScope) throws -> [ManagedArtifact] { + func extract( + _ artifacts: [ManagedArtifact], + artifactsDirectory: AbsolutePath, + observabilityScope: ObservabilityScope + ) throws -> [ManagedArtifact] { let result = ThreadSafeArrayStore() let group = DispatchGroup() for artifact in artifacts { - let destinationDirectory = artifactsDirectory.appending(components: [artifact.packageRef.identity.description, artifact.targetName]) + let destinationDirectory = artifactsDirectory + .appending(components: [artifact.packageRef.identity.description, artifact.targetName]) try fileSystem.createDirectory(destinationDirectory, recursive: true) - let tempExtractionDirectory = artifactsDirectory.appending(components: "extract", artifact.packageRef.identity.description, artifact.targetName, UUID().uuidString) + let tempExtractionDirectory = artifactsDirectory.appending( + components: "extract", + artifact.packageRef.identity.description, + artifact.targetName, + UUID().uuidString + ) try self.fileSystem.forceCreateDirectory(at: tempExtractionDirectory) group.enter() @@ -342,14 +462,21 @@ extension Workspace { switch extractResult { case .success: - observabilityScope.trap { () -> Void in + observabilityScope.trap { () in try self.fileSystem.withLock(on: destinationDirectory, type: .exclusive) { // strip first level component if needed - if try self.fileSystem.shouldStripFirstLevel(archiveDirectory: tempExtractionDirectory, acceptableExtensions: BinaryTarget.Kind.allCases.map({ $0.fileExtension })) { - observabilityScope.emit(debug: "stripping first level component from \(tempExtractionDirectory)") + if try self.fileSystem.shouldStripFirstLevel( + archiveDirectory: tempExtractionDirectory, + acceptableExtensions: BinaryTarget.Kind.allCases.map(\.fileExtension) + ) { + observabilityScope + .emit(debug: "stripping first level component from \(tempExtractionDirectory)") try self.fileSystem.stripFirstLevel(of: tempExtractionDirectory) } else { - observabilityScope.emit(debug: "no first level component stripping needed for \(tempExtractionDirectory)") + observabilityScope + .emit( + debug: "no first level component stripping needed for \(tempExtractionDirectory)" + ) } let content = try self.fileSystem.getDirectoryContents(tempExtractionDirectory) // copy from temp location to actual location @@ -367,8 +494,16 @@ extension Workspace { try self.fileSystem.removeFileTree(tempExtractionDirectory) // derive concrete artifact path and type - guard let (artifactPath, artifactKind) = try Self.deriveBinaryArtifact(fileSystem: self.fileSystem, path: destinationDirectory, observabilityScope: observabilityScope) else { - return observabilityScope.emit(.localArchivedArtifactNotFound(archivePath: artifact.path, targetName: artifact.targetName)) + guard let (artifactPath, artifactKind) = try Self.deriveBinaryArtifact( + fileSystem: self.fileSystem, + path: destinationDirectory, + observabilityScope: observabilityScope + ) else { + return observabilityScope + .emit(.localArchivedArtifactNotFound( + archivePath: artifact.path, + targetName: artifact.targetName + )) } // compute the checksum @@ -387,7 +522,12 @@ extension Workspace { case .failure(let error): let reason = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription - observabilityScope.emit(.localArtifactFailedExtraction(artifactPath: artifact.path, targetName: artifact.targetName, reason: reason)) + observabilityScope + .emit(.localArtifactFailedExtraction( + artifactPath: artifact.path, + targetName: artifact.targetName, + reason: reason + )) } }) } @@ -427,7 +567,11 @@ public protocol BinaryArtifactsManagerDelegate { /// The workspace has started downloading a binary artifact. func willDownloadBinaryArtifact(from url: String) /// The workspace has finished downloading a binary artifact. - func didDownloadBinaryArtifact(from url: String, result: Result, duration: DispatchTimeInterval) + func didDownloadBinaryArtifact( + from url: String, + result: Result, + duration: DispatchTimeInterval + ) /// The workspace is downloading a binary artifact. func downloadingBinaryArtifact(from url: String, bytesDownloaded: Int64, totalBytesToDownload: Int64?) /// The workspace finished downloading all binary artifacts. @@ -470,49 +614,76 @@ extension Workspace.BinaryArtifactsManager { } extension Workspace.BinaryArtifactsManager { - - static func deriveBinaryArtifact(fileSystem: FileSystem, path: AbsolutePath, observabilityScope: ObservabilityScope) throws -> (AbsolutePath, BinaryTarget.Kind)? { - let binaryArtifacts = try Self.deriveBinaryArtifacts(fileSystem: fileSystem, path: path, observabilityScope: observabilityScope) + static func deriveBinaryArtifact( + fileSystem: FileSystem, + path: AbsolutePath, + observabilityScope: ObservabilityScope + ) throws -> (AbsolutePath, BinaryTarget.Kind)? { + let binaryArtifacts = try Self.deriveBinaryArtifacts( + fileSystem: fileSystem, + path: path, + observabilityScope: observabilityScope + ) if binaryArtifacts.count > 1, let binaryArtifact = binaryArtifacts.last { // multiple ones, return the last one to preserve old behavior - observabilityScope.emit(warning: "multiple potential binary artifacts found: '\(binaryArtifacts.map{ $0.0.description }.joined(separator: "', '"))', using the one in '\(binaryArtifact.0)'") + observabilityScope + .emit( + warning: "multiple potential binary artifacts found: '\(binaryArtifacts.map(\.0.description).joined(separator: "', '"))', using the one in '\(binaryArtifact.0)'" + ) return binaryArtifact } else if let binaryArtifact = binaryArtifacts.first { // single one + observabilityScope.emit(info: "found binary artifact: '\(binaryArtifact)'") return binaryArtifact } else { return .none } } - private static func deriveBinaryArtifacts(fileSystem: FileSystem, path: AbsolutePath, observabilityScope: ObservabilityScope) throws -> [(AbsolutePath, BinaryTarget.Kind)] { + private static func deriveBinaryArtifacts( + fileSystem: FileSystem, + path: AbsolutePath, + observabilityScope: ObservabilityScope + ) throws -> [(AbsolutePath, BinaryTarget.Kind)] { guard fileSystem.exists(path) else { return [] } + let subdirectories = try fileSystem.getDirectoryContents(path) + .map { path.appending(component: $0) } + .filter { fileSystem.isDirectory($0) } + // is the current path it? - if let kind = try deriveBinaryArtifactKind(fileSystem: fileSystem, path: path, observabilityScope: observabilityScope) { + if let kind = try deriveBinaryArtifactKind( + fileSystem: fileSystem, + path: path, + observabilityScope: observabilityScope + ) { return [(path, kind)] } // try to find a matching subdirectory - let subdirectories = try fileSystem.getDirectoryContents(path) - .map{ path.appending(component: $0) } - .filter { fileSystem.isDirectory($0) } - var results = [(AbsolutePath, BinaryTarget.Kind)]() for subdirectory in subdirectories { observabilityScope.emit(debug: "searching for binary artifact in '\(path)'") - let subdirectoryResults = try Self.deriveBinaryArtifacts(fileSystem: fileSystem, path: subdirectory, observabilityScope: observabilityScope) + let subdirectoryResults = try Self.deriveBinaryArtifacts( + fileSystem: fileSystem, + path: subdirectory, + observabilityScope: observabilityScope + ) results.append(contentsOf: subdirectoryResults) } return results } - private static func deriveBinaryArtifactKind(fileSystem: FileSystem, path: AbsolutePath, observabilityScope: ObservabilityScope) throws -> BinaryTarget.Kind? { + private static func deriveBinaryArtifactKind( + fileSystem: FileSystem, + path: AbsolutePath, + observabilityScope: ObservabilityScope + ) throws -> BinaryTarget.Kind? { let files = try fileSystem.getDirectoryContents(path) - .map{ path.appending(component: $0) } + .map { path.appending(component: $0) } .filter { fileSystem.isFile($0) } if let infoPlist = files.first(where: { $0.basename.lowercased() == "info.plist" }) { @@ -521,7 +692,7 @@ extension Workspace.BinaryArtifactsManager { _ = try decoder.decode(XCFrameworkMetadata.self, from: fileSystem.readFileContents(infoPlist)) return .xcframework } catch { - observabilityScope.emit(warning: "info.plist found in '\(path)' but failed to parse: \(error)") + observabilityScope.emit(debug: "info.plist found in '\(path)' but failed to parse: \(error)") } } @@ -530,20 +701,22 @@ extension Workspace.BinaryArtifactsManager { _ = try ArtifactsArchiveMetadata.parse(fileSystem: fileSystem, rootPath: infoJSON.parentDirectory) return .artifactsArchive } catch { - observabilityScope.emit(warning: "info.json found in '\(path)' but failed to parse: \(error)") + observabilityScope.emit(debug: "info.json found in '\(path)' but failed to parse: \(error)") } } return .none } - } extension FileSystem { // helper to decide if an archive directory would benefit from stripping first level - fileprivate func shouldStripFirstLevel(archiveDirectory: AbsolutePath, acceptableExtensions: [String]? = nil) throws -> Bool { + fileprivate func shouldStripFirstLevel( + archiveDirectory: AbsolutePath, + acceptableExtensions: [String]? = nil + ) throws -> Bool { let subdirectories = try self.getDirectoryContents(archiveDirectory) - .map{ archiveDirectory.appending(component: $0) } + .map { archiveDirectory.appending(component: $0) } .filter { self.isDirectory($0) } // single top-level directory required @@ -563,7 +736,7 @@ extension FileSystem { // see if there is "grand-child" directory with one of the acceptable extensions return try self.getDirectoryContents(rootDirectory) - .map{ rootDirectory.appending(component: $0) } - .first{ $0.extension.map { acceptableExtensions.contains($0) } ?? false } != nil + .map { rootDirectory.appending(component: $0) } + .first { $0.extension.map { acceptableExtensions.contains($0) } ?? false } != nil } }