diff --git a/Sources/ScipioKit/Producer/FrameworkProducer.swift b/Sources/ScipioKit/Producer/FrameworkProducer.swift index cfb5be6f..33b1c8d4 100644 --- a/Sources/ScipioKit/Producer/FrameworkProducer.swift +++ b/Sources/ScipioKit/Producer/FrameworkProducer.swift @@ -104,23 +104,36 @@ struct FrameworkProducer { buildOptions: buildOptionsForProduct ) }) + let pinsStore = try descriptionPackage.workspace.pinsStore.load() + let cacheSystem = CacheSystem( + pinsStore: pinsStore, + outputDirectory: outputDir, + storage: cacheStorage + ) - let cacheSystem = CacheSystem(pinsStore: pinsStore, - outputDirectory: outputDir, - storage: cacheStorage) - let cacheEnabledTargets: Set + let targetsToBuild: OrderedSet if cacheMode.isConsumingCacheEnabled { - cacheEnabledTargets = await restoreAllAvailableCaches( - availableTargets: Set(allTargets), + let targets = Set(allTargets) + + // Validate the existing frameworks in `outputDir` before restoration + let valid = await validateExistingFrameworks( + availableTargets: targets, + cacheSystem: cacheSystem + ) + + let restored = await restoreAllAvailableCaches( + availableTargets: targets.subtracting(valid), cacheSystem: cacheSystem ) + + targetsToBuild = allTargets + .subtracting(valid) + .subtracting(restored) } else { - cacheEnabledTargets = [] + targetsToBuild = allTargets } - let targetsToBuild = allTargets.subtracting(cacheEnabledTargets) - for target in targetsToBuild { try await buildXCFrameworks( target, @@ -141,6 +154,46 @@ struct FrameworkProducer { } } + private func validateExistingFrameworks( + availableTargets: Set, + cacheSystem: CacheSystem + ) async -> Set { + let chunked = availableTargets.chunks(ofCount: CacheSystem.defaultParalellNumber) + + var validFrameworks: Set = [] + for chunk in chunked { + await withTaskGroup(of: CacheSystem.CacheTarget?.self) { group in + for target in chunk { + group.addTask { [outputDir, fileSystem] in + do { + let product = target.buildProduct + let outputPath = outputDir.appendingPathComponent(product.frameworkName) + let exists = fileSystem.exists(outputPath.absolutePath) + guard exists else { return nil } + + let expectedCacheKey = try await cacheSystem.calculateCacheKey(of: target) + let isValidCache = await cacheSystem.existsValidCache(cacheKey: expectedCacheKey) + guard isValidCache else { return nil } + + let expectedCacheKeyHash = try expectedCacheKey.calculateChecksum() + logger.info( + // swiftlint:disable:next line_length + "✅ Valid \(product.target.name).xcframework (\(expectedCacheKeyHash)) exists. Skip restoring or building.", metadata: .color(.green) + ) + return target + } catch { + return nil + } + } + } + for await case let target? in group { + validFrameworks.insert(target) + } + } + } + return validFrameworks + } + private func restoreAllAvailableCaches( availableTargets: Set, cacheSystem: CacheSystem @@ -149,7 +202,7 @@ struct FrameworkProducer { var restored: Set = [] for chunk in chunked { - let restorer = Restorer(outputDir: outputDir, cacheMode: cacheMode, fileSystem: fileSystem) + let restorer = Restorer(outputDir: outputDir, fileSystem: fileSystem) await withTaskGroup(of: CacheSystem.CacheTarget?.self) { group in for target in chunk { group.addTask { @@ -175,7 +228,6 @@ struct FrameworkProducer { /// Sendable interface to provide restore caches private struct Restorer: Sendable { let outputDir: URL - let cacheMode: Runner.Options.CacheMode let fileSystem: any FileSystem // Return true if pre-built artifact is available (already existing or restored from cache) @@ -185,40 +237,31 @@ struct FrameworkProducer { ) async throws -> Bool { let product = target.buildProduct let frameworkName = product.frameworkName - let outputPath = outputDir.appendingPathComponent(product.frameworkName) + let outputPath = outputDir.appendingPathComponent(frameworkName) let exists = fileSystem.exists(outputPath.absolutePath) - guard cacheMode.isConsumingCacheEnabled else { - return false - } let expectedCacheKey = try await cacheSystem.calculateCacheKey(of: target) - let isValidCache = await cacheSystem.existsValidCache(cacheKey: expectedCacheKey) let expectedCacheKeyHash = try expectedCacheKey.calculateChecksum() - if isValidCache && exists { - logger.info( - "✅ Valid \(product.target.name).xcframework (\(expectedCacheKeyHash)) is exists. Skip building.", metadata: .color(.green) - ) + + if exists { + logger.warning("⚠️ Existing \(frameworkName) is outdated.", metadata: .color(.yellow)) + logger.info("🗑️ Delete \(frameworkName)", metadata: .color(.red)) + try fileSystem.removeFileTree(outputPath.absolutePath) + } + + let restoreResult = await cacheSystem.restoreCacheIfPossible(target: target) + switch restoreResult { + case .succeeded: + logger.info("✅ Restore \(frameworkName) (\(expectedCacheKeyHash)) from cache storage.", metadata: .color(.green)) return true - } else { - if exists { - logger.warning("⚠️ Existing \(frameworkName) is outdated.", metadata: .color(.yellow)) - logger.info("🗑️ Delete \(frameworkName)", metadata: .color(.red)) - try fileSystem.removeFileTree(outputPath.absolutePath) - } - let restoreResult = await cacheSystem.restoreCacheIfPossible(target: target) - switch restoreResult { - case .succeeded: - logger.info("✅ Restore \(frameworkName) (\(expectedCacheKeyHash)) from cache storage.", metadata: .color(.green)) - return true - case .failed(let error): - logger.warning("⚠️ Restoring \(frameworkName) (\(expectedCacheKeyHash)) is failed", metadata: .color(.yellow)) - if let description = error?.errorDescription { - logger.warning("\(description)", metadata: .color(.yellow)) - } - return false - case .noCache: - return false + case .failed(let error): + logger.warning("⚠️ Restoring \(frameworkName) (\(expectedCacheKeyHash)) is failed", metadata: .color(.yellow)) + if let description = error?.errorDescription { + logger.warning("\(description)", metadata: .color(.yellow)) } + return false + case .noCache: + return false } } }