Sources/XCRemoteCache/Config/XCRemoteCacheConfig.swift (280 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.
// swiftlint:disable file_length
import Foundation
import Yams
public enum XCRemoteCacheConfigError: Error {
/// Obligatory configuration property is missing
case missingConfiguration(name: String)
}
public struct XCRemoteCacheConfig: Encodable {
/// Remote cache schema version. Bump that version if RC artifact generation introduces breaking changes
let schemaVersion = "5"
/// Mode: consumer|producer, defaults to consumer
var mode: Mode = .consumer
/// Address of all remote cache replicas. The best one (with the quickest response) will be chose in xcprepare step
/// Required to be non-empty array
var cacheAddresses: [String] = []
/// Address of the remote cache to use in the consumer mode
/// If not specified, the first item in `cacheAddresses` will be used
var recommendedCacheAddress: String = ""
/// Probe request path to the `cacheAddresses` (relative to `cacheAddresses`)
/// that determines the best cache to use (with the lowest latency)
var cacheHealthPath: String = "nginx-health"
/// Number of `cacheAddresses` probe requests
var cacheHealthPathProbeCount: Int = 3
/// Filepath to the file to the remote commit sha
var remoteCommitFile: String = "build/remote-cache/arc.rc"
/// Filepath to create xccc wrapper (that value should be equal to Xcode's CC BuildSetting)
var xcccFile: String = "build/bin/xccc"
/// Path, relative to $TARGET_TEMP_DIR which specifies prebuild discovery .d file
var prebuildDiscoveryPath: String = "prebuild.d"
/// Path, relative to $TARGET_TEMP_DIR which specifies postbuild discovery .d file
var postbuildDiscoveryPath: String = "postbuild.d"
/// Path, relative to $TARGET_TEMP_DIR of a maker file to enable (when exists) or disable (when missing)
/// Remote cache mode
/// Includes a list of all allowed input files to use remote cache
var modeMarkerPath: String = "rc.enabled"
/// Command for a standard C compilation (cc)
var clangCommand: String = "clang"
/// Command for a standard Swift compilation (swiftc)
var swiftcCommand: String = "swiftc"
/// Command for a standard Swift frontend compilation (swift-frontend)
var swiftFrontendCommand: String = "swift-frontend"
/// Path of the primary repository that produces cache artifacts
var primaryRepo: String = ""
/// Main (primary) branch that produces cache artifacts (default to 'master')
var primaryBranch: String = "master"
/// Path to the git repo root
var repoRoot: String = "."
/// Number of historical commits to look for a cache artifacts
var cacheCommitHistory: Int = 10
/// Source root of the Xcode project
var sourceRoot: String
/// Fingerprint override extension (sample override `Module.swiftmodule/x86_64.swiftmodule.md5`)
var fingerprintOverrideExtension: String = "md5"
/// Optional configuration file that overrides project configuration
var extraConfigurationFile: String = "user.rcinfo"
/// Custom commit sha to publish artifact
var publishingSha: String?
/// Maximum age in days for artifact to be cached before being evicted
var artifactMaximumAge: Int = 30
/// Extra ENV keys that should be convoluted into the environment fingerprint
var customFingerprintEnvs: [String] = []
/// Root directory where all XCRemoteCache statistics (e.g. counters) are stored
var statsDir: String = "~/.xccache"
/// Number of retries for download requests
var downloadRetries: Int = 0
/// Number of retries for upload requests
var uploadRetries: Int = 3
/// Delay between retries in seconds
var retryDelay: Double = 10.0
/// Maximum number of simultaneous requests. 0 means no limits
var uploadBatchSize: Int = 0
/// Extra headers appended to all remote HTTP(S) requests
var requestCustomHeaders: [String: String] = [:]
/// Filename (without an extension) of the compilation input file that is used
/// as a fake compilation for the forced-cached target (aka thin target)
/// The filename has to be exclusive nor a suffix of any compilation file in a target
var thinTargetMockFilename: String = "standin"
/// A List of all targets that are not thin. If an empty array, all targets are meant to be non-thin
/// A 'thin' target is a target-level mode that forces the cached artifact
var focusedTargets: [String] = []
/// Disable cache for http requests to fecth metadata and download artifacts
var disableHttpCache: Bool = false
/// Path, relative to $TARGET_TEMP_DIR which gathers all compilation commands that should be e
/// xecuted if a target switches to local compilation
/// Example: A new `.swift` file invalidates remote arXcodeProjIntegrate.swifttifact and triggers local compilation
/// When that happens, all previously skipped clang build steps
/// need to be eventually called locally - this file lists all these commands
var compilationHistoryFile: String = "history.compile"
/// Timeout for remote response data interval (in seconds). If an interval between data chunks is
/// longer than a timeout, a request fails
var timeoutResponseDataChunksInterval: Double = 20
/// It true, any observed request timeout switches off remote cache for all targets
var turnOffRemoteCacheOnFirstTimeout: Bool = false
/// List of all extensions that should carry over source fingerprints. Extensions of all product files that
/// contain non-deterministic content (absolute paths, timestamp, etc) should be included
/// .h files may contain absolute paths if NS_ENUM is used in a public API from Swift code
var productFilesExtensionsWithContentOverride = ["swiftmodule", "h"]
/// If true, plugins for thinning support should be enabled
var thinningEnabled: Bool = false
/// Module name of a target that works as a helper for thinned targets
var thinningTargetModuleName: String = "ThinningRemoteCacheModule"
/// Opt-in pretty json formatting for meta files
var prettifyMetaFiles: Bool = false
/// Secret key for AWS V4 Signature, if this is set the Authentication Header will be added
var AWSSecretKey: String = ""
/// Access key for AWS V4 Signature
var AWSAccessKey: String = ""
/// Temporary security token provided by the AWS Security Token Service
var AWSSecurityToken: String?
/// Region for AWS V4 Signature (e.g. `eu`)
var AWSRegion: String = ""
/// Service for AWS V4 Signature (e.g. `storage`)
var AWSService: String = ""
/// A dictionary of files path remapping that should be applied to make it absolute path agnostic on a list of
/// dependencies. Useful if a project refers files out of repo root, either compilation files or precompiled
/// dependencies. Keys represent generic replacement and values are substrings that should be replaced
/// Example: for mapping `["COOL_LIBRARY": "/CoolLibrary"]`
/// `/CoolLibrary/main.swift`will be represented as `$(COOL_LIBRARY)/main.swift`)
/// Warning: remapping order is not-deterministic so avoid remappings with multiple matchings
var outOfBandMappings: [String: String] = [:]
/// If true, SSL certificate validation is disabled
var disableCertificateVerification: Bool = false
/// A feature flag to disable virtual file system overlay support (temporary)
var disableVFSOverlay: Bool = false
/// A list of extra ENVs that should be used as placeholders in the dependency list
/// ENV rewrite process is optimistic - does nothing if an ENV is not defined in the pre/postbuild process
var customRewriteEnvs: [String] = []
/// Regexes of files that should not be included in a list of dependencies. Warning! Add entries here
/// with caution - excluding dependencies that are relevant might lead to a target overcaching
/// Note: The regex can match either partially or fully the filepath, e.g. `\\.modulemap$` will exclude
/// all `.modulemap` files
var irrelevantDependenciesPaths: [String] = []
/// If true, do not fail `prepare` if cannot find the most recent common commits with the primary branch
/// That might useful on CI, where a shallow clone is used
var gracefullyHandleMissingCommonSha: Bool = false
/// Enable experimental integration with swift driver, added in Xcode 14
var enableSwiftDriverIntegration: Bool = false
}
extension XCRemoteCacheConfig {
/// Merges existing config with the other config and returns a final result
/// `other` scheme overrides existing configuration
// swiftlint:disable:next function_body_length
func merged(with scheme: ConfigFileScheme) -> XCRemoteCacheConfig {
var merge = self
merge.mode = scheme.mode ?? mode
merge.recommendedCacheAddress = scheme.recommendedCacheAddress ?? recommendedCacheAddress
merge.cacheAddresses = scheme.cacheAddresses ?? cacheAddresses
merge.cacheHealthPath = scheme.cacheHealthPath ?? cacheHealthPath
merge.cacheHealthPathProbeCount = scheme.cacheHealthPathProbeCount ?? cacheHealthPathProbeCount
merge.remoteCommitFile = scheme.remoteCommitFile ?? remoteCommitFile
merge.xcccFile = scheme.xcccFile ?? xcccFile
merge.prebuildDiscoveryPath = scheme.prebuildDiscoveryPath ?? prebuildDiscoveryPath
merge.postbuildDiscoveryPath = scheme.postbuildDiscoveryPath ?? postbuildDiscoveryPath
merge.modeMarkerPath = scheme.modeMarkerPath ?? modeMarkerPath
merge.clangCommand = scheme.clangCommand ?? clangCommand
merge.swiftcCommand = scheme.swiftcCommand ?? swiftcCommand
merge.primaryRepo = scheme.primaryRepo ?? primaryRepo
merge.primaryBranch = scheme.primaryBranch ?? primaryBranch
merge.repoRoot = scheme.repoRoot ?? repoRoot
merge.cacheCommitHistory = scheme.cacheCommitHistory ?? cacheCommitHistory
merge.fingerprintOverrideExtension = scheme.fingerprintOverrideExtension ?? fingerprintOverrideExtension
merge.extraConfigurationFile = scheme.extraConfigurationFile ?? extraConfigurationFile
merge.publishingSha = scheme.publishingSha ?? publishingSha
merge.artifactMaximumAge = scheme.artifactMaximumAge ?? artifactMaximumAge
merge.customFingerprintEnvs = scheme.customFingerprintEnvs ?? customFingerprintEnvs
merge.statsDir = scheme.statsDir ?? statsDir
merge.downloadRetries = scheme.downloadRetries ?? downloadRetries
merge.uploadRetries = scheme.uploadRetries ?? uploadRetries
merge.retryDelay = scheme.retryDelay ?? retryDelay
merge.uploadBatchSize = scheme.uploadBatchSize ?? uploadBatchSize
merge.requestCustomHeaders = scheme.requestCustomHeaders ?? requestCustomHeaders
merge.thinTargetMockFilename = scheme.thinTargetMockFilename ?? thinTargetMockFilename
merge.focusedTargets = scheme.focusedTargets ?? focusedTargets
merge.disableHttpCache = scheme.disableHttpCache ?? disableHttpCache
merge.compilationHistoryFile = scheme.compilationHistoryFile ?? compilationHistoryFile
merge.timeoutResponseDataChunksInterval =
scheme.timeoutResponseDataChunksInterval ?? timeoutResponseDataChunksInterval
merge.turnOffRemoteCacheOnFirstTimeout =
scheme.turnOffRemoteCacheOnFirstTimeout ?? turnOffRemoteCacheOnFirstTimeout
merge.productFilesExtensionsWithContentOverride =
scheme.productFilesExtensionsWithContentOverride ?? productFilesExtensionsWithContentOverride
merge.thinningEnabled = scheme.thinningEnabled ?? thinningEnabled
merge.thinningTargetModuleName = scheme.thinningTargetModuleName ?? thinningTargetModuleName
merge.prettifyMetaFiles = scheme.prettifyMetaFiles ?? prettifyMetaFiles
merge.AWSAccessKey = scheme.AWSAccessKey ?? AWSAccessKey
merge.AWSSecretKey = scheme.AWSSecretKey ?? AWSSecretKey
merge.AWSSecurityToken = scheme.AWSSecurityToken ?? AWSSecurityToken
merge.AWSRegion = scheme.AWSRegion ?? AWSRegion
merge.AWSService = scheme.AWSService ?? AWSService
merge.outOfBandMappings = scheme.outOfBandMappings ?? outOfBandMappings
merge.disableCertificateVerification = scheme.disableCertificateVerification ?? disableCertificateVerification
merge.disableVFSOverlay = scheme.disableVFSOverlay ?? disableVFSOverlay
merge.customRewriteEnvs = scheme.customRewriteEnvs ?? customRewriteEnvs
merge.irrelevantDependenciesPaths = scheme.irrelevantDependenciesPaths ?? irrelevantDependenciesPaths
merge.gracefullyHandleMissingCommonSha =
scheme.gracefullyHandleMissingCommonSha ?? gracefullyHandleMissingCommonSha
merge.enableSwiftDriverIntegration = scheme.enableSwiftDriverIntegration ?? enableSwiftDriverIntegration
return merge
}
/// Verifies all required properties and set defualts
/// - Throws: `XCRemoteCacheConfigError` if the configuration is invalid
/// - Returns: valid `XCRemoteCacheConfig` with configured defaults
func verifyAndApplyDefaults() throws -> XCRemoteCacheConfig {
var newConfig = self
guard let fallbackCacheAddress = cacheAddresses.first else {
throw XCRemoteCacheConfigError.missingConfiguration(name: "cache_addresses")
}
if recommendedCacheAddress.isEmpty {
newConfig.recommendedCacheAddress = fallbackCacheAddress
}
return newConfig
}
}
/// A scheme of the user-specific overrides of configs
struct ConfigFileScheme: Decodable {
let mode: Mode?
let recommendedCacheAddress: String?
let cacheAddresses: [String]?
let cacheHealthPath: String?
let cacheHealthPathProbeCount: Int?
let remoteCommitFile: String?
let xcccFile: String?
let prebuildDiscoveryPath: String?
let postbuildDiscoveryPath: String?
let modeMarkerPath: String?
let clangCommand: String?
let swiftcCommand: String?
let primaryRepo: String?
let primaryBranch: String?
let repoRoot: String?
let cacheCommitHistory: Int?
let fingerprintOverrideExtension: String?
let extraConfigurationFile: String?
let publishingSha: String?
let artifactMaximumAge: Int?
let customFingerprintEnvs: [String]?
let statsDir: String?
let downloadRetries: Int?
let uploadRetries: Int?
let retryDelay: Double?
let uploadBatchSize: Int?
let requestCustomHeaders: [String: String]?
let thinTargetMockFilename: String?
let focusedTargets: [String]?
let disableHttpCache: Bool?
let compilationHistoryFile: String?
let timeoutResponseDataChunksInterval: Double?
let turnOffRemoteCacheOnFirstTimeout: Bool?
let productFilesExtensionsWithContentOverride: [String]?
let thinningEnabled: Bool?
let thinningTargetModuleName: String?
let prettifyMetaFiles: Bool?
let AWSSecretKey: String?
let AWSAccessKey: String?
let AWSSecurityToken: String?
let AWSRegion: String?
let AWSService: String?
let outOfBandMappings: [String: String]?
let disableCertificateVerification: Bool?
let disableVFSOverlay: Bool?
let customRewriteEnvs: [String]?
let irrelevantDependenciesPaths: [String]?
let gracefullyHandleMissingCommonSha: Bool?
let enableSwiftDriverIntegration: Bool?
// Yams library doesn't support encoding strategy, see https://github.com/jpsim/Yams/issues/84
enum CodingKeys: String, CodingKey {
case mode
case recommendedCacheAddress = "recommended_cache_address"
case cacheAddresses = "cache_addresses"
case cacheHealthPath = "cache_health_path"
case cacheHealthPathProbeCount = "cache_health_path_probe_count"
case remoteCommitFile = "remote_commit_file"
case xcccFile = "xccc_file"
case prebuildDiscoveryPath = "prebuild_discovery_path"
case postbuildDiscoveryPath = "postbuild_discovery_path"
case modeMarkerPath = "mode_marker_path"
case clangCommand = "clang_command"
case swiftcCommand = "swiftc_command"
case primaryRepo = "primary_repo"
case primaryBranch = "primary_branch"
case repoRoot = "repo_root"
case cacheCommitHistory = "cache_commit_history"
case fingerprintOverrideExtension = "fingerprint_override_extension"
case extraConfigurationFile = "extra_configuration_file"
case publishingSha = "publishing_sha"
case artifactMaximumAge = "artifact_maximum_age"
case customFingerprintEnvs = "custom_fingerprint_envs"
case statsDir = "stats_dir"
case downloadRetries = "download_retries"
case uploadRetries = "upload_retries"
case retryDelay = "retry_delay"
case uploadBatchSize = "upload_batch_size"
case requestCustomHeaders = "request_custom_headers"
case thinTargetMockFilename = "thin_target_mock_filename"
case focusedTargets = "focused_targets"
case disableHttpCache = "disable_http_cache"
case compilationHistoryFile = "compilation_history_file"
case timeoutResponseDataChunksInterval = "timeout_response_data_chunks_interval"
case turnOffRemoteCacheOnFirstTimeout = "turn_off_remote_cache_on_first_timeout"
case productFilesExtensionsWithContentOverride = "product_files_extensions_with_content_override"
case thinningEnabled = "thinning_enabled"
case thinningTargetModuleName = "thinning_target_module_name"
case prettifyMetaFiles = "prettify_meta_files"
case AWSSecretKey = "aws_secret_key"
case AWSAccessKey = "aws_access_key"
case AWSSecurityToken = "aws_security_token"
case AWSRegion = "aws_region"
case AWSService = "aws_service"
case outOfBandMappings = "out_of_band_mappings"
case disableCertificateVerification = "disable_certificate_verification"
case disableVFSOverlay = "disable_vfs_overlay"
case customRewriteEnvs = "custom_rewrite_envs"
case irrelevantDependenciesPaths = "irrelevant_dependencies_paths"
case gracefullyHandleMissingCommonSha = "gracefully_handle_missing_common_sha"
case enableSwiftDriverIntegration = "enable_swift_driver_integration"
}
}
enum XCRemoteCacheConfigReaderError: Error {
case missingConfigurationFile(URL)
case invalidConfiguration
}
class XCRemoteCacheConfigReader {
/// Name of the configuration file, required in $(SRCROOT) location
private static let configurationFile = ".rcinfo"
private let srcRoot: String
private let fileReader: FileReader
private lazy var yamlDecorer = YAMLDecoder(encoding: .utf8)
init(env: [String: String], fileReader: FileReader) throws {
let explicitSrcRoot: String? = env.readEnv(key: "SRCROOT")
srcRoot = explicitSrcRoot ?? FileManager.default.currentDirectoryPath
self.fileReader = fileReader
}
init(srcRootPath srcRoot: String, fileReader: FileReader) {
self.srcRoot = srcRoot
self.fileReader = fileReader
}
// Reads the final configuration by loading all extra configs
// until reaching a config that doesn't override `extraConfigurationFile`
func readConfiguration() throws -> XCRemoteCacheConfig {
let rootURL = URL(fileURLWithPath: srcRoot)
let configURL = URL(fileURLWithPath: Self.configurationFile, relativeTo: rootURL)
let userConfigs = try readUserConfig(configURL)
var config = XCRemoteCacheConfig(sourceRoot: srcRoot).merged(with: userConfigs)
var extraConfURL = URL(fileURLWithPath: config.extraConfigurationFile, relativeTo: rootURL)
var visitedFiles = Set([configURL])
while !visitedFiles.contains(extraConfURL) {
do {
let extraConfig = try readUserConfig(extraConfURL)
debugLog("Reading extra configuration from \(extraConfURL)")
config = config.merged(with: extraConfig)
visitedFiles.insert(extraConfURL)
// Advance extra configuration
extraConfURL = URL(fileURLWithPath: config.extraConfigurationFile, relativeTo: rootURL)
} catch {
infoLog("Extra config override failed with \(error). Skipping extra configuration")
// swiftlint:disable:next unneeded_break_in_switch
break
}
}
return try config.verifyAndApplyDefaults()
}
/// Reads user configuration from a file
private func readUserConfig(_ file: URL) throws -> ConfigFileScheme {
let configurationContent = try fileReader.contents(atPath: file.path)
guard let configurationData = configurationContent else {
throw XCRemoteCacheConfigReaderError.missingConfigurationFile(file)
}
guard let configurationString = String(data: configurationData, encoding: .utf8) else {
throw XCRemoteCacheConfigReaderError.invalidConfiguration
}
return try yamlDecorer.decode(from: configurationString)
}
}