Sources/XCRemoteCache/Commands/Swiftc/Swiftc.swift (129 lines of code) (raw):

// Copyright (c) 2021 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 SwiftCResult { /// Swiftc mock cannot be used and fallback to the compilation is required case forceFallback /// All compilation steps were mocked correctly case success } /// Swiftc mocking compilation protocol SwiftcProtocol { /// Tries to performs mocked compilation (moving all cached files to the expected location) /// If cached compilation products are not valid or incompatible, fallbacks to build-from-source /// - Returns: `.forceFallback` if the cached compilation products are incompatible and fallback /// to a standard 'swiftc' is required, `.success` otherwise /// - Throws: An error if there was an unrecoverable, serious error (e.g. IO error) func mockCompilation() throws -> SwiftCResult } /// Swiftc wrapper that mocks compilation with noop and moves all expected products from cache location class Swiftc: SwiftcProtocol { /// Reader of all input files of the compilation private let inputFileListReader: ListReader /// Reader of the marker file lists - list of dependencies to set for swiftc compilation private let markerReader: ListReader /// Checks if the input file exists in the file list private let allowedFilesListScanner: FileListScanner /// Manager of the downloaded artifact package private let artifactOrganizer: ArtifactOrganizer /// Reads all input and output files for the compilation from an input filemap private let inputFilesReader: SwiftcInputReader /// Write manager of the marker file private let markerWriter: MarkerWriter /// Generates products at the desired destination private let productsGenerator: SwiftcProductsGenerator private let context: SwiftcContext private let fileManager: FileManager private let dependenciesWriterFactory: (URL, FileManager) -> DependenciesWriter private let touchFactory: (URL, FileManager) -> Touch private let plugins: [SwiftcProductGenerationPlugin] private let allowedInputDeterminer: AllowedInputDeterminer init( inputFileListReader: ListReader, markerReader: ListReader, allowedFilesListScanner: FileListScanner, artifactOrganizer: ArtifactOrganizer, inputReader: SwiftcInputReader, context: SwiftcContext, markerWriter: MarkerWriter, productsGenerator: SwiftcProductsGenerator, fileManager: FileManager, dependenciesWriterFactory: @escaping (URL, FileManager) -> DependenciesWriter, touchFactory: @escaping (URL, FileManager) -> Touch, plugins: [SwiftcProductGenerationPlugin], allowedInputDeterminer: AllowedInputDeterminer ) { self.inputFileListReader = inputFileListReader self.markerReader = markerReader self.allowedFilesListScanner = allowedFilesListScanner self.artifactOrganizer = artifactOrganizer inputFilesReader = inputReader self.context = context self.markerWriter = markerWriter self.productsGenerator = productsGenerator self.fileManager = fileManager self.dependenciesWriterFactory = dependenciesWriterFactory self.touchFactory = touchFactory self.plugins = plugins self.allowedInputDeterminer = allowedInputDeterminer } // swiftlint:disable:next function_body_length func mockCompilation() throws -> SwiftCResult { let rcModeEnabled = markerReader.canRead() guard rcModeEnabled else { infoLog("Swiftc marker doesn't exist") return .forceFallback } let inputFilesInputs = try inputFileListReader.listFilesURLs() let markerAllowedFiles = try markerReader.listFilesURLs() let allDependencies = Set(markerAllowedFiles + inputFilesInputs) let cachedDependenciesWriterFactory = CachedFileDependenciesWriterFactory( dependencies: Array(allDependencies), fileManager: fileManager, writerFactory: dependenciesWriterFactory ) // Verify all input files to be present in a marker fileList let disallowedInputs = try inputFilesInputs.filter { file in try !allowedFilesListScanner.contains(file) && !allowedInputDeterminer.allowedNonDependencyInput(file: file) } if !disallowedInputs.isEmpty { // New file (disallowedFile) added without modifying the rest of the feature. Fallback to swiftc and // ensure that compilation from source will be forced up until next merge/rebase with "primary" branch infoLog("Swiftc new input file \(disallowedInputs)") // Deleting marker to indicate that the remote cached artifact cannot be used try markerWriter.disable() // Save custom prebuild discovery content to make sure that the following prebuild // phase will not try to reuse cached artifact (if present) // In other words: let prebuild know that it should not try to reenable cache // until the next merge with primary switch context.mode { case .consumer(commit: .available(let remoteCommit)): let prebuildDiscoveryURL = context.tempDir.appendingPathComponent(context.prebuildDependenciesPath) let prebuildDiscoverWriter = dependenciesWriterFactory(prebuildDiscoveryURL, fileManager) try prebuildDiscoverWriter.write(skipForSha: remoteCommit) case .consumer, .producer, .producerFast: // Never skip prebuild phase and fallback to the swiftc compilation for: // 1) Not enabled remote cache, 2) producer(s) break } return .forceFallback } let artifactLocation = artifactOrganizer.getActiveArtifactLocation() // Read swiftmodule location from XCRemoteCache // arbitrary format swiftmodule/${arch}/${moduleName}.swift{module|doc|sourceinfo} let moduleName = context.moduleName let allCompilations = try inputFilesReader.read() let artifactSwiftmoduleDir = artifactLocation .appendingPathComponent("swiftmodule") .appendingPathComponent(context.arch) let artifactSwiftmoduleBase = artifactSwiftmoduleDir.appendingPathComponent(moduleName) let artifactSwiftmoduleFiles = Dictionary( uniqueKeysWithValues: SwiftmoduleFileExtension.SwiftmoduleExtensions .map { ext, _ in (ext, artifactSwiftmoduleBase.appendingPathExtension(ext.rawValue)) } ) // emit module (if requested) if let emitModule = context.steps.emitModule { // Build -Swift.h location from XCRemoteCache arbitrary format include/${arch}/${target}-Swift.h let artifactSwiftModuleObjCDir = artifactLocation .appendingPathComponent("include") .appendingPathComponent(context.arch) .appendingPathComponent(context.moduleName) // Move cached xxxx-Swift.h to the location passed in arglist // Alternatively, artifactSwiftModuleObjCFile could be built as a first .h // file in artifactSwiftModuleObjCDir let artifactSwiftModuleObjCFile = artifactSwiftModuleObjCDir .appendingPathComponent(emitModule.objcHeaderOutput.lastPathComponent) _ = try productsGenerator.generateFrom( artifactSwiftModuleFiles: artifactSwiftmoduleFiles, artifactSwiftModuleObjCFile: artifactSwiftModuleObjCFile ) } try plugins.forEach { try $0.generate(for: allCompilations) } // Save individual .d and touch .o for each .swift file for compilation in allCompilations.files { if let object = compilation.object { // Touching .o is required to invalidate already existing .a or linked library let touch = touchFactory(object, fileManager) try touch.touch() } if let individualDeps = compilation.dependencies { // swiftc product should be invalidated if any of dependencies file has changed try cachedDependenciesWriterFactory.generate(output: individualDeps) } } // Save .d for the entire module (might not be required in the `swift-frontend -c` mode) if let swiftDependencies = allCompilations.info.swiftDependencies { try cachedDependenciesWriterFactory.generate(output: swiftDependencies) } // Generate .d file with all deps in the "-master.d" (e.g. for WMO) if let wmoDeps = allCompilations.info.dependencies { try cachedDependenciesWriterFactory.generate(output: wmoDeps) } infoLog("Swiftc noop for \(context.target)") return .success } }