diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5815bed6..dfa34895 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,12 +13,18 @@ jobs: args: --strict macOS: - runs-on: macos-12 + runs-on: macos-13 env: - XCODE_VERSION: ${{ '14.2' }} + XCODE_VERSION: ${{ '15.0' }} steps: + - name: Install nginx + run: "brew install nginx" - name: Select Xcode run: "sudo xcode-select -s /Applications/Xcode_$XCODE_VERSION.app" + - name: "Install iOS SDK" + run: "xcodebuild -downloadPlatform iOS" + - name: "Install watchOS SDK" + run: "xcodebuild -downloadPlatform watchOS" - name: Checkout uses: actions/checkout@v1 - name: Build and Run diff --git a/Package.swift b/Package.swift index 00c88e43..6d490152 100644 --- a/Package.swift +++ b/Package.swift @@ -67,6 +67,10 @@ let package = Package( name: "xcldplusplus", dependencies: ["XCRemoteCache"] ), + .target( + name: "xcactool", + dependencies: ["XCRemoteCache"] + ), .target( // Wrapper target that builds all binaries but does nothing in runtime name: "Aggregator", @@ -80,6 +84,7 @@ let package = Package( "xcld", "xcldplusplus", "xclipo", + "xcactool" ] ), .testTarget( diff --git a/README.md b/README.md index 813ee89f..bd1e6590 100755 --- a/README.md +++ b/README.md @@ -323,6 +323,7 @@ Note: This step is not required if at least one of these is true: | `mode_marker_path` | Path, relative to `$TARGET_TEMP_DIR`, of a maker file to enable or disable the remote cache for a given target. Includes a list of all allowed input files to use remote cache | `rc.enabled` | ⬜️ | | `clang_command` | Command for a standard C compilation fallback | `clang` | ⬜️ | | `swiftc_command` | Command for a standard Swift compilation fallback | `swiftc` | ⬜️ | +| `actool_command` | Command for the assets catalog tool. _By default uses a symlink that points to the default Xcode location's actool_ | `/var/db/xcode_select_link/usr/bin/actool` | ⬜️ | | `primary_repo` | Address of the primary git repository that produces cache artifacts (case-sensitive) | N/A | ✅ | | `primary_branch` | The main (primary) branch on the `primary_repo` that produces cache artifacts | `master` | ⬜️ | | `repo_root` | The path to the git repo root | `"."` | ⬜️ | diff --git a/Rakefile b/Rakefile index 45a4e867..40c3bde5 100644 --- a/Rakefile +++ b/Rakefile @@ -10,7 +10,7 @@ DERIVED_DATA_DIR = File.join('.build').freeze RELEASES_ROOT_DIR = File.join('releases').freeze EXECUTABLE_NAME = 'XCRemoteCache' -EXECUTABLE_NAMES = ['xclibtool', 'xcpostbuild', 'xcprebuild', 'xcprepare', 'xcswiftc', 'swiftc', 'xcswift-frontend', 'swift-frontend', 'xcld', 'xcldplusplus', 'xclipo'] +EXECUTABLE_NAMES = ['xclibtool', 'xcpostbuild', 'xcprebuild', 'xcprepare', 'xcswiftc', 'swiftc', 'xcswift-frontend', 'swift-frontend', 'xcld', 'xcldplusplus', 'xclipo', 'xcactool', 'actool'] PROJECT_NAME = 'XCRemoteCache' SWIFTLINT_ENABLED = true @@ -60,9 +60,10 @@ task :build, [:configuration, :arch, :sdks, :is_archive] do |task, args| # Path of the executable looks like: `.build/(debug|release)/XCRemoteCache` build_path_base = File.join(DERIVED_DATA_DIR, args.configuration) # swift-frontent integration requires that the SWIFT_EXEC is `swiftc` so create - # a symbolic link between swiftc->xcswiftc and swift-frontend->xcswift-frontend + # a symbolic link between swiftc->xcswiftc and swift-frontend->xcswift-frontend etc. system("cd #{build_path_base} && ln -s xcswiftc swiftc") system("cd #{build_path_base} && ln -s xcswift-frontend swift-frontend") + system("cd #{build_path_base} && ln -s xcactool actool") sdk_build_paths = EXECUTABLE_NAMES.map {|e| File.join(build_path_base, e)} build_paths.push(sdk_build_paths) diff --git a/Sources/XCRemoteCache/Artifacts/ArtifactMetaUpdater.swift b/Sources/XCRemoteCache/Artifacts/ArtifactMetaUpdater.swift new file mode 100644 index 00000000..df36741e --- /dev/null +++ b/Sources/XCRemoteCache/Artifacts/ArtifactMetaUpdater.swift @@ -0,0 +1,73 @@ +// Copyright (c) 2023 Spotify AB. +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import Foundation + +enum ArtifactMetaProcessorError: Error { + /// The prebuild plugin execution was called but the local + /// path to the artifact directory is unknown + /// Might happen that the artifact processor didn't invoke + /// a request to process an artifact + case artifactLocationIsUnknown +} + +/// Processes downloaded artifact by placing an up-to-date and remapped meta file +/// in the artifact directory +class ArtifactMetaUpdater: ArtifactProcessor { + private var artifactLocation: URL? + private let metaWriter: MetaWriter + private let fileRemapper: FileDependenciesRemapper + + init(fileRemapper: FileDependenciesRemapper, metaWriter: MetaWriter) { + self.metaWriter = metaWriter + self.fileRemapper = fileRemapper + } + + /// Remembers the artifact location, used later in the plugin + /// - Parameter url: artifact's root directory + func process(rawArtifact url: URL) throws { + // Storing the location of the just downloaded artifact + // Note, the location might include a meta (generated by producer + // while compiling and building an artifact), which might be outdated + // (e.g. a new schema has been applied to the meta format, while artifacts + // format is backward-compatible) + artifactLocation = url + } + + func process(localArtifact url: URL) throws { + // No need to do anything in the postbuild + } +} + +extension ArtifactMetaUpdater: ArtifactConsumerPrebuildPlugin { + + /// Overrides the meta json file in the downloaded artifact with a meta including + /// remapped paths + func run(meta: MainArtifactMeta) throws { + guard let artifactLocation = artifactLocation else { + throw ArtifactMetaProcessorError.artifactLocationIsUnknown + } + _ = try metaWriter.write(meta, locationDir: artifactLocation) + // TODO: extract the meta json location logic to a shared class + let metaURL = artifactLocation + .appendingPathComponent(meta.fileKey) + .appendingPathExtension("json") + try fileRemapper.remap(fromGeneric: metaURL) + } +} diff --git a/Sources/XCRemoteCache/Artifacts/ArtifactOrganizer.swift b/Sources/XCRemoteCache/Artifacts/ArtifactOrganizer.swift index 2139b4c7..4a9ec73b 100644 --- a/Sources/XCRemoteCache/Artifacts/ArtifactOrganizer.swift +++ b/Sources/XCRemoteCache/Artifacts/ArtifactOrganizer.swift @@ -47,6 +47,8 @@ protocol ArtifactOrganizer { } class ZipArtifactOrganizer: ArtifactOrganizer { + static let activeArtifactLocation = "active" + private let cacheDir: URL // all processors that should "prepare" the unzipped raw artifact private let artifactProcessors: [ArtifactProcessor] @@ -63,7 +65,7 @@ class ZipArtifactOrganizer: ArtifactOrganizer { } func getActiveArtifactLocation() -> URL { - return cacheDir.appendingPathComponent("active") + return cacheDir.appendingPathComponent(Self.self.activeArtifactLocation) } func getActiveArtifactFilekey() throws -> String { @@ -90,20 +92,27 @@ class ZipArtifactOrganizer: ArtifactOrganizer { let destinationURL = artifact.deletingPathExtension() guard fileManager.fileExists(atPath: destinationURL.path) == false else { infoLog("Skipping artifact, already existing at \(destinationURL)") + try runArtifactProcessors(artifactLocation: destinationURL) return destinationURL } - // Uzipping to a temp file first to never leave the uncompleted zip in the final location + // Unzipping to a temp file first to never leave the uncompleted zip in the final location // when the command was interrupted (internal crash or `kill -9` signal) let tempDestination = destinationURL.appendingPathExtension("tmp") try Zip.unzipFile(artifact, destination: tempDestination, overwrite: true, password: nil) - try artifactProcessors.forEach { processor in - try processor.process(rawArtifact: tempDestination) - } try fileManager.moveItem(at: tempDestination, to: destinationURL) + try runArtifactProcessors(artifactLocation: destinationURL) return destinationURL } + /// Iterates all processor when an artifact has been just downloaded or reused from already downloaded + /// and previously processed location + private func runArtifactProcessors(artifactLocation: URL) throws { + try artifactProcessors.forEach { processor in + try processor.process(rawArtifact: artifactLocation) + } + } + func activate(extractedArtifact: URL) throws { let activeLocationURL = getActiveArtifactLocation() try fileManager.spt_forceSymbolicLink(at: activeLocationURL, withDestinationURL: extractedArtifact) diff --git a/Sources/XCRemoteCache/Artifacts/MetaPathProvider.swift b/Sources/XCRemoteCache/Artifacts/MetaPathProvider.swift new file mode 100644 index 00000000..956c5876 --- /dev/null +++ b/Sources/XCRemoteCache/Artifacts/MetaPathProvider.swift @@ -0,0 +1,56 @@ +// Copyright (c) 2023 Spotify AB. +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import Foundation + +enum MetaPathProviderError: Error { + /// Generic error when a meta path cannot be provided + case failed(message: String) +} + +protocol MetaPathProvider { + /// Returns the location of the meta file on disk + func getMetaPath() throws -> URL +} + +/// Finds the location of the meta in the unzipped artifact. +/// Assumes the artifact contains only a single .json file: +/// a meta file with filename equal to the fileKey +class ArtifactMetaPathProvider: MetaPathProvider { + private let artifactLocation: URL + private let dirScanner: DirScanner + + init( + artifactLocation: URL, + dirScanner: DirScanner + ) { + self.artifactLocation = artifactLocation + self.dirScanner = dirScanner + } + + func getMetaPath() throws -> URL { + let items = try dirScanner.items(at: artifactLocation) + guard let meta = items.first(where: { $0.pathExtension == "json" }) else { + throw MetaPathProviderError.failed( + message: "artifact \(artifactLocation) doesn't contain expected .json with a meta" + ) + } + return meta + } +} diff --git a/Sources/XCRemoteCache/Commands/Actool/ACTool.swift b/Sources/XCRemoteCache/Commands/Actool/ACTool.swift new file mode 100644 index 00000000..5d5d72b0 --- /dev/null +++ b/Sources/XCRemoteCache/Commands/Actool/ACTool.swift @@ -0,0 +1,81 @@ +// Copyright (c) 2023 Spotify AB. +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import Foundation + +enum ACToolResult: Equatable { + /// the generated actool interfaces matches the one observed on a producer side + case cacheHit(dependencies: [URL]) + /// the generated interface is different - leads to a cache miss + case cacheMiss +} + +class ACTool { + private let markerReader: ListReader + private let markerWriter: MarkerWriter + private let metaReader: MetaReader + private let fingerprintAccumulator: FingerprintAccumulator + private let metaPathProvider: MetaPathProvider + + init( + markerReader: ListReader, + markerWriter: MarkerWriter, + metaReader: MetaReader, + fingerprintAccumulator: FingerprintAccumulator, + metaPathProvider: MetaPathProvider + ) { + self.markerReader = markerReader + self.markerWriter = markerWriter + self.metaReader = metaReader + self.fingerprintAccumulator = fingerprintAccumulator + self.metaPathProvider = metaPathProvider + } + + func run() throws -> ACToolResult { + // 1. do nothing if the RC is disabled + guard markerReader.canRead() else { + return .cacheMiss + } + let dependencies = try markerReader.listFilesURLs() + + // 2. Read meta's sources files & fingerprint + let metaPath = try metaPathProvider.getMetaPath() + let meta = try metaReader.read(localFile: metaPath) + // 3. Compare local vs meta's fingerprint + let localFingerprint = try computeFingerprints(meta.assetsSources) + // 4. Disable RC if the is fingerprint doesn't match + return (localFingerprint == meta.assetsSourcesFingerprint ? .cacheHit(dependencies: dependencies) : .cacheMiss) + } + + private func computeFingerprints(_ paths: [String]) throws -> RawFingerprint { + // TODO: + // 1. transform to URL + // 2. build (in the same order the fingerprint) + fingerprintAccumulator.reset() + for path in paths { + let file = URL(fileURLWithPath: path) + do { + try fingerprintAccumulator.append(file) + } catch FingerprintAccumulatorError.missingFile(let content) { + printWarning("File at \(content.path) was not found on disc. Calculating fingerprint without it.") + } + } + return try fingerprintAccumulator.generate() + } +} diff --git a/Sources/XCRemoteCache/Commands/Actool/XCACTool.swift b/Sources/XCRemoteCache/Commands/Actool/XCACTool.swift new file mode 100644 index 00000000..895b8161 --- /dev/null +++ b/Sources/XCRemoteCache/Commands/Actool/XCACTool.swift @@ -0,0 +1,108 @@ +// Copyright (c) 2023 Spotify AB. +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import Foundation + +public class XCACTool { + + private let args: [String] + private let objcOutput: String? + private let swiftOutput: String? + private let shellOut: ShellOut + + public init( + args: [String], + objcOutput: String?, + swiftOutput: String? + ) { + self.args = args + self.objcOutput = objcOutput + self.swiftOutput = swiftOutput + self.shellOut = ProcessShellOut() + } + + public func run() throws { + // Alternatively, read $PWD + let currentDir = FileManager.default.currentDirectoryPath + let fileManager = FileManager.default + let fileAccessor: FileAccessor = fileManager + let config: XCRemoteCacheConfig + let context: ACToolContext + let srcRoot: URL = URL(fileURLWithPath: currentDir) + config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileReader: fileAccessor) + .readConfiguration() + context = try ACToolContext( + config: config, + objcOutput: objcOutput, + swiftOutput: swiftOutput + ) + + let markerReader = FileMarkerReader(context.markerURL, fileManager: fileAccessor) + let markerWriter = FileMarkerWriter(context.markerURL, fileAccessor: fileAccessor) + let fingerprintAccumulator = FingerprintAccumulatorImpl(algorithm: MD5Algorithm(), fileManager: fileManager) + let metaPathProvider = ArtifactMetaPathProvider(artifactLocation: context.activeArtifactLocation, dirScanner: fileManager) + let metaReader = JsonMetaReader(fileAccessor: fileManager) + + // 0. Let the command run + try fallbackToDefaultAndWait(command: config.actoolCommand, args: args) + + let acTool = ACTool( + markerReader: markerReader, + markerWriter: markerWriter, + metaReader: metaReader, + fingerprintAccumulator: fingerprintAccumulator, + metaPathProvider: metaPathProvider + ) + + let acResult: ACToolResult + do { + acResult = try acTool.run() + } catch { + infoLog("\(config.actoolCommand) wrapper cannot recognize compare fingerprints with an error \(error)") + acResult = .cacheMiss + } + + do { + switch acResult { + case .cacheHit(let dependencies): + try markerWriter.enable(dependencies: dependencies) + default: + try markerWriter.disable() + } + } catch { + // separate invocations as os_log truncates long messages + errorLog("Failure in \(config.actoolCommand) marker setup with cache \(acResult)") + errorLog("\(error)") + } + } + + private func fallbackToDefaultAndWait(command: String = "actool", args: [String]) throws { + defaultLog("Fallbacking to compilation using \(command).") + do { + try shellOut.callExternalProcessAndWait( + command: command, + invocationArgs: Array(args.dropFirst()), + envs: ProcessInfo.processInfo.environment + ) + } catch ShellError.statusError(_, let exitCode) { + exit(exitCode) + } + } +} + diff --git a/Sources/XCRemoteCache/Commands/Actool/XCACToolContext.swift b/Sources/XCRemoteCache/Commands/Actool/XCACToolContext.swift new file mode 100644 index 00000000..c6e63ca0 --- /dev/null +++ b/Sources/XCRemoteCache/Commands/Actool/XCACToolContext.swift @@ -0,0 +1,59 @@ +// Copyright (c) 2023 Spotify AB. +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import Foundation + +enum XCACToolContextError: Error { + /// none of ObjC or Swift source output is defined + case noOutputFile +} + +struct ACToolContext { + let tempDir: URL + let objcOutput: URL? + let swiftOutput: URL? + let markerURL: URL + /// Location (might include a symlink) to the unzipped artifact + let activeArtifactLocation: URL + + init( + config: XCRemoteCacheConfig, + objcOutput: String?, + swiftOutput: String? + ) throws { + self.objcOutput = objcOutput.map(URL.init(fileURLWithPath:)) + self.swiftOutput = swiftOutput.map(URL.init(fileURLWithPath:)) + + // infer the target from either objc or swift + guard let sourceOutputFile = self.objcOutput ?? self.swiftOutput else { + throw XCACToolContextError.noOutputFile + } + + // sourceOutputFile has a format $TARGET_TEMP_DIR/DerivedSources/GeneratedAssetSymbols.{swift|h} + // That may be subject to change for other Xcode versions + self.tempDir = sourceOutputFile + .deletingLastPathComponent() + .deletingLastPathComponent() + + self.markerURL = tempDir.appendingPathComponent(config.modeMarkerPath) + activeArtifactLocation = tempDir + .appendingPathComponent("xccache") + .appendingPathComponent(ZipArtifactOrganizer.activeArtifactLocation) + } +} diff --git a/Sources/XCRemoteCache/Commands/Postbuild/Postbuild.swift b/Sources/XCRemoteCache/Commands/Postbuild/Postbuild.swift index 72ac8980..3a19ae4e 100644 --- a/Sources/XCRemoteCache/Commands/Postbuild/Postbuild.swift +++ b/Sources/XCRemoteCache/Commands/Postbuild/Postbuild.swift @@ -124,7 +124,7 @@ class Postbuild { public func generateFingerprintOverrides() throws { // Compute a local fingerprint and decorate the .swiftmodule files let dependencies = try generateDependencies() - let fingerprint = try generateFingerprint(dependencies) + let fingerprint = try generateFingerprint(dependencies.fingerprintScoped) try generateFingerprintOverrides(contextSpecificFingerprint: fingerprint.contextSpecific) } @@ -147,13 +147,15 @@ class Postbuild { /// Builds an artifact package and uploads it to the remote server public func performBuildUpload(for commit: String) throws { let dependencies = try generateDependencies() - let localFingerprint = try generateFingerprint(dependencies) + let localFingerprint = try generateFingerprint(dependencies.fingerprintScoped) + let assetsSourcesFingerprint = try generateFingerprint(dependencies.assetSources) // Filekey has to be unique for the context to not mix builds Debug/Release, iphonesimulator/iphoneos etc let fileKey = localFingerprint.contextSpecific // Replace all local paths to the generic ones (e.g. $SRCROOT) let remappers = [remapper] + creatorPlugins.compactMap(\.customPathsRemapper) let remapper = DependenciesRemapperComposite(remappers) - let abstractFingerprintFiles = try remapper.replace(localPaths: dependencies.map(\.path)) + let abstractFingerprintFiles = try remapper.replace(localPaths: dependencies.fingerprintScoped.map(\.path)) + let abstractAssetsSourcesFiles = try remapper.replace(localPaths: dependencies.assetSources.map(\.path)) // TODO: use `inputs` read by dependenciesReader var meta = MainArtifactMeta( dependencies: abstractFingerprintFiles, @@ -165,7 +167,9 @@ class Postbuild { platform: context.platform, xcode: context.xcodeBuildNumber, inputs: [], - pluginsKeys: [:] + pluginsKeys: [:], + assetsSources: abstractAssetsSourcesFiles, + assetsSourcesFingerprint: assetsSourcesFingerprint.raw ) meta = try creatorPlugins.reduce(meta) { prevMeta, plugin in var meta = prevMeta @@ -204,12 +208,14 @@ class Postbuild { try modeController.enable(allowedInputFiles: [], dependencies: [executableURL]) } + typealias GenerateDependenciesResult = (fingerprintScoped: [URL], assetSources: [URL]) /// Reads all relevant dependencies (e.g. Xcode-embedded dependencies are skipped) - private func generateDependencies() throws -> [URL] { + private func generateDependencies() throws -> GenerateDependenciesResult { let dependencies = try dependenciesReader.findDependencies().map(URL.init(fileURLWithPath:)) let processedDependencies = dependencyProcessor.process(dependencies) - let fingerprintFiles = processedDependencies.map(fingerprintOverrideManager.getFingerprintFile) - return fingerprintFiles.map { $0.url } + let fingerprintFiles = processedDependencies.fingerprintScoped.map(fingerprintOverrideManager.getFingerprintFile) + let assetsSourceFiles = processedDependencies.assetsSource.map(fingerprintOverrideManager.getFingerprintFile) + return (fingerprintScoped: fingerprintFiles.map { $0.url }, assetSources: assetsSourceFiles.map { $0.url }) } private func generateFingerprint(_ files: [URL]) throws -> Fingerprint { diff --git a/Sources/XCRemoteCache/Commands/Prebuild/XCPrebuild.swift b/Sources/XCRemoteCache/Commands/Prebuild/XCPrebuild.swift index b4c8dd8c..40f38d45 100644 --- a/Sources/XCRemoteCache/Commands/Prebuild/XCPrebuild.swift +++ b/Sources/XCRemoteCache/Commands/Prebuild/XCPrebuild.swift @@ -158,13 +158,17 @@ public class XCPrebuild { fileAccessor: fileManager ) let artifactProcessor = UnzippedArtifactProcessor(fileRemapper: fileRemapper, dirScanner: fileManager) + let metaUpdater = ArtifactMetaUpdater( + fileRemapper: fileRemapper, + metaWriter: JsonMetaWriter(fileWriter: fileManager, pretty: true) + ) let organizer = ZipArtifactOrganizer( targetTempDir: context.targetTempDir, - artifactProcessors: [artifactProcessor], + artifactProcessors: [artifactProcessor, metaUpdater], fileManager: fileManager ) let metaReader = JsonMetaReader(fileAccessor: fileManager) - var consumerPlugins: [ArtifactConsumerPrebuildPlugin] = [] + var consumerPlugins: [ArtifactConsumerPrebuildPlugin] = [metaUpdater] if config.thinningEnabled { if context.moduleName == config.thinningTargetModuleName, let thinnedTarget = context.thinnedTargets { diff --git a/Sources/XCRemoteCache/Commands/Prepare/Integrate/BuildSettingsIntegrateAppender.swift b/Sources/XCRemoteCache/Commands/Prepare/Integrate/BuildSettingsIntegrateAppender.swift index ec4f4b1c..96658b5d 100644 --- a/Sources/XCRemoteCache/Commands/Prepare/Integrate/BuildSettingsIntegrateAppender.swift +++ b/Sources/XCRemoteCache/Commands/Prepare/Integrate/BuildSettingsIntegrateAppender.swift @@ -76,6 +76,7 @@ class XcodeProjBuildSettingsIntegrateAppender: BuildSettingsIntegrateAppender { ) setBuildSetting(buildSettings: &result, key: "LIPO", value: wrappers.lipo.path ) setBuildSetting(buildSettings: &result, key: "LDPLUSPLUS", value: wrappers.ldplusplus.path ) + setBuildSetting(buildSettings: &result, key: "ASSETCATALOG_EXEC", value: wrappers.actool.path ) } let existingSwiftFlags = result["OTHER_SWIFT_FLAGS"] as? String diff --git a/Sources/XCRemoteCache/Commands/Prepare/Integrate/IntegrateContext.swift b/Sources/XCRemoteCache/Commands/Prepare/Integrate/IntegrateContext.swift index 84cc80d5..b1d5ea10 100644 --- a/Sources/XCRemoteCache/Commands/Prepare/Integrate/IntegrateContext.swift +++ b/Sources/XCRemoteCache/Commands/Prepare/Integrate/IntegrateContext.swift @@ -63,7 +63,8 @@ extension IntegrateContext { ld: binariesDir.appendingPathComponent("xcld"), ldplusplus: binariesDir.appendingPathComponent("xcldplusplus"), prebuild: binariesDir.appendingPathComponent("xcprebuild"), - postbuild: binariesDir.appendingPathComponent("xcpostbuild") + postbuild: binariesDir.appendingPathComponent("xcpostbuild"), + actool: binariesDir.appendingPathComponent("actool") ) self.buildSettingsAppenderOptions = buildSettingsAppenderOptions } diff --git a/Sources/XCRemoteCache/Commands/Prepare/Integrate/XCRCBinariesPaths.swift b/Sources/XCRemoteCache/Commands/Prepare/Integrate/XCRCBinariesPaths.swift index 73ec8344..67be55a1 100644 --- a/Sources/XCRemoteCache/Commands/Prepare/Integrate/XCRCBinariesPaths.swift +++ b/Sources/XCRemoteCache/Commands/Prepare/Integrate/XCRCBinariesPaths.swift @@ -30,4 +30,5 @@ struct XCRCBinariesPaths { let ldplusplus: URL let prebuild: URL let postbuild: URL + let actool: URL } diff --git a/Sources/XCRemoteCache/Commands/Swiftc/Swiftc.swift b/Sources/XCRemoteCache/Commands/Swiftc/Swiftc.swift index 9bc70687..96fad4dc 100644 --- a/Sources/XCRemoteCache/Commands/Swiftc/Swiftc.swift +++ b/Sources/XCRemoteCache/Commands/Swiftc/Swiftc.swift @@ -86,6 +86,13 @@ class Swiftc: SwiftcProtocol { self.plugins = plugins } + // TODO: consider refactoring to a separate entity + private func assetsGeneratedSources(inputFiles: [URL]) -> [URL] { + return inputFiles.filter { url in + url.lastPathComponent == "\(DependencyProcessorImpl.GENERATED_ASSETS_FILENAME).swift" + } + } + // swiftlint:disable:next function_body_length func mockCompilation() throws -> SwiftCResult { let rcModeEnabled = markerReader.canRead() @@ -96,13 +103,14 @@ class Swiftc: SwiftcProtocol { let inputFilesInputs = try inputFileListReader.listFilesURLs() let markerAllowedFiles = try markerReader.listFilesURLs() + let generatedAssetsFiles = assetsGeneratedSources(inputFiles: inputFilesInputs) let cachedDependenciesWriterFactory = CachedFileDependenciesWriterFactory( - dependencies: markerAllowedFiles, + dependencies: markerAllowedFiles + generatedAssetsFiles, fileManager: fileManager, writerFactory: dependenciesWriterFactory ) // Verify all input files to be present in a marker fileList - let disallowedInputs = try inputFilesInputs.filter { try !allowedFilesListScanner.contains($0) } + let disallowedInputs = try inputFilesInputs.filter { try !allowedFilesListScanner.contains($0) && !generatedAssetsFiles.contains($0) } if !disallowedInputs.isEmpty { // New file (disallowedFile) added without modifying the rest of the feature. Fallback to swiftc and diff --git a/Sources/XCRemoteCache/Config/XCRemoteCacheConfig.swift b/Sources/XCRemoteCache/Config/XCRemoteCacheConfig.swift index 067e5d01..556ec616 100644 --- a/Sources/XCRemoteCache/Config/XCRemoteCacheConfig.swift +++ b/Sources/XCRemoteCache/Config/XCRemoteCacheConfig.swift @@ -61,6 +61,8 @@ public struct XCRemoteCacheConfig: Encodable { var swiftcCommand: String = "swiftc" /// Command for a standard Swift frontend compilation (swift-frontend) var swiftFrontendCommand: String = "swift-frontend" + /// Command for the assets catalog tool. By default takes from a symlink that points to the default Xcode location + var actoolCommand: String = "/var/db/xcode_select_link/usr/bin/actool" /// Path of the primary repository that produces cache artifacts var primaryRepo: String = "" /// Main (primary) branch that produces cache artifacts (default to 'master') @@ -177,6 +179,7 @@ extension XCRemoteCacheConfig { merge.modeMarkerPath = scheme.modeMarkerPath ?? modeMarkerPath merge.clangCommand = scheme.clangCommand ?? clangCommand merge.swiftcCommand = scheme.swiftcCommand ?? swiftcCommand + merge.actoolCommand = scheme.actoolCommand ?? actoolCommand merge.primaryRepo = scheme.primaryRepo ?? primaryRepo merge.primaryBranch = scheme.primaryBranch ?? primaryBranch merge.repoRoot = scheme.repoRoot ?? repoRoot @@ -250,6 +253,7 @@ struct ConfigFileScheme: Decodable { let modeMarkerPath: String? let clangCommand: String? let swiftcCommand: String? + let actoolCommand: String? let primaryRepo: String? let primaryBranch: String? let repoRoot: String? @@ -302,6 +306,7 @@ struct ConfigFileScheme: Decodable { case modeMarkerPath = "mode_marker_path" case clangCommand = "clang_command" case swiftcCommand = "swiftc_command" + case actoolCommand = "actool_command" case primaryRepo = "primary_repo" case primaryBranch = "primary_branch" case repoRoot = "repo_root" diff --git a/Sources/XCRemoteCache/Dependencies/DependenciesMapping.swift b/Sources/XCRemoteCache/Dependencies/DependenciesMapping.swift index 460e0152..560437f0 100644 --- a/Sources/XCRemoteCache/Dependencies/DependenciesMapping.swift +++ b/Sources/XCRemoteCache/Dependencies/DependenciesMapping.swift @@ -22,5 +22,5 @@ import Foundation enum DependenciesMapping { /// Specifies which ENVs should be rewritten in the dependencies generation to make generic (paths agnostics) /// list of dependencies - static let rewrittenEnvs = ["BUILD_DIR", "SRCROOT"] + static let rewrittenEnvs = ["DERIVED_FILE_DIR", "BUILD_DIR", "SRCROOT"] } diff --git a/Sources/XCRemoteCache/Dependencies/DependencyProcessor.swift b/Sources/XCRemoteCache/Dependencies/DependencyProcessor.swift index fc20375b..ffa3f900 100644 --- a/Sources/XCRemoteCache/Dependencies/DependencyProcessor.swift +++ b/Sources/XCRemoteCache/Dependencies/DependencyProcessor.swift @@ -32,6 +32,8 @@ public struct Dependency: Equatable { case ownProduct // User-excluded path case userExcluded + // the on-fly .swift/.h file that contains all assets references (colors&images) + case generatedAssetSymbol case unknown } @@ -44,16 +46,29 @@ public struct Dependency: Equatable { } } +/// A pseudo-type that classifies all dependencies into two buckets: +/// fingerprintScoped: all these files have to be included in the fingerprint +/// extra: all other deps +typealias DependencyProcessorResult = ( + fingerprintScoped: [Dependency], + assetsSource: [Dependency], + extra: [Dependency] +) + /// Processes raw compilation URL dependencies from .d files protocol DependencyProcessor { /// Processes a list of dependencies and provides a list of project-specific dependencies /// - Parameter files: raw dependency locations /// - Returns: array of project-specific dependencies - func process(_ files: [URL]) -> [Dependency] + func process(_ files: [URL]) -> DependencyProcessorResult } /// Classifies raw dependencies and strips irrelevant dependencies class DependencyProcessorImpl: DependencyProcessor { + /// Name of a file that is auto-generated by Xcode with all assets references and placed in + /// the DerivedSources/ dir + static let GENERATED_ASSETS_FILENAME = "GeneratedAssetSymbols" + private let xcodePath: String private let productPath: String private let sourcePath: String @@ -72,9 +87,25 @@ class DependencyProcessorImpl: DependencyProcessor { self.skippedRegexes = skippedRegexes } - func process(_ files: [URL]) -> [Dependency] { + func process(_ files: [URL]) -> DependencyProcessorResult { let dependencies = classify(files) - return dependencies.filter(isRelevantDependency) + return split(allDependencies: dependencies) + } + + private func split(allDependencies: [Dependency]) -> DependencyProcessorResult { + var fingerprintScoped: [Dependency] = [] + var assetsSources: [Dependency] = [] + var extra: [Dependency] = [] + allDependencies.forEach { dep in + if isFingerprintRelevantDependency(dep) { + fingerprintScoped.append(dep) + } else if isAssetsSourcesDependency(dep) { + assetsSources.append(dep) + } else { + extra.append(dep) + } + } + return (fingerprintScoped, assetsSources, extra) } private func classify(_ files: [URL]) -> [Dependency] { @@ -84,7 +115,17 @@ class DependencyProcessorImpl: DependencyProcessor { return Dependency(url: file, type: .userExcluded) } else if filePath.hasPrefix(xcodePath) { return Dependency(url: file, type: .xcode) - } else if filePath.hasPrefix(intermediatePath) { + } else if filePath.hasPrefix(derivedFilesPath) && + filePath.hasSuffix("/\(Self.self.GENERATED_ASSETS_FILENAME).swift") || + filePath.hasSuffix("/\(Self.self.GENERATED_ASSETS_FILENAME).h" + ) { + // Starting Xcode 15.0, the filename of that file is static + // and placed in DerivedSources, e.g. + // /DerivedData/Build/Intermediates.noindex/xxx.build/Debug-iphonesimulator/xxx.build/DerivedSources/GeneratedAssetSymbols.swift + + return Dependency(url: file, type: .generatedAssetSymbol) + } + else if filePath.hasPrefix(intermediatePath) { return Dependency(url: file, type: .intermediate) } else if filePath.hasPrefix(derivedFilesPath) { return Dependency(url: file, type: .derivedFile) @@ -102,7 +143,7 @@ class DependencyProcessorImpl: DependencyProcessor { } } - private func isRelevantDependency(_ dependency: Dependency) -> Bool { + private func isFingerprintRelevantDependency(_ dependency: Dependency) -> Bool { // Generated modulemaps may not be an actual dependency. Swift selects them as a // dependency because these contribute to the final module context but doesn't mean that given module has // been imported and it should invalidate current target when modified @@ -115,17 +156,22 @@ class DependencyProcessorImpl: DependencyProcessor { // Skip: // - A fingerprint generated includes Xcode version build number so no need to analyze prepackaged Xcode files - // - All files in `*/Interemediates/*` - this file are created on-fly for a given target + // - All files in `*/Interemediates/*` - this file are created on-fly for a given target (except of GeneratedAssetSymbols.swift) // - Some files may depend on its own product (e.g. .m may #include *-Swift.h) - we know products will match // because in case of a hit, these will be taken from the artifact // - Customized DERIVED_FILE_DIR may change a directory of // derived files, which by default is under `*/Interemediates` // - User-specified (in .rcinfo) files to exclude + // - on-fly generated assets symbols, as these will not be available yet when a prebuild script is invoked let irrelevantDependenciesType: [Dependency.Kind] = [ - .xcode, .intermediate, .ownProduct, .derivedFile, .userExcluded, + .xcode, .intermediate, .ownProduct, .derivedFile, .userExcluded, .generatedAssetSymbol, ] return !irrelevantDependenciesType.contains(dependency.type) } + + private func isAssetsSourcesDependency(_ dependency: Dependency) -> Bool { + dependency.type == .generatedAssetSymbol + } } fileprivate extension String { diff --git a/Sources/XCRemoteCache/Dependencies/MarkerReader.swift b/Sources/XCRemoteCache/Dependencies/MarkerReader.swift index 424cfa52..3ad8baf3 100644 --- a/Sources/XCRemoteCache/Dependencies/MarkerReader.swift +++ b/Sources/XCRemoteCache/Dependencies/MarkerReader.swift @@ -22,12 +22,12 @@ import Foundation /// Reads a list of files from a marker file class FileMarkerReader: ListReader { private let file: URL - private let fileManager: FileManager + private let fileReader: FileReader private var cachedFiles: [URL]? - init(_ file: URL, fileManager: FileManager) { + init(_ file: URL, fileManager: FileReader) { self.file = file - self.fileManager = fileManager + self.fileReader = fileManager } func listFilesURLs() throws -> [URL] { @@ -45,6 +45,6 @@ class FileMarkerReader: ListReader { } func canRead() -> Bool { - return fileManager.fileExists(atPath: file.path) + return fileReader.fileExists(atPath: file.path) } } diff --git a/Sources/XCRemoteCache/Models/MainArtifactMeta.swift b/Sources/XCRemoteCache/Models/MainArtifactMeta.swift index 8cd6dcb9..aeb51b93 100644 --- a/Sources/XCRemoteCache/Models/MainArtifactMeta.swift +++ b/Sources/XCRemoteCache/Models/MainArtifactMeta.swift @@ -42,4 +42,55 @@ struct MainArtifactMeta: Meta, Equatable { var inputs: [String] /// Extra keys added by meta plugins var pluginsKeys: [String: String] + /// A list of internal assets compiler sources that should be verified on a consumer side + /// after those derived sources have been generated + var assetsSources: [String] + /// The cumulative fingerprint of all `assetsSources` + var assetsSourcesFingerprint: String + + init( + dependencies: [String], + fileKey: String, + rawFingerprint: String, + generationCommit: String, + targetName: String, + configuration: String, + platform: String, + xcode: String, + inputs: [String], + pluginsKeys: [String : String], + assetsSources: [String], + assetsSourcesFingerprint: String + ) { + self.dependencies = dependencies + self.fileKey = fileKey + self.rawFingerprint = rawFingerprint + self.generationCommit = generationCommit + self.targetName = targetName + self.configuration = configuration + self.platform = platform + self.xcode = xcode + self.inputs = inputs + self.pluginsKeys = pluginsKeys + self.assetsSources = assetsSources + self.assetsSourcesFingerprint = assetsSourcesFingerprint + } + + + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.dependencies = try container.decode([String].self, forKey: .dependencies) + self.fileKey = try container.decode(String.self, forKey: .fileKey) + self.rawFingerprint = try container.decode(String.self, forKey: .rawFingerprint) + self.generationCommit = try container.decode(String.self, forKey: .generationCommit) + self.targetName = try container.decode(String.self, forKey: .targetName) + self.configuration = try container.decode(String.self, forKey: .configuration) + self.platform = try container.decode(String.self, forKey: .platform) + self.xcode = try container.decode(String.self, forKey: .xcode) + self.inputs = try container.decode([String].self, forKey: .inputs) + self.pluginsKeys = try container.decode([String : String].self, forKey: .pluginsKeys) + self.assetsSources = (try? container.decode([String].self, forKey: .assetsSources)) ?? [] + self.assetsSourcesFingerprint = (try? container.decode(String.self, forKey: .assetsSourcesFingerprint)) ?? "" + } } diff --git a/Sources/xcactool/XCACToolMain.swift b/Sources/xcactool/XCACToolMain.swift new file mode 100644 index 00000000..2fee50bb --- /dev/null +++ b/Sources/xcactool/XCACToolMain.swift @@ -0,0 +1,71 @@ +// Copyright (c) 2023 Spotify AB. +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import Foundation +import XCRemoteCache + +/// Wrapper for a `actool` +public class XCACToolMain { + public func main() { + let args = ProcessInfo().arguments + var objcOutput: String? + var swiftOutput: String? + var i = 0 + while i < args.count { + switch args[i] { + case "--generate-swift-asset-symbols": + swiftOutput = args[i + 1] + i += 1 + case "--generate-objc-asset-symbols": + objcOutput = args[i + 1] + i += 1 + default: + break + } + i += 1 + } + if objcOutput == nil && swiftOutput == nil { + // no need to run the wrapper body. Possible scenarios: + // - Xcode 14 or older + // - probe invocation (e.g. --version) + // - compiling asset(s) + // etc. + + let acCommand = "/var/db/xcode_select_link/usr/bin/actool" + + let args = ProcessInfo().arguments + let paramList = [acCommand] + args.dropFirst() + let cargs = paramList.map { strdup($0) } + [nil] + execvp(acCommand, cargs) + + /// C-function `execv` returns only when the command fails + exit(1) + } + + do { + try XCACTool( + args: args, + objcOutput: objcOutput, + swiftOutput: swiftOutput + ).run() + } catch { + exit(1, "Failed with: \(error). Args: \(args)") + } + } +} diff --git a/Sources/xcactool/main.swift b/Sources/xcactool/main.swift new file mode 100644 index 00000000..44577808 --- /dev/null +++ b/Sources/xcactool/main.swift @@ -0,0 +1,22 @@ +// Copyright (c) 2023 Spotify AB. +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import XCRemoteCache + +XCACToolMain().main() diff --git a/Tests/XCRemoteCacheTests/Artifacts/ArtifactMetaPathProviderTests.swift b/Tests/XCRemoteCacheTests/Artifacts/ArtifactMetaPathProviderTests.swift new file mode 100644 index 00000000..58003cb6 --- /dev/null +++ b/Tests/XCRemoteCacheTests/Artifacts/ArtifactMetaPathProviderTests.swift @@ -0,0 +1,68 @@ +// Copyright (c) 2023 Spotify AB. +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +@testable import XCRemoteCache +import XCTest + +class ArtifactMetaPathProviderTests: XCTestCase { + private let artifactURL: URL = "/artifact/" + private let dirScannerFake = FileAccessorFake(mode: .normal) + + + func testFindsExistingMeta() throws { + let provider = ArtifactMetaPathProvider( + artifactLocation: artifactURL, + dirScanner: dirScannerFake + ) + try dirScannerFake.write(toPath: "/artifact/abc.json", contents: nil) + + XCTAssertEqual(try provider.getMetaPath(), "/artifact/abc.json") + } + + func testThrowsWhenNoJsonFileInTheArtifact() throws { + let provider = ArtifactMetaPathProvider( + artifactLocation: artifactURL, + dirScanner: dirScannerFake + ) + + XCTAssertThrowsError(try provider.getMetaPath()) { error in + switch error { + case MetaPathProviderError.failed: break + default: + XCTFail("Not expected error") + } + } + } + + func testDoesntSearchMetaInRecursiveDirs() throws { + let provider = ArtifactMetaPathProvider( + artifactLocation: artifactURL, + dirScanner: dirScannerFake + ) + try dirScannerFake.write(toPath: "/artifact/nested/abc.json", contents: nil) + + XCTAssertThrowsError(try provider.getMetaPath()) { error in + switch error { + case MetaPathProviderError.failed: break + default: + XCTFail("Not expected error") + } + } + } +} diff --git a/Tests/XCRemoteCacheTests/Commands/Prepare/Integrate/XcodeProjBuildSettingsIntegrateAppenderTests.swift b/Tests/XCRemoteCacheTests/Commands/Prepare/Integrate/XcodeProjBuildSettingsIntegrateAppenderTests.swift index 819323c7..167db80f 100644 --- a/Tests/XCRemoteCacheTests/Commands/Prepare/Integrate/XcodeProjBuildSettingsIntegrateAppenderTests.swift +++ b/Tests/XCRemoteCacheTests/Commands/Prepare/Integrate/XcodeProjBuildSettingsIntegrateAppenderTests.swift @@ -38,7 +38,8 @@ class XcodeProjBuildSettingsIntegrateAppenderTests: XCTestCase { ld: binariesDir.appendingPathComponent("xcld"), ldplusplus: binariesDir.appendingPathComponent("xcldplusplus"), prebuild: binariesDir.appendingPathComponent("xcprebuild"), - postbuild: binariesDir.appendingPathComponent("xcpostbuild") + postbuild: binariesDir.appendingPathComponent("xcpostbuild"), + actool: binariesDir.appendingPathComponent("actool") ) } diff --git a/Tests/XCRemoteCacheTests/Dependencies/DependencyProcessorImplTests.swift b/Tests/XCRemoteCacheTests/Dependencies/DependencyProcessorImplTests.swift index 6ae2daaa..a2da04bb 100644 --- a/Tests/XCRemoteCacheTests/Dependencies/DependencyProcessorImplTests.swift +++ b/Tests/XCRemoteCacheTests/Dependencies/DependencyProcessorImplTests.swift @@ -45,7 +45,7 @@ class DependencyProcessorImplTests: FileXCTestCase { ) XCTAssertEqual( - processor.process([intermediateFile]), + processor.process([intermediateFile]).fingerprintScoped, [] ) } @@ -63,7 +63,7 @@ class DependencyProcessorImplTests: FileXCTestCase { ) XCTAssertEqual( - processor.process([bundleFile]), + processor.process([bundleFile]).fingerprintScoped, [] ) } @@ -73,7 +73,7 @@ class DependencyProcessorImplTests: FileXCTestCase { "/DerivedFiles/ModuleName-Swift.h", ]) - XCTAssertEqual(dependencies, []) + XCTAssertEqual(dependencies.fingerprintScoped, []) } func testFiltersOutDerivedFile() throws { @@ -81,7 +81,7 @@ class DependencyProcessorImplTests: FileXCTestCase { "/DerivedFiles/output.h", ]) - XCTAssertEqual(dependencies, []) + XCTAssertEqual(dependencies.fingerprintScoped, []) } func testFiltersOutProductModulemap() throws { @@ -89,7 +89,7 @@ class DependencyProcessorImplTests: FileXCTestCase { "/Product/some.modulemap", ]) - XCTAssertEqual(dependencies, []) + XCTAssertEqual(dependencies.fingerprintScoped, []) } func testDoesNotFilterOutOtherProductModulemap() throws { @@ -97,7 +97,7 @@ class DependencyProcessorImplTests: FileXCTestCase { "/ProductOther/some.modulemap", ]) - XCTAssertEqual(dependencies, [.init(url: "/ProductOther/some.modulemap", type: .unknown)]) + XCTAssertEqual(dependencies.fingerprintScoped, [.init(url: "/ProductOther/some.modulemap", type: .unknown)]) } func testDoesNotFilterOutNonProductModulemap() throws { @@ -105,7 +105,7 @@ class DependencyProcessorImplTests: FileXCTestCase { "/Source/some.modulemap", ]) - XCTAssertEqual(dependencies, [.init(url: "/Source/some.modulemap", type: .source)]) + XCTAssertEqual(dependencies.fingerprintScoped, [.init(url: "/Source/some.modulemap", type: .source)]) } func testFiltersOutXcodeFiles() throws { @@ -113,7 +113,7 @@ class DependencyProcessorImplTests: FileXCTestCase { "/Xcode/some", ]) - XCTAssertEqual(dependencies, []) + XCTAssertEqual(dependencies.fingerprintScoped, []) } func testFiltersOutIntermediateFiles() throws { @@ -121,7 +121,7 @@ class DependencyProcessorImplTests: FileXCTestCase { "/Intermediate/some", ]) - XCTAssertEqual(dependencies, []) + XCTAssertEqual(dependencies.fingerprintScoped, []) } func testFiltersOutBundleFiles() throws { @@ -129,7 +129,7 @@ class DependencyProcessorImplTests: FileXCTestCase { "/Bundle/some", ]) - XCTAssertEqual(dependencies, []) + XCTAssertEqual(dependencies.fingerprintScoped, []) } func testDoesNotFilterOutUnknownFiles() throws { @@ -137,7 +137,7 @@ class DependencyProcessorImplTests: FileXCTestCase { "/xxx/some", ]) - XCTAssertEqual(dependencies, [.init(url: "/xxx/some", type: .unknown)]) + XCTAssertEqual(dependencies.fingerprintScoped, [.init(url: "/xxx/some", type: .unknown)]) } func testFiltersOutIntermediateBySymlink() throws { @@ -167,7 +167,7 @@ class DependencyProcessorImplTests: FileXCTestCase { intermediateFileSymlink, ]) - XCTAssertEqual(dependencies, []) + XCTAssertEqual(dependencies.fingerprintScoped, []) } func testDoesNotFilterOutSourceBySymlink() throws { @@ -197,7 +197,7 @@ class DependencyProcessorImplTests: FileXCTestCase { sourceFileSymlink, ]) - XCTAssertEqual(dependencies, [.init(url: sourceFileSymlink, type: .source)]) + XCTAssertEqual(dependencies.fingerprintScoped, [.init(url: sourceFileSymlink, type: .source)]) } /** @@ -229,7 +229,7 @@ class DependencyProcessorImplTests: FileXCTestCase { ) XCTAssertEqual( - processor.process([derivedFile]), + processor.process([derivedFile]).fingerprintScoped, [] ) } @@ -247,7 +247,7 @@ class DependencyProcessorImplTests: FileXCTestCase { ) XCTAssertEqual( - processor.process([source]), + processor.process([source]).fingerprintScoped, [] ) } @@ -265,7 +265,7 @@ class DependencyProcessorImplTests: FileXCTestCase { ) XCTAssertEqual( - processor.process([derivedModulemap]), + processor.process([derivedModulemap]).fingerprintScoped, [] ) } @@ -283,7 +283,7 @@ class DependencyProcessorImplTests: FileXCTestCase { ) XCTAssertEqual( - processor.process([source]), + processor.process([source]).fingerprintScoped, [.init(url: source, type: .source)] ) } diff --git a/Tests/XCRemoteCacheTests/TestDoubles/MainArtifactSampleMeta.swift b/Tests/XCRemoteCacheTests/TestDoubles/MainArtifactSampleMeta.swift index 4d3557d8..1c6986f6 100644 --- a/Tests/XCRemoteCacheTests/TestDoubles/MainArtifactSampleMeta.swift +++ b/Tests/XCRemoteCacheTests/TestDoubles/MainArtifactSampleMeta.swift @@ -31,6 +31,8 @@ enum MainArtifactSampleMeta { platform: "", xcode: "", inputs: [], - pluginsKeys: [:] + pluginsKeys: [:], + assetsSources: [], + assetsSourcesFingerprint: "" ) }