Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| afb1f9e531 | |||
| 53e7ddd34c | |||
| 344b1f13ca | |||
| bc13f58e73 | |||
| 47ad8a890b | |||
| 801920dc87 | |||
| 37e144c36a | |||
| f389887e37 | |||
| b74e002415 | |||
| 9a34a99f0b | |||
| f4eed9e8aa | |||
| 811cc00f0c | |||
| 56d9c208a1 | |||
| 8dffbd4162 | |||
| 3853ce2bc2 | |||
| 148c99d2f5 | |||
| 65bf9156ec | |||
| 22484b4daf | |||
| 867bbb6265 | |||
| 532484c3ab | |||
| 338cbd141a | |||
| f15dd8f98d | |||
| ee31a3815f | |||
| 5749762b86 | |||
| f8757b6ee7 | |||
| c26aaf7d42 | |||
| d837f6e14b | |||
| 5528d507b0 | |||
| 352e72f44c | |||
| 1c67b79a7a | |||
| c7de203741 | |||
| b7e18916e6 |
@@ -32,6 +32,10 @@ let package = Package(
|
||||
name: "xcswiftc",
|
||||
dependencies: ["XCRemoteCache"]
|
||||
),
|
||||
.target(
|
||||
name: "xcswift-frontend",
|
||||
dependencies: ["XCRemoteCache"]
|
||||
),
|
||||
.target(
|
||||
name: "xclibtoolSupport",
|
||||
dependencies: ["XCRemoteCache"]
|
||||
@@ -69,6 +73,7 @@ let package = Package(
|
||||
dependencies: [
|
||||
"xcprebuild",
|
||||
"xcswiftc",
|
||||
"xcswift-frontend",
|
||||
"xclibtool",
|
||||
"xcpostbuild",
|
||||
"xcprepare",
|
||||
|
||||
@@ -45,6 +45,7 @@ _XCRemoteCache is a remote cache tool for Xcode projects. It reuses target artif
|
||||
- [Limitations](#limitations)
|
||||
- [FAQ](#faq)
|
||||
- [Development](#development)
|
||||
- [Architectural Designs](#architectural-designs)
|
||||
- [Release](#release)
|
||||
* [Releasing CocoaPods plugin](#releasing-cocoapods-plugin)
|
||||
* [Building release package](#building-release-package)
|
||||
@@ -291,7 +292,7 @@ where
|
||||
|
||||
```shell
|
||||
ditto "${SCRIPT_INPUT_FILE_0}" "${SCRIPT_OUTPUT_FILE_0}"
|
||||
[ -f "${SCRIPT_INPUT_FILE_1}" ] && ditto "${SCRIPT_INPUT_FILE_1}" "${SCRIPT_OUTPUT_FILE_1}" || rm "${SCRIPT_OUTPUT_FILE_1}"
|
||||
[ -f "${SCRIPT_INPUT_FILE_1}" ] && ditto "${SCRIPT_INPUT_FILE_1}" "${SCRIPT_OUTPUT_FILE_1}" || rm -f "${SCRIPT_OUTPUT_FILE_1}"
|
||||
```
|
||||
|
||||
where
|
||||
@@ -358,6 +359,8 @@ Note: This step is not required if at least one of these is true:
|
||||
| `disable_vfs_overlay` | A feature flag to disable virtual file system overlay support (temporary) | `false` | ⬜️ |
|
||||
| `custom_rewrite_envs` | 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. | `[]` | ⬜️ |
|
||||
| `irrelevant_dependencies_paths` | 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. The regex can match either partially or fully the filepath, e.g. `\\.modulemap$` will exclude all `.modulemap` files. | `[]` | ⬜️ |
|
||||
| `gracefully_handle_missing_common_sha` | If true, do not fail `prepare` if cannot find the most recent common commits with the primary branch. That might be useful on CI, where a shallow clone is used and cloning depth is not big enough to fetch a commit from a primary branch | `false` | ⬜️ |
|
||||
| `enable_swift_driver_integration` | Enable experimental integration with swift driver, added in Xcode 14 | `false` | ⬜️ |
|
||||
|
||||
## Backend cache server
|
||||
|
||||
@@ -467,6 +470,10 @@ Follow the [FAQ](docs/FAQ.md) page.
|
||||
|
||||
Follow the [Development](docs/Development.md) guide. It has all the information on how to get started.
|
||||
|
||||
## Architectural designs
|
||||
|
||||
Follow the [Architectural designs](docs/design/ArchitecturalDesigns.md) document that describes and documents XCRemoteCache designs and implementation details.
|
||||
|
||||
## Release
|
||||
|
||||
To release a version, in [Releases](https://github.com/spotify/XCRemoteCache/releases) draft a new release with `v0.3.0{-rc0}` tag format.
|
||||
|
||||
@@ -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', 'xcld', 'xcldplusplus', 'xclipo']
|
||||
EXECUTABLE_NAMES = ['xclibtool', 'xcpostbuild', 'xcprebuild', 'xcprepare', 'xcswiftc', 'swiftc', 'xcswift-frontend', 'swift-frontend', 'xcld', 'xcldplusplus', 'xclipo']
|
||||
PROJECT_NAME = 'XCRemoteCache'
|
||||
|
||||
SWIFTLINT_ENABLED = true
|
||||
@@ -59,6 +59,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
|
||||
system("cd #{build_path_base} && ln -s xcswiftc swiftc")
|
||||
system("cd #{build_path_base} && ln -s xcswift-frontend swift-frontend")
|
||||
sdk_build_paths = EXECUTABLE_NAMES.map {|e| File.join(build_path_base, e)}
|
||||
|
||||
build_paths.push(sdk_build_paths)
|
||||
@@ -130,7 +134,9 @@ def create_release_zip(build_paths)
|
||||
# Create and move files into the release directory
|
||||
mkdir_p release_dir
|
||||
build_paths.each {|p|
|
||||
cp_r p, release_dir
|
||||
# -r for recursive
|
||||
# -P for copying symbolic link as is
|
||||
system("cp -rP #{p} #{release_dir}")
|
||||
}
|
||||
|
||||
output_artifact_basename = "#{PROJECT_NAME}.zip"
|
||||
@@ -139,7 +145,8 @@ def create_release_zip(build_paths)
|
||||
# -X: no extras (uid, gid, file times, ...)
|
||||
# -x: exclude .DS_Store
|
||||
# -r: recursive
|
||||
system("zip -X -x '*.DS_Store' -r #{output_artifact_basename} .") or abort "zip failure"
|
||||
# -y: to store symbolic links (used for swiftc -> xcswiftc)
|
||||
system("zip -X -x '*.DS_Store' -r -y #{output_artifact_basename} .") or abort "zip failure"
|
||||
# List contents of zip file
|
||||
system("unzip -l #{output_artifact_basename}") or abort "unzip failure"
|
||||
end
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// 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
|
||||
|
||||
class FallbackXCLibtoolLogic: XCLibtoolLogic {
|
||||
private let fallbackCommand: String
|
||||
|
||||
init(fallbackCommand: String) {
|
||||
self.fallbackCommand = fallbackCommand
|
||||
}
|
||||
|
||||
func run() {
|
||||
let args = ProcessInfo().arguments
|
||||
let paramList = [fallbackCommand] + args.dropFirst()
|
||||
let cargs = paramList.map { strdup($0) } + [nil]
|
||||
execvp(fallbackCommand, cargs)
|
||||
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,8 @@ public enum XCLibtoolMode: Equatable {
|
||||
case createLibrary(output: String, filelist: String, dependencyInfo: String)
|
||||
/// Creating a universal library (multiple-architectures) from a set of input .a static libraries
|
||||
case createUniversalBinary(output: String, inputs: [String])
|
||||
/// print the toolchain version
|
||||
case version
|
||||
}
|
||||
|
||||
public class XCLibtool {
|
||||
@@ -50,6 +52,8 @@ public class XCLibtool {
|
||||
toolName: "Libtool",
|
||||
fallbackCommand: "libtool"
|
||||
)
|
||||
case .version:
|
||||
logic = FallbackXCLibtoolLogic(fallbackCommand: "libtool")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -89,6 +89,9 @@ public struct PostbuildContext {
|
||||
var publicHeadersFolderPath: URL?
|
||||
/// XCRemoteCache is explicitly disabled
|
||||
let disabled: Bool
|
||||
/// The LLBUILD_BUILD_ID ENV that describes the compilation identifier
|
||||
/// it is used in the swift-frontend flow
|
||||
let llbuildIdLockFile: URL
|
||||
}
|
||||
|
||||
extension PostbuildContext {
|
||||
@@ -149,5 +152,10 @@ extension PostbuildContext {
|
||||
publicHeadersFolderPath = builtProductsDir.appendingPathComponent(publicHeadersPath)
|
||||
}
|
||||
disabled = try env.readEnv(key: "XCRC_DISABLED") ?? false
|
||||
let llbuildId: String = try env.readEnv(key: "LLBUILD_BUILD_ID")
|
||||
llbuildIdLockFile = SwiftFrontendContext.buildLlbuildIdSharedLockUrl(
|
||||
llbuildId: llbuildId,
|
||||
tmpDir: targetTempDir
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ public class XCPostbuild {
|
||||
dependenciesWriter: FileDependenciesWriter.init,
|
||||
dependenciesReader: FileDependenciesReader.init,
|
||||
markerWriter: NoopMarkerWriter.init,
|
||||
llbuildLockFile: context.llbuildIdLockFile,
|
||||
fileManager: fileManager
|
||||
)
|
||||
|
||||
@@ -152,9 +153,14 @@ public class XCPostbuild {
|
||||
let fileReaderFactory: (URL) -> DependenciesReader = {
|
||||
FileDependenciesReader($0, accessor: fileManager)
|
||||
}
|
||||
let assetsFileDependenciesFactory: (URL) -> DependenciesReader = {
|
||||
AssetsFileDependenciesReader($0, dirAccessor: fileManager)
|
||||
}
|
||||
let dependenciesReader = TargetDependenciesReader(
|
||||
context.compilationTempDir,
|
||||
fileDependeciesReaderFactory: fileReaderFactory,
|
||||
compilationOutputDir: context.compilationTempDir,
|
||||
assetsCatalogOutputDir: context.targetTempDir,
|
||||
fileDependenciesReaderFactory: fileReaderFactory,
|
||||
assetsDependenciesReaderFactory: assetsFileDependenciesFactory,
|
||||
dirScanner: fileManager
|
||||
)
|
||||
var remappers: [DependenciesRemapper] = []
|
||||
|
||||
@@ -48,6 +48,9 @@ public struct PrebuildContext {
|
||||
let overlayHeadersPath: URL
|
||||
/// XCRemoteCache is explicitly disabled
|
||||
let disabled: Bool
|
||||
/// The LLBUILD_BUILD_ID ENV that describes the compilation identifier
|
||||
/// it is used in the swift-frontend flow
|
||||
let llbuildIdLockFile: URL
|
||||
}
|
||||
|
||||
extension PrebuildContext {
|
||||
@@ -72,5 +75,10 @@ extension PrebuildContext {
|
||||
/// Note: The file has yaml extension, even it is in the json format
|
||||
overlayHeadersPath = targetTempDir.appendingPathComponent("all-product-headers.yaml")
|
||||
disabled = try env.readEnv(key: "XCRC_DISABLED") ?? false
|
||||
let llbuildId: String = try env.readEnv(key: "LLBUILD_BUILD_ID")
|
||||
llbuildIdLockFile = SwiftFrontendContext.buildLlbuildIdSharedLockUrl(
|
||||
llbuildId: llbuildId,
|
||||
tmpDir: targetTempDir
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ public class XCPrebuild {
|
||||
dependenciesWriter: FileDependenciesWriter.init,
|
||||
dependenciesReader: FileDependenciesReader.init,
|
||||
markerWriter: lazyMarkerWriterFactory,
|
||||
llbuildLockFile: context.llbuildIdLockFile,
|
||||
fileManager: fileManager
|
||||
)
|
||||
|
||||
|
||||
@@ -21,6 +21,11 @@ import Foundation
|
||||
|
||||
typealias BuildSettings = [String: Any]
|
||||
|
||||
struct BuildSettingsIntegrateAppenderOption: OptionSet {
|
||||
let rawValue: Int
|
||||
|
||||
static let disableSwiftDriverIntegration = BuildSettingsIntegrateAppenderOption(rawValue: 1 << 0)
|
||||
}
|
||||
// Manages Xcode build settings
|
||||
protocol BuildSettingsIntegrateAppender {
|
||||
/// Appends XCRemoteCache-specific build settings
|
||||
@@ -35,18 +40,28 @@ class XcodeProjBuildSettingsIntegrateAppender: BuildSettingsIntegrateAppender {
|
||||
private let repoRoot: URL
|
||||
private let fakeSrcRoot: URL
|
||||
private let sdksExclude: [String]
|
||||
private let options: BuildSettingsIntegrateAppenderOption
|
||||
|
||||
init(mode: Mode, repoRoot: URL, fakeSrcRoot: URL, sdksExclude: [String]) {
|
||||
init(
|
||||
mode: Mode,
|
||||
repoRoot: URL,
|
||||
fakeSrcRoot: URL,
|
||||
sdksExclude: [String],
|
||||
options: BuildSettingsIntegrateAppenderOption
|
||||
) {
|
||||
self.mode = mode
|
||||
self.repoRoot = repoRoot
|
||||
self.fakeSrcRoot = fakeSrcRoot
|
||||
self.sdksExclude = sdksExclude
|
||||
self.options = options
|
||||
}
|
||||
|
||||
func appendToBuildSettings(buildSettings: BuildSettings, wrappers: XCRCBinariesPaths) -> BuildSettings {
|
||||
var result = buildSettings
|
||||
setBuildSetting(buildSettings: &result, key: "SWIFT_EXEC", value: wrappers.swiftc.path )
|
||||
setBuildSetting(buildSettings: &result, key: "SWIFT_USE_INTEGRATED_DRIVER", value: "NO" )
|
||||
if options.contains(.disableSwiftDriverIntegration) {
|
||||
setBuildSetting(buildSettings: &result, key: "SWIFT_USE_INTEGRATED_DRIVER", value: "NO" )
|
||||
}
|
||||
// When generating artifacts, no need to shell-out all compilation commands to our wrappers
|
||||
if case .consumer = mode {
|
||||
setBuildSetting(buildSettings: &result, key: "CC", value: wrappers.cc.path )
|
||||
|
||||
@@ -27,14 +27,14 @@ struct IntegrateContext {
|
||||
let configOverride: URL
|
||||
let fakeSrcRoot: URL
|
||||
let output: URL?
|
||||
let buildSettingsAppenderOptions: BuildSettingsIntegrateAppenderOption
|
||||
}
|
||||
|
||||
extension IntegrateContext {
|
||||
init(
|
||||
input: String,
|
||||
repoRootPath: String,
|
||||
config: XCRemoteCacheConfig,
|
||||
mode: Mode,
|
||||
configOverridePath: String,
|
||||
env: [String: String],
|
||||
binariesDir: URL,
|
||||
fakeSrcRoot: String,
|
||||
@@ -42,15 +42,22 @@ extension IntegrateContext {
|
||||
) throws {
|
||||
projectPath = URL(fileURLWithPath: input)
|
||||
let srcRoot = projectPath.deletingLastPathComponent()
|
||||
repoRoot = URL(fileURLWithPath: repoRootPath, relativeTo: srcRoot)
|
||||
repoRoot = URL(fileURLWithPath: config.repoRoot, relativeTo: srcRoot)
|
||||
self.mode = mode
|
||||
configOverride = URL(fileURLWithPath: configOverridePath, relativeTo: srcRoot)
|
||||
configOverride = URL(fileURLWithPath: config.extraConfigurationFile, relativeTo: srcRoot)
|
||||
output = outputPath.flatMap(URL.init(fileURLWithPath:))
|
||||
self.fakeSrcRoot = URL(fileURLWithPath: fakeSrcRoot)
|
||||
var swiftcBinaryName = "swiftc"
|
||||
var buildSettingsAppenderOptions: BuildSettingsIntegrateAppenderOption = []
|
||||
// Keep the legacy behaviour (supported in Xcode 14 and lower)
|
||||
if !config.enableSwiftDriverIntegration {
|
||||
buildSettingsAppenderOptions.insert(.disableSwiftDriverIntegration)
|
||||
swiftcBinaryName = "xcswiftc"
|
||||
}
|
||||
binaries = XCRCBinariesPaths(
|
||||
prepare: binariesDir.appendingPathComponent("xcprepare"),
|
||||
cc: binariesDir.appendingPathComponent("xccc"),
|
||||
swiftc: binariesDir.appendingPathComponent("xcswiftc"),
|
||||
swiftc: binariesDir.appendingPathComponent(swiftcBinaryName),
|
||||
libtool: binariesDir.appendingPathComponent("xclibtool"),
|
||||
lipo: binariesDir.appendingPathComponent("xclipo"),
|
||||
ld: binariesDir.appendingPathComponent("xcld"),
|
||||
@@ -58,5 +65,6 @@ extension IntegrateContext {
|
||||
prebuild: binariesDir.appendingPathComponent("xcprebuild"),
|
||||
postbuild: binariesDir.appendingPathComponent("xcpostbuild")
|
||||
)
|
||||
self.buildSettingsAppenderOptions = buildSettingsAppenderOptions
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,9 +82,8 @@ public class XCIntegrate {
|
||||
|
||||
let context = try IntegrateContext(
|
||||
input: projectPath,
|
||||
repoRootPath: config.repoRoot,
|
||||
config: config,
|
||||
mode: mode,
|
||||
configOverridePath: config.extraConfigurationFile,
|
||||
env: env,
|
||||
binariesDir: binariesDir,
|
||||
fakeSrcRoot: fakeSrcRoot,
|
||||
@@ -102,7 +101,8 @@ public class XCIntegrate {
|
||||
mode: context.mode,
|
||||
repoRoot: context.repoRoot,
|
||||
fakeSrcRoot: context.fakeSrcRoot,
|
||||
sdksExclude: sdksExclude.integrateArrayArguments
|
||||
sdksExclude: sdksExclude.integrateArrayArguments,
|
||||
options: context.buildSettingsAppenderOptions
|
||||
)
|
||||
let lldbPatcher: LLDBInitPatcher
|
||||
switch lldbMode {
|
||||
|
||||
@@ -77,7 +77,18 @@ class Prepare: PrepareLogic {
|
||||
guard fileAccessor.fileExists(atPath: PhaseCacheModeController.xcodeSelectLink.path) else {
|
||||
throw PrepareError.missingXcodeSelectDirectory
|
||||
}
|
||||
let commonSha = try gitClient.getCommonPrimarySha()
|
||||
|
||||
let commonSha: String
|
||||
do {
|
||||
commonSha = try gitClient.getCommonPrimarySha()
|
||||
} catch let GitClientError.noCommonShaWithPrimaryRepo(remoteName, error) {
|
||||
guard context.gracefullyHandleMissingCommonSha else {
|
||||
throw GitClientError.noCommonShaWithPrimaryRepo(remoteName: remoteName, error: error)
|
||||
}
|
||||
infoLog("Cannot find a common sha with the primary branch: \(error). Gracefully disabling remote cache")
|
||||
try disable()
|
||||
return .failed
|
||||
}
|
||||
|
||||
if context.offline {
|
||||
// Optimistically take first common sha
|
||||
|
||||
@@ -52,6 +52,8 @@ public struct PrepareContext {
|
||||
let cacheHealthPathProbeCount: Int
|
||||
/// clang wrapper output file
|
||||
let xcccCommand: URL
|
||||
/// gracefully disable remote cache for missing common sha with the primary branch
|
||||
let gracefullyHandleMissingCommonSha: Bool
|
||||
}
|
||||
|
||||
extension PrepareContext {
|
||||
@@ -77,5 +79,6 @@ extension PrepareContext {
|
||||
cacheAddresses = try config.cacheAddresses.map(URL.build)
|
||||
cacheHealthPath = config.cacheHealthPath
|
||||
cacheHealthPathProbeCount = config.cacheHealthPathProbeCount
|
||||
gracefullyHandleMissingCommonSha = config.gracefullyHandleMissingCommonSha
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
// 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 SwiftFrontendArgInputError: Error, Equatable {
|
||||
// swift-frontend should either be compling or emiting a module
|
||||
case bothCompilationAndEmitAction
|
||||
// no .swift files have been passed as input files
|
||||
case noCompilationInputs
|
||||
// no -primary-file .swift files have been passed as input files
|
||||
case noPrimaryFileCompilationInputs
|
||||
// number of -emit-dependencies-path doesn't match compilation inputs
|
||||
case dependenciesOuputCountDoesntMatch(expected: Int, parsed: Int)
|
||||
// number of -serialize-diagnostics-path doesn't match compilation inputs
|
||||
case diagnosticsOuputCountDoesntMatch(expected: Int, parsed: Int)
|
||||
// number of -o doesn't match compilation inputs
|
||||
case outputsOuputCountDoesntMatch(expected: Int, parsed: Int)
|
||||
// number of -o for emit-module can be only 1
|
||||
case emitModulOuputCountIsNot1(parsed: Int)
|
||||
// number of -emit-dependencies-path for emit-module can be 0 or 1 (generate or not)
|
||||
case emitModuleDependenciesOuputCountIsHigherThan1(parsed: Int)
|
||||
// number of -serialize-diagnostics-path for emit-module can be 0 or 1 (generate or not)
|
||||
case emitModuleDiagnosticsOuputCountIsHigherThan1(parsed: Int)
|
||||
// emit-module requires -emit-objc-header-path
|
||||
case emitModuleMissingObjcHeaderPath
|
||||
// -target is required
|
||||
case emitMissingTarget
|
||||
// -moduleName is required
|
||||
case emitMissingModuleName
|
||||
}
|
||||
|
||||
public struct SwiftFrontendArgInput {
|
||||
let compile: Bool
|
||||
let emitModule: Bool
|
||||
let objcHeaderOutput: String?
|
||||
let moduleName: String?
|
||||
let target: String?
|
||||
let primaryInputPaths: [String]
|
||||
let inputPaths: [String]
|
||||
var outputPaths: [String]
|
||||
var dependenciesPaths: [String]
|
||||
// Extra params
|
||||
// Diagnostics are not supported yet in the XCRemoteCache (cached artifacts assumes no warnings)
|
||||
var diagnosticsPaths: [String]
|
||||
// Unsed for now:
|
||||
// .swiftsourceinfo and .swiftdoc will be placed next to the .swiftmodule
|
||||
let sourceInfoPath: String?
|
||||
let docPath: String?
|
||||
// Passed as -supplementary-output-file-map
|
||||
let supplementaryOutputFileMap: String?
|
||||
|
||||
/// Manual initializer implementation required to be public
|
||||
public init(
|
||||
compile: Bool,
|
||||
emitModule: Bool,
|
||||
objcHeaderOutput: String?,
|
||||
moduleName: String?,
|
||||
target: String?,
|
||||
primaryInputPaths: [String],
|
||||
inputPaths: [String],
|
||||
outputPaths: [String],
|
||||
dependenciesPaths: [String],
|
||||
diagnosticsPaths: [String],
|
||||
sourceInfoPath: String?,
|
||||
docPath: String?,
|
||||
supplementaryOutputFileMap: String?
|
||||
) {
|
||||
self.compile = compile
|
||||
self.emitModule = emitModule
|
||||
self.objcHeaderOutput = objcHeaderOutput
|
||||
self.moduleName = moduleName
|
||||
self.target = target
|
||||
self.primaryInputPaths = primaryInputPaths
|
||||
self.inputPaths = inputPaths
|
||||
self.outputPaths = outputPaths
|
||||
self.dependenciesPaths = dependenciesPaths
|
||||
self.diagnosticsPaths = diagnosticsPaths
|
||||
self.sourceInfoPath = sourceInfoPath
|
||||
self.docPath = docPath
|
||||
self.supplementaryOutputFileMap = supplementaryOutputFileMap
|
||||
}
|
||||
|
||||
private func generateForCompilation(
|
||||
config: XCRemoteCacheConfig,
|
||||
target: String,
|
||||
moduleName: String
|
||||
) throws -> SwiftcContext {
|
||||
let primaryInputsCount = primaryInputPaths.count
|
||||
|
||||
guard primaryInputsCount > 0 else {
|
||||
throw SwiftFrontendArgInputError.noPrimaryFileCompilationInputs
|
||||
}
|
||||
guard [primaryInputsCount, 0].contains(dependenciesPaths.count) else {
|
||||
throw SwiftFrontendArgInputError.dependenciesOuputCountDoesntMatch(
|
||||
expected: primaryInputsCount,
|
||||
parsed: dependenciesPaths.count
|
||||
)
|
||||
}
|
||||
guard [primaryInputsCount, 0].contains(diagnosticsPaths.count) else {
|
||||
throw SwiftFrontendArgInputError.diagnosticsOuputCountDoesntMatch(
|
||||
expected: primaryInputsCount,
|
||||
parsed: diagnosticsPaths.count
|
||||
)
|
||||
}
|
||||
guard outputPaths.count == primaryInputsCount else {
|
||||
throw SwiftFrontendArgInputError.outputsOuputCountDoesntMatch(
|
||||
expected: primaryInputsCount,
|
||||
parsed: outputPaths.count
|
||||
)
|
||||
}
|
||||
let primaryInputFilesURLs: [URL] = primaryInputPaths.map(URL.init(fileURLWithPath:))
|
||||
|
||||
let steps: SwiftcContext.SwiftcSteps = SwiftcContext.SwiftcSteps(
|
||||
compileFilesScope: .subset(primaryInputFilesURLs),
|
||||
emitModule: nil
|
||||
)
|
||||
|
||||
let compilationFilesInputs = buildCompilationFilesInputs(
|
||||
primaryInputsCount: primaryInputsCount,
|
||||
primaryInputFilesURLs: primaryInputFilesURLs
|
||||
)
|
||||
|
||||
return try .init(
|
||||
config: config,
|
||||
moduleName: moduleName,
|
||||
steps: steps,
|
||||
inputs: compilationFilesInputs,
|
||||
target: target,
|
||||
compilationFiles: .list(inputPaths),
|
||||
exampleWorkspaceFilePath: outputPaths[0]
|
||||
)
|
||||
}
|
||||
|
||||
private func buildCompilationFilesInputs(
|
||||
primaryInputsCount: Int,
|
||||
primaryInputFilesURLs: [URL]
|
||||
) -> SwiftcContext.CompilationFilesInputs {
|
||||
if let compimentaryFileMa = supplementaryOutputFileMap {
|
||||
return .supplementaryFileMap(compimentaryFileMa)
|
||||
} else {
|
||||
return .map((0..<primaryInputsCount).reduce(
|
||||
[String: SwiftFileCompilationInfo]()
|
||||
) { prev, i in
|
||||
var new = prev
|
||||
new[primaryInputPaths[i]] = SwiftFileCompilationInfo(
|
||||
file: primaryInputFilesURLs[i],
|
||||
dependencies: dependenciesPaths.get(i).map(URL.init(fileURLWithPath:)),
|
||||
object: outputPaths.get(i).map(URL.init(fileURLWithPath:)),
|
||||
// for now - swift-dependencies are not requested in the driver compilation mode
|
||||
swiftDependencies: nil
|
||||
)
|
||||
return new
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func generateForEmitModule(
|
||||
config: XCRemoteCacheConfig,
|
||||
target: String,
|
||||
moduleName: String
|
||||
) throws -> SwiftcContext {
|
||||
guard outputPaths.count == 1 else {
|
||||
throw SwiftFrontendArgInputError.emitModulOuputCountIsNot1(parsed: outputPaths.count)
|
||||
}
|
||||
guard let objcHeaderOutput = objcHeaderOutput else {
|
||||
throw SwiftFrontendArgInputError.emitModuleMissingObjcHeaderPath
|
||||
}
|
||||
guard diagnosticsPaths.count <= 1 else {
|
||||
throw SwiftFrontendArgInputError.emitModuleDiagnosticsOuputCountIsHigherThan1(
|
||||
parsed: diagnosticsPaths.count
|
||||
)
|
||||
}
|
||||
guard dependenciesPaths.count <= 1 else {
|
||||
throw SwiftFrontendArgInputError.emitModuleDependenciesOuputCountIsHigherThan1(
|
||||
parsed: dependenciesPaths.count
|
||||
)
|
||||
}
|
||||
|
||||
let steps: SwiftcContext.SwiftcSteps = SwiftcContext.SwiftcSteps(
|
||||
compileFilesScope: .none,
|
||||
emitModule: SwiftcContext.SwiftcStepEmitModule(
|
||||
objcHeaderOutput: URL(fileURLWithPath: objcHeaderOutput),
|
||||
modulePathOutput: URL(fileURLWithPath: outputPaths[0]),
|
||||
dependencies: dependenciesPaths.first.map(URL.init(fileURLWithPath:))
|
||||
)
|
||||
)
|
||||
return try .init(
|
||||
config: config,
|
||||
moduleName: moduleName,
|
||||
steps: steps,
|
||||
inputs: .map([:]),
|
||||
target: target,
|
||||
compilationFiles: .list(inputPaths),
|
||||
exampleWorkspaceFilePath: objcHeaderOutput
|
||||
)
|
||||
}
|
||||
|
||||
func generateSwiftcContext(config: XCRemoteCacheConfig) throws -> SwiftcContext {
|
||||
guard compile != emitModule else {
|
||||
throw SwiftFrontendArgInputError.bothCompilationAndEmitAction
|
||||
}
|
||||
let inputPathsCount = inputPaths.count
|
||||
guard inputPathsCount > 0 else {
|
||||
throw SwiftFrontendArgInputError.noCompilationInputs
|
||||
}
|
||||
guard let target = target else {
|
||||
throw SwiftFrontendArgInputError.emitMissingTarget
|
||||
}
|
||||
guard let moduleName = moduleName else {
|
||||
throw SwiftFrontendArgInputError.emitMissingModuleName
|
||||
}
|
||||
|
||||
if compile {
|
||||
return try generateForCompilation(
|
||||
config: config,
|
||||
target: target,
|
||||
moduleName: moduleName
|
||||
)
|
||||
} else {
|
||||
return try generateForEmitModule(
|
||||
config: config,
|
||||
target: target,
|
||||
moduleName: moduleName
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// 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
|
||||
|
||||
struct SwiftFrontendContext {
|
||||
/// File lock used for synchronizing multiple invocations
|
||||
let invocationLockFile: URL
|
||||
}
|
||||
|
||||
extension SwiftFrontendContext {
|
||||
init(_ swiftcContext: SwiftcContext, env: [String: String]) throws {
|
||||
/// The LLBUILD_BUILD_ID ENV that describes the entire (incl. parent's swiftc) bui;d
|
||||
let llbuildId: String = try env.readEnv(key: "LLBUILD_BUILD_ID")
|
||||
invocationLockFile = Self.self.buildLlbuildIdSharedLockUrl(
|
||||
llbuildId: llbuildId,
|
||||
tmpDir: swiftcContext.tempDir
|
||||
)
|
||||
}
|
||||
|
||||
/// Generate the filename to be used to synchronize multiple swift-frontend invocations
|
||||
/// The same file is used in prebuild, xcswift-frontend and postbuild (to clean it up)
|
||||
static func buildLlbuildIdSharedLockUrl(llbuildId: String, tmpDir: URL) -> URL {
|
||||
return tmpDir.appendingPathComponent(llbuildId).appendingPathExtension("lock")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
// 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
|
||||
|
||||
/// Manages the `swift-frontend` logic
|
||||
protocol SwiftFrontendOrchestrator {
|
||||
/// Executes the critical section according to the required order
|
||||
/// - Parameter criticalSection: the block that should be synchronized
|
||||
func run(criticalSection: () -> Void ) throws
|
||||
}
|
||||
|
||||
/// The default orchestrator that manages the order or swift-frontend invocations
|
||||
/// For emit-module (the "first" process) action, it locks a shared file between all swift-frontend invocations,
|
||||
/// verifies that the mocking can be done and continues the mocking/fall-backing along the lock release
|
||||
/// For the compilation action, tries to acquire a lock and waits until the "emit-module" makes a decision
|
||||
/// if the compilation should be skipped and a "mocking" should used instead
|
||||
class CommonSwiftFrontendOrchestrator {
|
||||
/// Content saved to the shared file
|
||||
/// Safe to use forced unwrapping
|
||||
private static let emitModuleContent = "done".data(using: .utf8)!
|
||||
|
||||
enum Action {
|
||||
case emitModule
|
||||
case compile
|
||||
}
|
||||
private let mode: SwiftcContext.SwiftcMode
|
||||
private let action: Action
|
||||
private let lockAccessor: ExclusiveFileAccessor
|
||||
private let maxLockTimeout: TimeInterval
|
||||
|
||||
init(
|
||||
mode: SwiftcContext.SwiftcMode,
|
||||
action: Action,
|
||||
lockAccessor: ExclusiveFileAccessor,
|
||||
maxLockTimeout: TimeInterval
|
||||
) {
|
||||
self.mode = mode
|
||||
self.action = action
|
||||
self.lockAccessor = lockAccessor
|
||||
self.maxLockTimeout = maxLockTimeout
|
||||
}
|
||||
|
||||
func run(criticalSection: () throws -> Void) throws {
|
||||
guard case .consumer(commit: .available) = mode else {
|
||||
// no need to lock anything - just allow fallbacking to the `swiftc or swift-frontend`
|
||||
// for a producer or a consumer where RC is disabled (we have already caught the
|
||||
// cache miss)
|
||||
try criticalSection()
|
||||
return
|
||||
}
|
||||
try executeMockAttemp(criticalSection: criticalSection)
|
||||
}
|
||||
|
||||
private func executeMockAttemp(criticalSection: () throws -> Void) throws {
|
||||
switch action {
|
||||
case .emitModule:
|
||||
try validateEmitModuleStep(criticalSection: criticalSection)
|
||||
case .compile:
|
||||
try waitForEmitModuleLock(criticalSection: criticalSection)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// For emit-module, wrap the critical section with the shared lock so other processes (compilation)
|
||||
/// have to wait until emit-module finishes
|
||||
/// Once the emit-module is done, the "magical" string is saved to the file and the lock is released
|
||||
///
|
||||
/// Note: The design of wrapping the entire "emit-module" has a small performance downside if inside
|
||||
/// the critical section, the code realizes that remote cache cannot be used
|
||||
/// (in practice - a new file has been added)
|
||||
/// None of compilation process (so with '-c' args) can continue until the entire emit-module logic finishes
|
||||
/// Because it is expected to happen not that often and emit-module is usually quite fast, this makes the
|
||||
/// implementation way simpler. If we ever want to optimize it, we should release the lock as early
|
||||
/// as we know, the remote cache cannot be used. Then all other compilation process (-c) can run
|
||||
/// in parallel with emit-module
|
||||
private func validateEmitModuleStep(criticalSection: () throws -> Void) throws {
|
||||
debugLog("starting the emit-module step: locking")
|
||||
try lockAccessor.exclusiveAccess { handle in
|
||||
debugLog("starting the emit-module step: locked")
|
||||
// writing to the file content proactively - incase the critical section never returns
|
||||
// (in case of a fallback to the local compilation), all awaiting swift-frontend processes
|
||||
// will be immediately unblocked
|
||||
handle.write(Self.self.emitModuleContent)
|
||||
try criticalSection()
|
||||
debugLog("lock file emit-module criticial end")
|
||||
}
|
||||
}
|
||||
|
||||
/// Locks a shared file in a loop until its content is non-empty - meaning the "parent" emit-module
|
||||
/// has already finished
|
||||
private func waitForEmitModuleLock(criticalSection: () throws -> Void) throws {
|
||||
// emit-module process should really quickly obtain a lock (it is always invoked
|
||||
// by Xcode as a first process)
|
||||
var executed = false
|
||||
let startingDate = Date()
|
||||
while !executed {
|
||||
debugLog("lock file compilation trying to acquire a lock ....")
|
||||
try lockAccessor.exclusiveAccess { handle in
|
||||
if !handle.availableData.isEmpty {
|
||||
// the file is not empty so the emit-module process is done with the "check"
|
||||
debugLog("swift-frontend lock file is unlocked for compilation")
|
||||
try criticalSection()
|
||||
executed = true
|
||||
} else {
|
||||
debugLog("swift-frontend lock file is not ready for compilation")
|
||||
}
|
||||
}
|
||||
// When a max locking time is achieved, execute anyway
|
||||
if !executed && Date().timeIntervalSince(startingDate) > self.maxLockTimeout {
|
||||
errorLog("""
|
||||
Executing command \(action) without lock synchronization. That may be cause by the\
|
||||
crashed or extremely long emit-module. Contact XCRemoteCache authors about this error.
|
||||
""")
|
||||
try criticalSection()
|
||||
executed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// 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 XCSwiftFrontend: XCSwiftAbstract<SwiftFrontendArgInput> {
|
||||
// don't lock individual compilation invocations for more than 10s
|
||||
private static let MaxLockingTimeout: TimeInterval = 10
|
||||
private let env: [String: String]
|
||||
|
||||
public init(
|
||||
command: String,
|
||||
inputArgs: SwiftFrontendArgInput,
|
||||
env: [String: String],
|
||||
dependenciesWriter: @escaping (URL, FileManager) -> DependenciesWriter,
|
||||
touchFactory: @escaping (URL, FileManager) -> Touch
|
||||
) throws {
|
||||
self.env = env
|
||||
super.init(
|
||||
command: command,
|
||||
inputArgs: inputArgs,
|
||||
dependenciesWriter: dependenciesWriter,
|
||||
touchFactory: touchFactory
|
||||
)
|
||||
}
|
||||
|
||||
override func buildContext() throws -> (XCRemoteCacheConfig, SwiftcContext) {
|
||||
let fileManager = FileManager.default
|
||||
let config: XCRemoteCacheConfig
|
||||
let context: SwiftcContext
|
||||
|
||||
let srcRoot: URL = URL(fileURLWithPath: fileManager.currentDirectoryPath)
|
||||
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileReader: fileManager)
|
||||
.readConfiguration()
|
||||
context = try SwiftcContext(config: config, input: inputArgs)
|
||||
// do not cache this context, as it is subject to change when
|
||||
// the emit-module finds that the cached artifact cannot be used
|
||||
return (config, context)
|
||||
}
|
||||
|
||||
override public func run() throws {
|
||||
do {
|
||||
let (_, context) = try buildContext()
|
||||
|
||||
let frontendContext = try SwiftFrontendContext(context, env: env)
|
||||
let sharedLock = ExclusiveFile(frontendContext.invocationLockFile, mode: .override)
|
||||
|
||||
let action: CommonSwiftFrontendOrchestrator.Action = inputArgs.emitModule ? .emitModule : .compile
|
||||
let swiftFrontendOrchestrator = CommonSwiftFrontendOrchestrator(
|
||||
mode: context.mode,
|
||||
action: action,
|
||||
lockAccessor: sharedLock,
|
||||
maxLockTimeout: Self.self.MaxLockingTimeout
|
||||
)
|
||||
|
||||
try swiftFrontendOrchestrator.run(criticalSection: super.run)
|
||||
} catch {
|
||||
// Splitting into 2 invocations as os_log truncates a massage
|
||||
defaultLog("Cannot correctly orchestrate the \(command) with params \(inputArgs)")
|
||||
defaultLog("Cannot correctly orchestrate error: \(error)")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// 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
|
||||
|
||||
/// Decides if an input to the compilation step should allow reusing the cached artifact
|
||||
protocol AllowedInputDeterminer {
|
||||
/// Decides if the input file is allowed to be compiled, even not specified in the dependency list
|
||||
func allowedNonDependencyInput(file: URL) -> Bool
|
||||
}
|
||||
|
||||
class FilenameBasedAllowedInputDeterminer: AllowedInputDeterminer {
|
||||
private let filenames: [String]
|
||||
|
||||
init(_ filenames: [String]) {
|
||||
self.filenames = filenames
|
||||
}
|
||||
|
||||
func allowedNonDependencyInput(file: URL) -> Bool {
|
||||
return filenames.contains(file.lastPathComponent)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// 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
|
||||
|
||||
/// Products generator that doesn't create any swiftmodule. It is used in the compilation swift-frontend mocking, where
|
||||
/// only individual .o files are created and not .swiftmodule of -Swift.h
|
||||
/// (which is part of swift-frontend -emit-module invocation)
|
||||
class NoopSwiftcProductsGenerator: SwiftcProductsGenerator {
|
||||
func generateFrom(
|
||||
artifactSwiftModuleFiles: [SwiftmoduleFileExtension: URL],
|
||||
artifactSwiftModuleObjCFile: URL
|
||||
) throws -> SwiftcProductsGeneratorOutput {
|
||||
infoLog("""
|
||||
Invoking module generation from NoopSwiftcProductsGenerator does nothing. \
|
||||
It might be a side-effect of a plugin asking to generate a module.
|
||||
""")
|
||||
// NoopSwiftcProductsGenerator is intended only for the swift-frontend
|
||||
let trivialURL = URL(fileURLWithPath: "/non-existing")
|
||||
return SwiftcProductsGeneratorOutput(swiftmoduleDir: trivialURL, objcHeaderFile: trivialURL)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// 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
|
||||
|
||||
class StaticSwiftcInputReader: SwiftcInputReader {
|
||||
private let moduleDependencies: URL?
|
||||
private let swiftDependencies: URL?
|
||||
private let compilationFiles: [SwiftFileCompilationInfo]
|
||||
|
||||
init(
|
||||
moduleDependencies: URL?,
|
||||
swiftDependencies: URL?,
|
||||
compilationFiles: [SwiftFileCompilationInfo]
|
||||
) {
|
||||
self.moduleDependencies = moduleDependencies
|
||||
self.swiftDependencies = swiftDependencies
|
||||
self.compilationFiles = compilationFiles
|
||||
}
|
||||
|
||||
func read() throws -> SwiftCompilationInfo {
|
||||
return .init(
|
||||
info: .init(
|
||||
dependencies: moduleDependencies,
|
||||
swiftDependencies: swiftDependencies
|
||||
),
|
||||
files: compilationFiles
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,7 @@ class Swiftc: SwiftcProtocol {
|
||||
private let dependenciesWriterFactory: (URL, FileManager) -> DependenciesWriter
|
||||
private let touchFactory: (URL, FileManager) -> Touch
|
||||
private let plugins: [SwiftcProductGenerationPlugin]
|
||||
private let allowedInputDeterminer: AllowedInputDeterminer
|
||||
|
||||
init(
|
||||
inputFileListReader: ListReader,
|
||||
@@ -70,7 +71,8 @@ class Swiftc: SwiftcProtocol {
|
||||
fileManager: FileManager,
|
||||
dependenciesWriterFactory: @escaping (URL, FileManager) -> DependenciesWriter,
|
||||
touchFactory: @escaping (URL, FileManager) -> Touch,
|
||||
plugins: [SwiftcProductGenerationPlugin]
|
||||
plugins: [SwiftcProductGenerationPlugin],
|
||||
allowedInputDeterminer: AllowedInputDeterminer
|
||||
) {
|
||||
self.inputFileListReader = inputFileListReader
|
||||
self.markerReader = markerReader
|
||||
@@ -84,6 +86,7 @@ class Swiftc: SwiftcProtocol {
|
||||
self.dependenciesWriterFactory = dependenciesWriterFactory
|
||||
self.touchFactory = touchFactory
|
||||
self.plugins = plugins
|
||||
self.allowedInputDeterminer = allowedInputDeterminer
|
||||
}
|
||||
|
||||
// swiftlint:disable:next function_body_length
|
||||
@@ -96,13 +99,17 @@ class Swiftc: SwiftcProtocol {
|
||||
|
||||
let inputFilesInputs = try inputFileListReader.listFilesURLs()
|
||||
let markerAllowedFiles = try markerReader.listFilesURLs()
|
||||
let allDependencies = Set(markerAllowedFiles + inputFilesInputs)
|
||||
let cachedDependenciesWriterFactory = CachedFileDependenciesWriterFactory(
|
||||
dependencies: markerAllowedFiles,
|
||||
dependencies: Array(allDependencies),
|
||||
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 { 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
|
||||
@@ -132,7 +139,7 @@ class Swiftc: SwiftcProtocol {
|
||||
|
||||
// Read swiftmodule location from XCRemoteCache
|
||||
// arbitrary format swiftmodule/${arch}/${moduleName}.swift{module|doc|sourceinfo}
|
||||
let moduleName = context.modulePathOutput.deletingPathExtension().lastPathComponent
|
||||
let moduleName = context.moduleName
|
||||
let allCompilations = try inputFilesReader.read()
|
||||
let artifactSwiftmoduleDir = artifactLocation
|
||||
.appendingPathComponent("swiftmodule")
|
||||
@@ -145,20 +152,24 @@ class Swiftc: SwiftcProtocol {
|
||||
}
|
||||
)
|
||||
|
||||
// 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(context.objcHeaderOutput.lastPathComponent)
|
||||
// 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 productsGenerator.generateFrom(
|
||||
artifactSwiftModuleFiles: artifactSwiftmoduleFiles,
|
||||
artifactSwiftModuleObjCFile: artifactSwiftModuleObjCFile
|
||||
)
|
||||
}
|
||||
|
||||
try plugins.forEach {
|
||||
try $0.generate(for: allCompilations)
|
||||
@@ -176,8 +187,10 @@ class Swiftc: SwiftcProtocol {
|
||||
try cachedDependenciesWriterFactory.generate(output: individualDeps)
|
||||
}
|
||||
}
|
||||
// Save .d for the entire module
|
||||
try cachedDependenciesWriterFactory.generate(output: allCompilations.info.swiftDependencies)
|
||||
// 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)
|
||||
|
||||
@@ -20,6 +20,53 @@
|
||||
import Foundation
|
||||
|
||||
public struct SwiftcContext {
|
||||
/// Describes the action if the module emit should happen
|
||||
/// that generates .swiftmodule and/or -Swift.h
|
||||
public struct SwiftcStepEmitModule: Equatable {
|
||||
// where the -Swift.h should be placed
|
||||
let objcHeaderOutput: URL
|
||||
// where should the .swiftmodule be placed
|
||||
let modulePathOutput: URL
|
||||
// might be passed as an explicit argument in the swiftc
|
||||
// -emit-dependencies-path
|
||||
let dependencies: URL?
|
||||
}
|
||||
|
||||
/// Which files (from the list of all files in the module)
|
||||
/// should be compiled in this process
|
||||
public enum SwiftcStepCompileFilesScope: Equatable {
|
||||
/// used if only emit module should be done
|
||||
case none
|
||||
case all
|
||||
case subset([URL])
|
||||
}
|
||||
|
||||
/// Describes which steps should be done as a part of this process
|
||||
public struct SwiftcSteps: Equatable {
|
||||
/// which files should be compiled
|
||||
let compileFilesScope: SwiftcStepCompileFilesScope
|
||||
/// if a module should be generated
|
||||
let emitModule: SwiftcStepEmitModule?
|
||||
}
|
||||
|
||||
/// Defines how a list of input files (*.swift) is passed to the invocation
|
||||
public enum CompilationFilesSource: Equatable {
|
||||
/// defined in a separate file (via @/.../*.SwiftFileList)
|
||||
case fileList(String)
|
||||
/// explicitly passed a list of files
|
||||
case list([String])
|
||||
}
|
||||
|
||||
/// Defines how a list of output files (*.d, *.o etc.) is passed to the invocation
|
||||
public enum CompilationFilesInputs: Equatable {
|
||||
/// defined in a separate file (via -output-file-map)
|
||||
case fileMap(String)
|
||||
/// defined in a separate file (via -supplementary-output-file-map)
|
||||
case supplementaryFileMap(String)
|
||||
/// explicitly passed in the invocation
|
||||
case map([String: SwiftFileCompilationInfo])
|
||||
}
|
||||
|
||||
enum SwiftcMode: Equatable {
|
||||
case producer
|
||||
/// Commit sha of the commit to use during remote cache
|
||||
@@ -28,14 +75,13 @@ public struct SwiftcContext {
|
||||
case producerFast
|
||||
}
|
||||
|
||||
let objcHeaderOutput: URL
|
||||
let steps: SwiftcSteps
|
||||
let moduleName: String
|
||||
let modulePathOutput: URL
|
||||
/// File that defines output files locations (.d, .swiftmodule etc.)
|
||||
let filemap: URL
|
||||
/// A source that defines output files locations (.d, .swiftmodule etc.)
|
||||
let inputs: CompilationFilesInputs
|
||||
let target: String
|
||||
/// File that contains input files for the swift module compilation
|
||||
let fileList: URL
|
||||
/// A source that contains all input files for the swift module compilation
|
||||
let compilationFiles: CompilationFilesSource
|
||||
let tempDir: URL
|
||||
let arch: String
|
||||
let prebuildDependenciesPath: String
|
||||
@@ -43,29 +89,29 @@ public struct SwiftcContext {
|
||||
/// File that stores all compilation invocation arguments
|
||||
let invocationHistoryFile: URL
|
||||
|
||||
|
||||
public init(
|
||||
config: XCRemoteCacheConfig,
|
||||
objcHeaderOutput: String,
|
||||
moduleName: String,
|
||||
modulePathOutput: String,
|
||||
filemap: String,
|
||||
steps: SwiftcSteps,
|
||||
inputs: CompilationFilesInputs,
|
||||
target: String,
|
||||
fileList: String
|
||||
compilationFiles: CompilationFilesSource,
|
||||
/// any workspace file path - all other intermediate files for this compilation
|
||||
/// are placed next to it. This path is used to infer the arch and TARGET_TEMP_DIR
|
||||
exampleWorkspaceFilePath: String
|
||||
) throws {
|
||||
self.objcHeaderOutput = URL(fileURLWithPath: objcHeaderOutput)
|
||||
self.moduleName = moduleName
|
||||
self.modulePathOutput = URL(fileURLWithPath: modulePathOutput)
|
||||
self.filemap = URL(fileURLWithPath: filemap)
|
||||
self.steps = steps
|
||||
self.inputs = inputs
|
||||
self.target = target
|
||||
self.fileList = URL(fileURLWithPath: fileList)
|
||||
// modulePathOutput is place in $TARGET_TEMP_DIR/Objects-normal/$ARCH/$TARGET_NAME.swiftmodule
|
||||
self.compilationFiles = compilationFiles
|
||||
// exampleWorkspaceFilePath has a format $TARGET_TEMP_DIR/Objects-normal/$ARCH/some.file
|
||||
// That may be subject to change for other Xcode versions
|
||||
tempDir = URL(fileURLWithPath: modulePathOutput)
|
||||
tempDir = URL(fileURLWithPath: exampleWorkspaceFilePath)
|
||||
.deletingLastPathComponent()
|
||||
.deletingLastPathComponent()
|
||||
.deletingLastPathComponent()
|
||||
arch = URL(fileURLWithPath: modulePathOutput).deletingLastPathComponent().lastPathComponent
|
||||
arch = URL(fileURLWithPath: exampleWorkspaceFilePath).deletingLastPathComponent().lastPathComponent
|
||||
|
||||
let srcRoot: URL = URL(fileURLWithPath: config.sourceRoot)
|
||||
let remoteCommitLocation = URL(fileURLWithPath: config.remoteCommitFile, relativeTo: srcRoot)
|
||||
@@ -92,14 +138,32 @@ public struct SwiftcContext {
|
||||
config: XCRemoteCacheConfig,
|
||||
input: SwiftcArgInput
|
||||
) throws {
|
||||
let steps = SwiftcSteps(
|
||||
compileFilesScope: .all,
|
||||
emitModule: SwiftcStepEmitModule(
|
||||
objcHeaderOutput: URL(fileURLWithPath: (input.objcHeaderOutput)),
|
||||
modulePathOutput: URL(fileURLWithPath: input.modulePathOutput),
|
||||
// in `swiftc`, .d dependencies are pass in the output filemap
|
||||
dependencies: nil
|
||||
)
|
||||
)
|
||||
let inputs = CompilationFilesInputs.fileMap(input.filemap)
|
||||
let compilationFiles = CompilationFilesSource.fileList(input.fileList)
|
||||
try self.init(
|
||||
config: config,
|
||||
objcHeaderOutput: input.objcHeaderOutput,
|
||||
moduleName: input.moduleName,
|
||||
modulePathOutput: input.modulePathOutput,
|
||||
filemap: input.filemap,
|
||||
steps: steps,
|
||||
inputs: inputs,
|
||||
target: input.target,
|
||||
fileList: input.fileList
|
||||
compilationFiles: compilationFiles,
|
||||
exampleWorkspaceFilePath: input.modulePathOutput
|
||||
)
|
||||
}
|
||||
|
||||
init(
|
||||
config: XCRemoteCacheConfig,
|
||||
input: SwiftFrontendArgInput
|
||||
) throws {
|
||||
self = try input.generateSwiftcContext(config: config)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,16 @@
|
||||
// under the License.
|
||||
|
||||
import Foundation
|
||||
import Yams
|
||||
|
||||
/// Errors with reading swiftc inputs
|
||||
enum SwiftcInputReaderError: Error {
|
||||
case readingFailed
|
||||
case invalidFormat
|
||||
/// The file is not in the yaml format
|
||||
case invalidYamlFormat
|
||||
/// The yaml string contains illegal characters
|
||||
case invalidYamlString
|
||||
case missingField(String)
|
||||
}
|
||||
|
||||
@@ -45,10 +50,11 @@ struct SwiftCompilationInfo: Encodable, Equatable {
|
||||
struct SwiftModuleCompilationInfo: Encodable, Equatable {
|
||||
// not present for incremental builds
|
||||
let dependencies: URL?
|
||||
let swiftDependencies: URL
|
||||
// might be nil for the swift-frontend '-c' invocation
|
||||
let swiftDependencies: URL?
|
||||
}
|
||||
|
||||
struct SwiftFileCompilationInfo: Encodable, Equatable {
|
||||
public struct SwiftFileCompilationInfo: Encodable, Hashable {
|
||||
let file: URL
|
||||
// not present for WMO builds
|
||||
let dependencies: URL?
|
||||
@@ -60,11 +66,18 @@ struct SwiftFileCompilationInfo: Encodable, Equatable {
|
||||
|
||||
class SwiftcFilemapInputEditor: SwiftcInputReader, SwiftcInputWriter {
|
||||
|
||||
enum Format {
|
||||
case json
|
||||
case yaml
|
||||
}
|
||||
|
||||
private let file: URL
|
||||
private let fileFormat: Format
|
||||
private let fileManager: FileManager
|
||||
|
||||
init(_ file: URL, fileManager: FileManager) {
|
||||
init(_ file: URL, fileFormat: Format, fileManager: FileManager) {
|
||||
self.file = file
|
||||
self.fileFormat = fileFormat
|
||||
self.fileManager = fileManager
|
||||
}
|
||||
|
||||
@@ -72,7 +85,7 @@ class SwiftcFilemapInputEditor: SwiftcInputReader, SwiftcInputWriter {
|
||||
guard let content = fileManager.contents(atPath: file.path) else {
|
||||
throw SwiftcInputReaderError.readingFailed
|
||||
}
|
||||
guard let representation = try JSONSerialization.jsonObject(with: content, options: []) as? [String: Any] else {
|
||||
guard let representation = try decodeFile(content: content) else {
|
||||
throw SwiftcInputReaderError.invalidFormat
|
||||
}
|
||||
return try SwiftCompilationInfo(from: representation)
|
||||
@@ -82,11 +95,23 @@ class SwiftcFilemapInputEditor: SwiftcInputReader, SwiftcInputWriter {
|
||||
let data = try JSONSerialization.data(withJSONObject: info.dump(), options: [.prettyPrinted])
|
||||
fileManager.createFile(atPath: file.path, contents: data, attributes: nil)
|
||||
}
|
||||
|
||||
private func decodeFile(content: Data) throws -> [String: Any]? {
|
||||
switch fileFormat {
|
||||
case .json:
|
||||
return try JSONSerialization.jsonObject(with: content, options: []) as? [String: Any]
|
||||
case .yaml:
|
||||
guard let stringContent = String(data: content, encoding: .utf8) else {
|
||||
throw SwiftcInputReaderError.invalidYamlString
|
||||
}
|
||||
return try Yams.load(yaml: stringContent) as? [String: Any]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SwiftCompilationInfo {
|
||||
init(from object: [String: Any]) throws {
|
||||
info = try SwiftModuleCompilationInfo(from: object[""])
|
||||
info = try SwiftModuleCompilationInfo(from: object["", default: [:]])
|
||||
files = try object.reduce([]) { prev, new in
|
||||
let (key, value) = new
|
||||
if key.isEmpty {
|
||||
@@ -111,14 +136,14 @@ extension SwiftModuleCompilationInfo {
|
||||
guard let dict = object as? [String: String] else {
|
||||
throw SwiftcInputReaderError.invalidFormat
|
||||
}
|
||||
swiftDependencies = try dict.readURL(key: "swift-dependencies")
|
||||
swiftDependencies = dict.readURL(key: "swift-dependencies")
|
||||
dependencies = dict.readURL(key: "dependencies")
|
||||
}
|
||||
|
||||
func dump() -> [String: String] {
|
||||
return [
|
||||
"dependencies": dependencies?.path,
|
||||
"swift-dependencies": swiftDependencies.path,
|
||||
"swift-dependencies": swiftDependencies?.path,
|
||||
].compactMapValues { $0 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,10 @@ class SwiftcOrchestrator {
|
||||
private let mode: SwiftcContext.SwiftcMode
|
||||
// swiftc command that should be called to generate artifacts
|
||||
private let swiftcCommand: String
|
||||
private let objcHeaderOutput: URL
|
||||
private let moduleOutput: URL
|
||||
// Might be nil if invoking from frontend compilation: `swift-frontend -c`
|
||||
private let objcHeaderOutput: URL?
|
||||
// Might be nil if invoking from frontend compilation: `swift-frontend -c`
|
||||
private let moduleOutput: URL?
|
||||
private let arch: String
|
||||
private let artifactBuilder: ArtifactSwiftProductsBuilder
|
||||
private let shellOut: ShellOut
|
||||
@@ -39,8 +41,8 @@ class SwiftcOrchestrator {
|
||||
mode: SwiftcContext.SwiftcMode,
|
||||
swiftc: SwiftcProtocol,
|
||||
swiftcCommand: String,
|
||||
objcHeaderOutput: URL,
|
||||
moduleOutput: URL,
|
||||
objcHeaderOutput: URL?,
|
||||
moduleOutput: URL?,
|
||||
arch: String,
|
||||
artifactBuilder: ArtifactSwiftProductsBuilder,
|
||||
producerFallbackCommandProcessors: [ShellCommandsProcessor],
|
||||
@@ -128,10 +130,14 @@ class SwiftcOrchestrator {
|
||||
try processor.applyArgsRewrite(args)
|
||||
}
|
||||
try fallbackToDefaultAndWait(command: swiftcCommand, args: swiftcArgs)
|
||||
// move generated .h to the location where artifact creator expects it
|
||||
try artifactBuilder.includeObjCHeaderToTheArtifact(arch: arch, headerURL: objcHeaderOutput)
|
||||
// move generated .swiftmodule to the location where artifact creator expects it
|
||||
try artifactBuilder.includeModuleDefinitionsToTheArtifact(arch: arch, moduleURL: moduleOutput)
|
||||
if let objcHeaderOutput = objcHeaderOutput {
|
||||
// move generated .h to the location where artifact creator expects it
|
||||
try artifactBuilder.includeObjCHeaderToTheArtifact(arch: arch, headerURL: objcHeaderOutput)
|
||||
}
|
||||
if let moduleOutput = moduleOutput {
|
||||
// move generated .swiftmodule to the location where artifact creator expects it
|
||||
try artifactBuilder.includeModuleDefinitionsToTheArtifact(arch: arch, moduleURL: moduleOutput)
|
||||
}
|
||||
|
||||
try producerFallbackCommandProcessors.forEach {
|
||||
try $0.postCommandProcessing()
|
||||
|
||||
@@ -45,15 +45,15 @@ public struct SwiftcArgInput {
|
||||
}
|
||||
}
|
||||
|
||||
public class XCSwiftc {
|
||||
private let command: String
|
||||
private let inputArgs: SwiftcArgInput
|
||||
public class XCSwiftAbstract<InputArgs> {
|
||||
let command: String
|
||||
let inputArgs: InputArgs
|
||||
private let dependenciesWriterFactory: (URL, FileManager) -> DependenciesWriter
|
||||
private let touchFactory: (URL, FileManager) -> Touch
|
||||
|
||||
public init(
|
||||
command: String,
|
||||
inputArgs: SwiftcArgInput,
|
||||
inputArgs: InputArgs,
|
||||
dependenciesWriter: @escaping (URL, FileManager) -> DependenciesWriter,
|
||||
touchFactory: @escaping (URL, FileManager) -> Touch
|
||||
) {
|
||||
@@ -63,26 +63,52 @@ public class XCSwiftc {
|
||||
self.touchFactory = touchFactory
|
||||
}
|
||||
|
||||
func buildContext() throws -> (XCRemoteCacheConfig, SwiftcContext) {
|
||||
fatalError("Need to override in \(Self.self)")
|
||||
}
|
||||
|
||||
// swiftlint:disable:next function_body_length
|
||||
public func run() {
|
||||
public func run() throws {
|
||||
let fileManager = FileManager.default
|
||||
let config: XCRemoteCacheConfig
|
||||
let context: SwiftcContext
|
||||
do {
|
||||
let srcRoot: URL = URL(fileURLWithPath: fileManager.currentDirectoryPath)
|
||||
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileReader: fileManager)
|
||||
.readConfiguration()
|
||||
context = try SwiftcContext(config: config, input: inputArgs)
|
||||
} catch {
|
||||
exit(1, "FATAL: Swiftc initialization failed with error: \(error)")
|
||||
}
|
||||
let (config, context) = try buildContext()
|
||||
|
||||
let swiftcCommand = config.swiftcCommand
|
||||
let markerURL = context.tempDir.appendingPathComponent(config.modeMarkerPath)
|
||||
let markerReader = FileMarkerReader(markerURL, fileManager: fileManager)
|
||||
let markerWriter = FileMarkerWriter(markerURL, fileAccessor: fileManager)
|
||||
|
||||
let inputReader = SwiftcFilemapInputEditor(context.filemap, fileManager: fileManager)
|
||||
let fileListEditor = FileListEditor(context.fileList, fileManager: fileManager)
|
||||
let inputReader: SwiftcInputReader
|
||||
switch context.inputs {
|
||||
case .fileMap(let path):
|
||||
inputReader = SwiftcFilemapInputEditor(
|
||||
URL(fileURLWithPath: path),
|
||||
fileFormat: .json,
|
||||
fileManager: fileManager
|
||||
)
|
||||
case .supplementaryFileMap(let path):
|
||||
// Supplementary file map is endoded in the yaml file (contraty to
|
||||
// the standard filemap, which is in json)
|
||||
inputReader = SwiftcFilemapInputEditor(
|
||||
URL(fileURLWithPath: path),
|
||||
fileFormat: .yaml,
|
||||
fileManager: fileManager
|
||||
)
|
||||
case .map(let map):
|
||||
// static - passed via the arguments list
|
||||
inputReader = StaticSwiftcInputReader(
|
||||
moduleDependencies: context.steps.emitModule?.dependencies,
|
||||
// with Xcode 14, inputs via cmd are only used for compilations
|
||||
swiftDependencies: nil,
|
||||
compilationFiles: Array(map.values)
|
||||
)
|
||||
}
|
||||
let fileListReader: ListReader
|
||||
switch context.compilationFiles {
|
||||
case .fileList(let path):
|
||||
fileListReader = FileListEditor(URL(fileURLWithPath: path), fileManager: fileManager)
|
||||
case .list(let paths):
|
||||
fileListReader = StaticFileListReader(list: paths.map(URL.init(fileURLWithPath:)))
|
||||
}
|
||||
let artifactOrganizer = ZipArtifactOrganizer(
|
||||
targetTempDir: context.tempDir,
|
||||
// xcswiftc doesn't call artifact preprocessing
|
||||
@@ -101,11 +127,20 @@ public class XCSwiftc {
|
||||
moduleName: context.moduleName,
|
||||
fileManager: fileManager
|
||||
)
|
||||
let productsGenerator = DiskSwiftcProductsGenerator(
|
||||
modulePathOutput: context.modulePathOutput,
|
||||
objcHeaderOutput: context.objcHeaderOutput,
|
||||
diskCopier: HardLinkDiskCopier(fileManager: fileManager)
|
||||
)
|
||||
let productsGenerator: SwiftcProductsGenerator
|
||||
if let emitModule = context.steps.emitModule {
|
||||
productsGenerator = DiskSwiftcProductsGenerator(
|
||||
modulePathOutput: emitModule.modulePathOutput,
|
||||
objcHeaderOutput: emitModule.objcHeaderOutput,
|
||||
diskCopier: HardLinkDiskCopier(fileManager: fileManager)
|
||||
)
|
||||
} else {
|
||||
// If the module was not requested for this proces (compiling files only)
|
||||
// do nothing, when someone (e.g. a plugin) asks for the products generation
|
||||
// This generation will happend in a separate process, where the module
|
||||
// generation is requested
|
||||
productsGenerator = NoopSwiftcProductsGenerator()
|
||||
}
|
||||
let allInvocationsStorage = ExistingFileStorage(
|
||||
storageFile: context.invocationHistoryFile,
|
||||
command: swiftcCommand
|
||||
@@ -117,9 +152,12 @@ public class XCSwiftc {
|
||||
retrieveIgnoredCommands: [swiftcCommand]
|
||||
)
|
||||
let shellOut = ProcessShellOut()
|
||||
// Always allow an input file from the actool generation step
|
||||
// As of Xcode15, the filename is confirmed to be static
|
||||
let allowedInputDeterminer = FilenameBasedAllowedInputDeterminer(["GeneratedAssetSymbols.swift"])
|
||||
|
||||
let swiftc = Swiftc(
|
||||
inputFileListReader: fileListEditor,
|
||||
inputFileListReader: fileListReader,
|
||||
markerReader: markerReader,
|
||||
allowedFilesListScanner: allowedFilesListScanner,
|
||||
artifactOrganizer: artifactOrganizer,
|
||||
@@ -130,24 +168,35 @@ public class XCSwiftc {
|
||||
fileManager: fileManager,
|
||||
dependenciesWriterFactory: dependenciesWriterFactory,
|
||||
touchFactory: touchFactory,
|
||||
plugins: []
|
||||
plugins: [],
|
||||
allowedInputDeterminer: allowedInputDeterminer
|
||||
)
|
||||
let orchestrator = SwiftcOrchestrator(
|
||||
mode: context.mode,
|
||||
swiftc: swiftc,
|
||||
swiftcCommand: swiftcCommand,
|
||||
objcHeaderOutput: context.objcHeaderOutput,
|
||||
moduleOutput: context.modulePathOutput,
|
||||
objcHeaderOutput: context.steps.emitModule?.objcHeaderOutput,
|
||||
moduleOutput: context.steps.emitModule?.modulePathOutput,
|
||||
arch: context.arch,
|
||||
artifactBuilder: artifactBuilder,
|
||||
producerFallbackCommandProcessors: [],
|
||||
invocationStorage: invocationStorage,
|
||||
shellOut: shellOut
|
||||
)
|
||||
do {
|
||||
try orchestrator.run()
|
||||
} catch {
|
||||
exit(1, "Swiftc failed with error: \(error)")
|
||||
}
|
||||
try orchestrator.run()
|
||||
}
|
||||
}
|
||||
|
||||
public class XCSwiftc: XCSwiftAbstract<SwiftcArgInput> {
|
||||
override func buildContext() throws -> (XCRemoteCacheConfig, SwiftcContext) {
|
||||
let fileReader = FileManager.default
|
||||
let config: XCRemoteCacheConfig
|
||||
let context: SwiftcContext
|
||||
let srcRoot: URL = URL(fileURLWithPath: fileReader.currentDirectoryPath)
|
||||
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileReader: fileReader)
|
||||
.readConfiguration()
|
||||
context = try SwiftcContext(config: config, input: inputArgs)
|
||||
|
||||
return (config, context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
// swiftlint:disable file_length
|
||||
|
||||
import Foundation
|
||||
import Yams
|
||||
|
||||
@@ -57,6 +59,8 @@ public struct XCRemoteCacheConfig: Encodable {
|
||||
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')
|
||||
@@ -148,6 +152,11 @@ public struct XCRemoteCacheConfig: Encodable {
|
||||
/// 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 {
|
||||
@@ -206,6 +215,9 @@ extension XCRemoteCacheConfig {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -273,6 +285,8 @@ struct ConfigFileScheme: Decodable {
|
||||
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 {
|
||||
@@ -323,6 +337,8 @@ struct ConfigFileScheme: Decodable {
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
// 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
|
||||
|
||||
/// Parser for `assetcatalog_dependencies` file: an output of the `actool`
|
||||
/// that lists all dependencies of this command
|
||||
class AssetsFileDependenciesReader: DependenciesReader {
|
||||
private let file: URL
|
||||
private let dirAccessor: DirAccessor
|
||||
|
||||
public init(_ file: URL, dirAccessor: DirAccessor) {
|
||||
self.file = file
|
||||
self.dirAccessor = dirAccessor
|
||||
}
|
||||
|
||||
public func findDependencies() throws -> [String] {
|
||||
return try Array(findAllDependencies())
|
||||
}
|
||||
|
||||
public func findInputs() throws -> [String] {
|
||||
// XCRemoteCache doesn't use it yet
|
||||
exit(1, "TODO: implement")
|
||||
}
|
||||
|
||||
public func readFilesAndDependencies() throws -> [String: [String]] {
|
||||
return try ["": findAllDependencies()]
|
||||
}
|
||||
|
||||
private func findAllDependencies() throws -> [String] {
|
||||
let fileData = try getFileData()
|
||||
// all dependency files are separated by the \0 byte
|
||||
// each path has a file type prefix:
|
||||
// 0x10 - directory
|
||||
// 0x40 - file
|
||||
// We only care about dirs, as *.xcassets is a folder
|
||||
let pathDatas = fileData.split(separator: 0x0)
|
||||
let paths = pathDatas
|
||||
.filter { !$0.isEmpty && $0.first == 0x10 }
|
||||
.map { String(data: $0.dropFirst(), encoding: .utf8)! }
|
||||
.map(URL.init(fileURLWithPath:))
|
||||
let xcassetsPaths = paths.filter { path in
|
||||
path.pathExtension == "xcassets"
|
||||
}
|
||||
return try xcassetsPaths.flatMap { try findAssetsContentJsons(xcasset: $0) }
|
||||
}
|
||||
|
||||
private func findAssetsContentJsons(xcasset: URL) throws -> [String] {
|
||||
return try dirAccessor.recursiveItems(at: xcasset).filter { url in
|
||||
url.lastPathComponent == "Contents.json"
|
||||
}.map(\.path)
|
||||
}
|
||||
|
||||
private func getFileData() throws -> Data {
|
||||
guard let fileData = try dirAccessor.contents(atPath: file.path) else {
|
||||
throw DependenciesReaderError.readingError
|
||||
}
|
||||
return fileData
|
||||
}
|
||||
|
||||
}
|
||||
@@ -48,6 +48,7 @@ class PhaseCacheModeController: CacheModeController {
|
||||
private let dependenciesWriter: DependenciesWriter
|
||||
private let dependenciesReader: DependenciesReader
|
||||
private let markerWriter: MarkerWriter
|
||||
private let llbuildLockFile: URL
|
||||
private let fileManager: FileManager
|
||||
|
||||
init(
|
||||
@@ -59,6 +60,7 @@ class PhaseCacheModeController: CacheModeController {
|
||||
dependenciesWriter: (URL, FileManager) -> DependenciesWriter,
|
||||
dependenciesReader: (URL, FileManager) -> DependenciesReader,
|
||||
markerWriter: (URL, FileManager) -> MarkerWriter,
|
||||
llbuildLockFile: URL,
|
||||
fileManager: FileManager
|
||||
) {
|
||||
|
||||
@@ -69,10 +71,12 @@ class PhaseCacheModeController: CacheModeController {
|
||||
let discoveryURL = tempDir.appendingPathComponent(phaseDependencyPath)
|
||||
self.dependenciesWriter = dependenciesWriter(discoveryURL, fileManager)
|
||||
self.dependenciesReader = dependenciesReader(discoveryURL, fileManager)
|
||||
self.llbuildLockFile = llbuildLockFile
|
||||
self.markerWriter = markerWriter(modeMarker, fileManager)
|
||||
}
|
||||
|
||||
func enable(allowedInputFiles: [URL], dependencies: [URL]) throws {
|
||||
try cleanupLlBuildLock()
|
||||
// marker file contains filepaths that contribute to the build products
|
||||
// and should invalidate all other target steps (swiftc,libtool etc.)
|
||||
let targetSensitiveFiles = dependencies + [modeMarker, Self.xcodeSelectLink]
|
||||
@@ -84,6 +88,7 @@ class PhaseCacheModeController: CacheModeController {
|
||||
}
|
||||
|
||||
func disable() throws {
|
||||
try cleanupLlBuildLock()
|
||||
guard !forceCached else {
|
||||
throw PhaseCacheModeControllerError.cannotUseRemoteCacheForForcedCacheMode
|
||||
}
|
||||
@@ -110,8 +115,24 @@ class PhaseCacheModeController: CacheModeController {
|
||||
} catch {
|
||||
// Gracefully don't disable a cache
|
||||
// That may happen if building a target for the first time
|
||||
errorLog("Couldn't verify if should disable RC for \(commitValue).")
|
||||
debugLog("Couldn't verify if should disable RC for \(commitValue).")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// cleanup the build lock file (if exists) as the very last step of this controller
|
||||
// this is just a non-critical cleanup step to not leave {{LLBUILD_BUILD_ID}}.lock
|
||||
// files in $TARGET_TEMP_DIR. It is expected that both prebuild and postbuild will
|
||||
// invoke it, to ensure:
|
||||
// - swift-frontend synchronization is done per-target build
|
||||
// - no .lock leftover files
|
||||
private func cleanupLlBuildLock() throws {
|
||||
if fileManager.fileExists(atPath: llbuildLockFile.path) {
|
||||
do {
|
||||
try fileManager.removeItem(at: llbuildLockFile)
|
||||
} catch {
|
||||
printWarning("Removing llbuild lock at \(llbuildLockFile.path) failed. Error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// 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
|
||||
|
||||
class StaticFileListReader: ListReader {
|
||||
private let list: [URL]
|
||||
|
||||
init(list: [URL]) {
|
||||
self.list = list
|
||||
}
|
||||
|
||||
func listFilesURLs() throws -> [URL] {
|
||||
list
|
||||
}
|
||||
|
||||
func canRead() -> Bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -21,26 +21,35 @@ import Foundation
|
||||
|
||||
/// Reads and aggregates all compilation dependencies from a single directory
|
||||
class TargetDependenciesReader: DependenciesReader {
|
||||
private let directory: URL
|
||||
// As of Xcode15, the filename is static
|
||||
private static let assetsDependenciesFilename = "assetcatalog_dependencies"
|
||||
private let compilationDirectory: URL
|
||||
private let assetsCatalogOutputDir: URL
|
||||
private let dirScanner: DirScanner
|
||||
private let fileDependeciesReaderFactory: (URL) -> DependenciesReader
|
||||
private let fileDependenciesReaderFactory: (URL) -> DependenciesReader
|
||||
private let assetsDependenciesReaderFactory: (URL) -> DependenciesReader
|
||||
|
||||
public init(
|
||||
_ directory: URL,
|
||||
fileDependeciesReaderFactory: @escaping (URL) -> DependenciesReader,
|
||||
compilationOutputDir: URL,
|
||||
assetsCatalogOutputDir: URL,
|
||||
fileDependenciesReaderFactory: @escaping (URL) -> DependenciesReader,
|
||||
assetsDependenciesReaderFactory: @escaping (URL) -> DependenciesReader,
|
||||
dirScanner: DirScanner
|
||||
) {
|
||||
self.directory = directory
|
||||
self.compilationDirectory = compilationOutputDir
|
||||
self.assetsCatalogOutputDir = assetsCatalogOutputDir
|
||||
self.dirScanner = dirScanner
|
||||
self.fileDependeciesReaderFactory = fileDependeciesReaderFactory
|
||||
self.fileDependenciesReaderFactory = fileDependenciesReaderFactory
|
||||
self.assetsDependenciesReaderFactory = assetsDependenciesReaderFactory
|
||||
}
|
||||
|
||||
// Optimized way of finding dependencies only for files that have corresponding .o file on a disk
|
||||
// includes also inputs to the `actool` assets generator
|
||||
public func findDependencies() throws -> [String] {
|
||||
// Not calling `readFilesAndDependencies` as it may unnecessary call expensive `findDependencies()` for
|
||||
// files that eventually will not be considered
|
||||
let allURLs = try dirScanner.items(at: directory)
|
||||
let mergedDependencies = try allURLs.reduce(Set<String>()) { (prev: Set<String>, file) in
|
||||
let allCompilationOutputURLs = try dirScanner.items(at: compilationDirectory)
|
||||
var mergedDependencies = try allCompilationOutputURLs.reduce(Set<String>()) { (prev: Set<String>, file) in
|
||||
// include only these .d files that either have corresponding .o file (incremental) or end
|
||||
// with '-master' (whole-module)
|
||||
// Otherwise .d is probably just a leftover from previous builds
|
||||
@@ -53,20 +62,33 @@ class TargetDependenciesReader: DependenciesReader {
|
||||
return prev
|
||||
}
|
||||
|
||||
return try prev.union(fileDependeciesReaderFactory(file).findDependencies())
|
||||
return try prev.union(fileDependenciesReaderFactory(file).findDependencies())
|
||||
}
|
||||
// include also dependencies from optional assets compilation (`actool`)
|
||||
try mergedDependencies.formUnion(findAssetsCatalogDependencies())
|
||||
return Array(mergedDependencies).sorted()
|
||||
}
|
||||
|
||||
// finds all assets compilation's dependencies, which are always appended to the list of
|
||||
// files to compare on the consumer side (in the fingerprint comparison)
|
||||
private func findAssetsCatalogDependencies() throws -> Set<String> {
|
||||
let expectedAssetsDepsFile = assetsCatalogOutputDir
|
||||
.appendingPathComponent(Self.assetsDependenciesFilename)
|
||||
guard try dirScanner.itemType(atPath: expectedAssetsDepsFile.path) == .file else {
|
||||
return []
|
||||
}
|
||||
return try Set(assetsDependenciesReaderFactory(expectedAssetsDepsFile).findDependencies())
|
||||
}
|
||||
|
||||
public func findInputs() throws -> [String] {
|
||||
fatalError("TODO: implement")
|
||||
}
|
||||
|
||||
public func readFilesAndDependencies() throws -> [String: [String]] {
|
||||
let allURLs = try dirScanner.items(at: directory)
|
||||
let allURLs = try dirScanner.items(at: compilationDirectory)
|
||||
return try allURLs.reduce([String: [String]]()) { prev, file in
|
||||
var new = prev
|
||||
new[file.path] = try fileDependeciesReaderFactory(file).findDependencies()
|
||||
new[file.path] = try fileDependenciesReaderFactory(file).findDependencies()
|
||||
return new
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ enum RemoteCommitInfo: Equatable {
|
||||
extension RemoteCommitInfo {
|
||||
init(_ commit: String?) {
|
||||
switch commit {
|
||||
case .some(let value) where !value.isEmpty :
|
||||
case .some(let value) where !value.isEmpty:
|
||||
self = .available(commit: value)
|
||||
default:
|
||||
self = .unavailable
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// 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 extension Array {
|
||||
func get(_ i: Index) -> Element? {
|
||||
guard count > i else {
|
||||
return nil
|
||||
}
|
||||
return self[i]
|
||||
}
|
||||
}
|
||||
@@ -32,9 +32,12 @@ public class XCLibtoolHelper {
|
||||
var inputLibraries: [String] = []
|
||||
var filelist: String?
|
||||
var dependencyInfo: String?
|
||||
var asksForVersion = false
|
||||
var i = 0
|
||||
while i < args.count {
|
||||
switch args[i] {
|
||||
case "-V":
|
||||
asksForVersion = true
|
||||
case "-o":
|
||||
output = args[i + 1]
|
||||
i += 1
|
||||
@@ -52,6 +55,9 @@ public class XCLibtoolHelper {
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
if asksForVersion {
|
||||
return .version
|
||||
}
|
||||
guard let outputInput = output else {
|
||||
throw XCLibtoolHelperError.missingOutput
|
||||
}
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
// 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 `swift-frontend` that skips compilation and
|
||||
/// produces empty output files (.o). Just like in xcswiftc, compilation dependencies
|
||||
/// (.d) files are copied from the prebuild marker file which includes all relevant files
|
||||
/// Fallbacks to a standard `swift-frontend` when the
|
||||
/// ramote cache is not applicable (e.g. modified sources)
|
||||
public class XCSwiftcFrontendMain {
|
||||
// swiftlint:disable:next function_body_length cyclomatic_complexity
|
||||
public func main() {
|
||||
let env = ProcessInfo.processInfo.environment
|
||||
// Do not invoke raw swift-frontend because that would lead to the infinite loop
|
||||
// swift-frontent -> xcswift-frontent -> swift-frontent
|
||||
//
|
||||
// Note: Returning the `swiftc` executaion here because it is possible to pass all arguments
|
||||
// from swift-frontend to `swiftc` and swiftc will be able to redirect to swift-frontend
|
||||
// (because the first argument is `-frontend`). If that is not a case (might change in
|
||||
// future swift compiler versions), invoke swift-frontend from the Xcode, but that introduces
|
||||
// a limitation that disallows custom toolchains in Xcode:
|
||||
// $DEVELOPER_DIR/Toolchains/XcodeDefault.xctoolchain/usr/bin/{ ProcessInfo().processName}
|
||||
let command = "swiftc"
|
||||
let args = ProcessInfo().arguments
|
||||
var compile = false
|
||||
var emitModule = false
|
||||
var objcHeaderOutput: String?
|
||||
var moduleName: String?
|
||||
var target: String?
|
||||
var inputPaths: [String] = []
|
||||
var primaryInputPaths: [String] = []
|
||||
var outputPaths: [String] = []
|
||||
var dependenciesPaths: [String] = []
|
||||
var diagnosticsPaths: [String] = []
|
||||
var sourceInfoPath: String?
|
||||
var docPath: String?
|
||||
var supplementaryOutputFileMap: String?
|
||||
|
||||
for i in 0..<args.count {
|
||||
let arg = args[i]
|
||||
switch arg {
|
||||
case "-c":
|
||||
compile = true
|
||||
case "-emit-module":
|
||||
emitModule = true
|
||||
case "-o":
|
||||
outputPaths.append(args[i + 1])
|
||||
case "-emit-objc-header-path":
|
||||
objcHeaderOutput = args[i + 1]
|
||||
case "-module-name":
|
||||
moduleName = args[i + 1]
|
||||
case "-target":
|
||||
target = args[i + 1]
|
||||
case "-serialize-diagnostics-path":
|
||||
// .dia
|
||||
diagnosticsPaths.append(args[i + 1])
|
||||
case "-emit-dependencies-path":
|
||||
// .d
|
||||
dependenciesPaths.append(args[i + 1])
|
||||
case "-emit-module-source-info-path":
|
||||
// .swiftsourceinfo
|
||||
sourceInfoPath = args[i + 1]
|
||||
case "-emit-module-doc-path":
|
||||
// .swiftdoc
|
||||
docPath = args[i + 1]
|
||||
case "-primary-file":
|
||||
// .swift
|
||||
primaryInputPaths.append(args[i + 1])
|
||||
case "-supplementary-output-file-map":
|
||||
supplementaryOutputFileMap = args[i + 1]
|
||||
default:
|
||||
if arg.hasSuffix(".swift") {
|
||||
inputPaths.append(arg)
|
||||
}
|
||||
}
|
||||
}
|
||||
// support either emitModule (the preflight step) or compilation
|
||||
// all other types of invocations (like -print-target-info) should be
|
||||
// automatically redirected to the original swift-frontend
|
||||
let argInput = SwiftFrontendArgInput(
|
||||
compile: compile,
|
||||
emitModule: emitModule,
|
||||
objcHeaderOutput: objcHeaderOutput,
|
||||
moduleName: moduleName,
|
||||
target: target,
|
||||
primaryInputPaths: primaryInputPaths,
|
||||
inputPaths: inputPaths,
|
||||
outputPaths: outputPaths,
|
||||
dependenciesPaths: dependenciesPaths,
|
||||
diagnosticsPaths: diagnosticsPaths,
|
||||
sourceInfoPath: sourceInfoPath,
|
||||
docPath: docPath,
|
||||
supplementaryOutputFileMap: supplementaryOutputFileMap
|
||||
)
|
||||
// swift-frontend is first invoked with some "probing" args like
|
||||
// -print-target-info
|
||||
guard emitModule != compile else {
|
||||
runFallback(envs: env)
|
||||
}
|
||||
|
||||
do {
|
||||
let frontend = try XCSwiftFrontend(
|
||||
command: command,
|
||||
inputArgs: argInput,
|
||||
env: env,
|
||||
dependenciesWriter: FileDependenciesWriter.init,
|
||||
touchFactory: FileTouch.init)
|
||||
try frontend.run()
|
||||
} catch {
|
||||
runFallback(envs: env)
|
||||
}
|
||||
}
|
||||
|
||||
private func runFallback(envs env: [String: String]) -> Never {
|
||||
// DEVELOPER_DIR is always set by Xcode
|
||||
let developerDir = env["DEVELOPER_DIR"]!
|
||||
// limitation: always using the Xcode's toolchain, otherwise
|
||||
// there will be a loop for invoking swift-frontend wrapper from XCRemoteCache
|
||||
// Cause: for injecting into the swift driver pipeline, Xcode looks for
|
||||
// an executable with the name `swift-frontend` that is placed in the same
|
||||
// dir as `SWIFT_EXEC`'s `swiftc` wrapper
|
||||
let swiftFrontendCommand = "\(developerDir)/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-frontend"
|
||||
|
||||
let args = ProcessInfo().arguments
|
||||
let paramList = [swiftFrontendCommand] + args.dropFirst()
|
||||
let cargs = paramList.map { strdup($0) } + [nil]
|
||||
execvp(swiftFrontendCommand, cargs)
|
||||
|
||||
/// C-function `execv` returns only when the command fails
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
XCSwiftcFrontendMain().main()
|
||||
@@ -60,30 +60,37 @@ public class XCSwiftcMain {
|
||||
let targetInputInput = target,
|
||||
let swiftFileListInput = swiftFileList
|
||||
else {
|
||||
let swiftcCommand = "swiftc"
|
||||
print("Fallbacking to compilation using \(swiftcCommand).")
|
||||
|
||||
let args = ProcessInfo().arguments
|
||||
let paramList = [swiftcCommand] + args.dropFirst()
|
||||
let cargs = paramList.map { strdup($0) } + [nil]
|
||||
execvp(swiftcCommand, cargs)
|
||||
|
||||
/// C-function `execv` returns only when the command fails
|
||||
exit(1)
|
||||
executeFallback()
|
||||
}
|
||||
let swiftcArgsInput = SwiftcArgInput(
|
||||
objcHeaderOutput: objcHeaderOutputInput,
|
||||
moduleName: moduleNameInput,
|
||||
modulePathOutput: modulePathOutputInput,
|
||||
filemap: filemapInput,
|
||||
target: targetInputInput,
|
||||
fileList: swiftFileListInput
|
||||
)
|
||||
XCSwiftc(
|
||||
command: command,
|
||||
inputArgs: swiftcArgsInput,
|
||||
dependenciesWriter: FileDependenciesWriter.init,
|
||||
touchFactory: FileTouch.init
|
||||
).run()
|
||||
do {
|
||||
let swiftcArgsInput = SwiftcArgInput(
|
||||
objcHeaderOutput: objcHeaderOutputInput,
|
||||
moduleName: moduleNameInput,
|
||||
modulePathOutput: modulePathOutputInput,
|
||||
filemap: filemapInput,
|
||||
target: targetInputInput,
|
||||
fileList: swiftFileListInput
|
||||
)
|
||||
try XCSwiftc(
|
||||
command: command,
|
||||
inputArgs: swiftcArgsInput,
|
||||
dependenciesWriter: FileDependenciesWriter.init,
|
||||
touchFactory: FileTouch.init
|
||||
).run()
|
||||
} catch {
|
||||
executeFallback()
|
||||
}
|
||||
}
|
||||
private func executeFallback() -> Never {
|
||||
let swiftcCommand = "swiftc"
|
||||
print("Fallbacking to compilation using \(swiftcCommand).")
|
||||
|
||||
let args = ProcessInfo().arguments
|
||||
let paramList = [swiftcCommand] + args.dropFirst()
|
||||
let cargs = paramList.map { strdup($0) } + [nil]
|
||||
execvp(swiftcCommand, cargs)
|
||||
|
||||
/// C-function `execv` returns only when the command fails
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ class PostbuildContextTests: FileXCTestCase {
|
||||
"TARGET_TEMP_DIR": "TARGET_TEMP_DIR",
|
||||
"DERIVED_FILE_DIR": "DERIVED_FILE_DIR",
|
||||
"ARCHS": "x86_64",
|
||||
"OBJECT_FILE_DIR_normal": "/OBJECT_FILE_DIR_normal" ,
|
||||
"OBJECT_FILE_DIR_normal": "/OBJECT_FILE_DIR_normal",
|
||||
"CONFIGURATION": "CONFIGURATION",
|
||||
"PLATFORM_NAME": "PLATFORM_NAME",
|
||||
"XCODE_PRODUCT_BUILD_VERSION": "XCODE_PRODUCT_BUILD_VERSION",
|
||||
@@ -45,6 +45,7 @@ class PostbuildContextTests: FileXCTestCase {
|
||||
"DERIVED_SOURCES_DIR": "DERIVED_SOURCES_DIR",
|
||||
"CURRENT_VARIANT": "normal",
|
||||
"PUBLIC_HEADERS_FOLDER_PATH": "/usr/local/include",
|
||||
"LLBUILD_BUILD_ID": "1",
|
||||
]
|
||||
|
||||
override func setUpWithError() throws {
|
||||
@@ -186,4 +187,17 @@ class PostbuildContextTests: FileXCTestCase {
|
||||
|
||||
XCTAssertFalse(context.disabled)
|
||||
}
|
||||
|
||||
func testFailsIfLlBuildIdEnvIsMissing() throws {
|
||||
var envs = Self.SampleEnvs
|
||||
envs.removeValue(forKey: "LLBUILD_BUILD_ID")
|
||||
|
||||
XCTAssertThrowsError(try PostbuildContext(config, env: envs))
|
||||
}
|
||||
|
||||
func testBuildsLockValidFileUrl() throws {
|
||||
let context = try PostbuildContext(config, env: Self.SampleEnvs)
|
||||
|
||||
XCTAssertEqual(context.llbuildIdLockFile, "TARGET_TEMP_DIR/1.lock")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,8 @@ class PostbuildTests: FileXCTestCase {
|
||||
overlayHeadersPath: "",
|
||||
irrelevantDependenciesPaths: [],
|
||||
publicHeadersFolderPath: nil,
|
||||
disabled: false
|
||||
disabled: false,
|
||||
llbuildIdLockFile: "/file"
|
||||
)
|
||||
private var network = RemoteNetworkClientImpl(
|
||||
NetworkClientFake(fileManager: .default),
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// 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 PrebuildContextTests: FileXCTestCase {
|
||||
private var config: XCRemoteCacheConfig!
|
||||
private var remoteCommitFile: URL!
|
||||
private static let SampleEnvs = [
|
||||
"TARGET_NAME": "TARGET_NAME",
|
||||
"TARGET_TEMP_DIR": "TARGET_TEMP_DIR",
|
||||
"DERIVED_FILE_DIR": "DERIVED_FILE_DIR",
|
||||
"ARCHS": "x86_64",
|
||||
"OBJECT_FILE_DIR_normal": "/OBJECT_FILE_DIR_normal",
|
||||
"CONFIGURATION": "CONFIGURATION",
|
||||
"PLATFORM_NAME": "PLATFORM_NAME",
|
||||
"XCODE_PRODUCT_BUILD_VERSION": "XCODE_PRODUCT_BUILD_VERSION",
|
||||
"TARGET_BUILD_DIR": "TARGET_BUILD_DIR",
|
||||
"PRODUCT_MODULE_NAME": "PRODUCT_MODULE_NAME",
|
||||
"EXECUTABLE_PATH": "EXECUTABLE_PATH",
|
||||
"SRCROOT": "SRCROOT",
|
||||
"DEVELOPER_DIR": "DEVELOPER_DIR",
|
||||
"MACH_O_TYPE": "MACH_O_TYPE",
|
||||
"DWARF_DSYM_FILE_SHOULD_ACCOMPANY_PRODUCT": "DWARF_DSYM_FILE_SHOULD_ACCOMPANY_PRODUCT",
|
||||
"DWARF_DSYM_FOLDER_PATH": "DWARF_DSYM_FOLDER_PATH",
|
||||
"DWARF_DSYM_FILE_NAME": "DWARF_DSYM_FILE_NAME",
|
||||
"BUILT_PRODUCTS_DIR": "BUILT_PRODUCTS_DIR",
|
||||
"DERIVED_SOURCES_DIR": "DERIVED_SOURCES_DIR",
|
||||
"CURRENT_VARIANT": "normal",
|
||||
"PUBLIC_HEADERS_FOLDER_PATH": "/usr/local/include",
|
||||
"LLBUILD_BUILD_ID": "1",
|
||||
]
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
let workingDir = try prepareTempDir()
|
||||
remoteCommitFile = workingDir.appendingPathComponent("arc.rc")
|
||||
_ = workingDir.appendingPathComponent("mpo")
|
||||
config = XCRemoteCacheConfig(remoteCommitFile: remoteCommitFile.path, sourceRoot: workingDir.path)
|
||||
config.recommendedCacheAddress = "http://test.com"
|
||||
}
|
||||
|
||||
func testFailsIfLlBuildIdEnvIsMissing() throws {
|
||||
var envs = Self.SampleEnvs
|
||||
envs.removeValue(forKey: "LLBUILD_BUILD_ID")
|
||||
|
||||
XCTAssertThrowsError(try PrebuildContext(config, env: envs))
|
||||
}
|
||||
|
||||
func testBuildsLockValidFileUrl() throws {
|
||||
let context = try PrebuildContext(config, env: Self.SampleEnvs)
|
||||
|
||||
XCTAssertEqual(context.llbuildIdLockFile, "TARGET_TEMP_DIR/1.lock")
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
@testable import XCRemoteCache
|
||||
import XCTest
|
||||
|
||||
// swiftlint:disable file_length
|
||||
// swiftlint:disable:next type_body_length
|
||||
class PrebuildTests: FileXCTestCase {
|
||||
|
||||
@@ -52,6 +53,13 @@ class PrebuildTests: FileXCTestCase {
|
||||
remoteNetwork = RemoteNetworkClientImpl(network, URLBuilderFake(remoteCacheURL))
|
||||
remapper = DependenciesRemapperFake(baseURL: URL(fileURLWithPath: "/"))
|
||||
metaReader = JsonMetaReader(fileAccessor: FileManager.default)
|
||||
setupNonCachedContext()
|
||||
setupCachedContext()
|
||||
organizer = ArtifactOrganizerFake(artifactRoot: artifactsRoot, unzippedExtension: "unzip")
|
||||
globalCacheSwitcher = InMemoryGlobalCacheSwitcher()
|
||||
}
|
||||
|
||||
private func setupNonCachedContext() {
|
||||
contextNonCached = PrebuildContext(
|
||||
targetTempDir: sampleURL,
|
||||
productsDir: sampleURL,
|
||||
@@ -64,8 +72,12 @@ class PrebuildTests: FileXCTestCase {
|
||||
turnOffRemoteCacheOnFirstTimeout: true,
|
||||
targetName: "",
|
||||
overlayHeadersPath: "",
|
||||
disabled: false
|
||||
disabled: false,
|
||||
llbuildIdLockFile: "/tmp/lock"
|
||||
)
|
||||
}
|
||||
|
||||
private func setupCachedContext() {
|
||||
contextCached = PrebuildContext(
|
||||
targetTempDir: sampleURL,
|
||||
productsDir: sampleURL,
|
||||
@@ -78,10 +90,9 @@ class PrebuildTests: FileXCTestCase {
|
||||
turnOffRemoteCacheOnFirstTimeout: true,
|
||||
targetName: "",
|
||||
overlayHeadersPath: "",
|
||||
disabled: false
|
||||
disabled: false,
|
||||
llbuildIdLockFile: "/tmp/lock"
|
||||
)
|
||||
organizer = ArtifactOrganizerFake(artifactRoot: artifactsRoot, unzippedExtension: "unzip")
|
||||
globalCacheSwitcher = InMemoryGlobalCacheSwitcher()
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
@@ -244,7 +255,8 @@ class PrebuildTests: FileXCTestCase {
|
||||
turnOffRemoteCacheOnFirstTimeout: true,
|
||||
targetName: "",
|
||||
overlayHeadersPath: "",
|
||||
disabled: false
|
||||
disabled: false,
|
||||
llbuildIdLockFile: "/tmp/lock"
|
||||
)
|
||||
|
||||
let prebuild = Prebuild(
|
||||
@@ -276,7 +288,8 @@ class PrebuildTests: FileXCTestCase {
|
||||
turnOffRemoteCacheOnFirstTimeout: true,
|
||||
targetName: "",
|
||||
overlayHeadersPath: "",
|
||||
disabled: false
|
||||
disabled: false,
|
||||
llbuildIdLockFile: "/tmp/lock"
|
||||
)
|
||||
metaContent = try generateMeta(fingerprint: generator.generate(), filekey: "1")
|
||||
let downloadedArtifactPackage = artifactsRoot.appendingPathComponent("1")
|
||||
@@ -340,7 +353,8 @@ class PrebuildTests: FileXCTestCase {
|
||||
turnOffRemoteCacheOnFirstTimeout: false,
|
||||
targetName: "",
|
||||
overlayHeadersPath: "",
|
||||
disabled: false
|
||||
disabled: false,
|
||||
llbuildIdLockFile: "/tmp/lock"
|
||||
)
|
||||
try globalCacheSwitcher.enable(sha: "1")
|
||||
let prebuild = Prebuild(
|
||||
@@ -372,7 +386,8 @@ class PrebuildTests: FileXCTestCase {
|
||||
turnOffRemoteCacheOnFirstTimeout: true,
|
||||
targetName: "",
|
||||
overlayHeadersPath: "",
|
||||
disabled: true
|
||||
disabled: true,
|
||||
llbuildIdLockFile: "/tmp/lock"
|
||||
)
|
||||
|
||||
let prebuild = Prebuild(
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// 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 IntegrateTests: FileXCTestCase {
|
||||
private var config: XCRemoteCacheConfig!
|
||||
private var remoteCommitFile: URL!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
let workingDir = try prepareTempDir()
|
||||
remoteCommitFile = workingDir.appendingPathComponent("arc.rc")
|
||||
_ = workingDir.appendingPathComponent("mpo")
|
||||
config = XCRemoteCacheConfig(remoteCommitFile: remoteCommitFile.path, sourceRoot: workingDir.path)
|
||||
config.recommendedCacheAddress = "http://test.com"
|
||||
}
|
||||
|
||||
|
||||
func tesFallbacksToNoDriverByDefault() throws {
|
||||
let context = try IntegrateContext(
|
||||
input: "project.xcodeproj",
|
||||
config: config,
|
||||
mode: .producer,
|
||||
env: [:],
|
||||
binariesDir: "/binaries",
|
||||
fakeSrcRoot: "/src",
|
||||
outputPath: "/output"
|
||||
)
|
||||
|
||||
XCTAssertEqual(context.buildSettingsAppenderOptions, [.disableSwiftDriverIntegration])
|
||||
XCTAssertEqual(context.binaries.swiftc, "/binaries/xcswiftc")
|
||||
}
|
||||
|
||||
func testEnablesDriverOnRequest() throws {
|
||||
config.enableSwiftDriverIntegration = true
|
||||
let context = try IntegrateContext(
|
||||
input: "project.xcodeproj",
|
||||
config: config,
|
||||
mode: .producer,
|
||||
env: [:],
|
||||
binariesDir: "/binaries",
|
||||
fakeSrcRoot: "/src",
|
||||
outputPath: "/output"
|
||||
)
|
||||
|
||||
XCTAssertEqual(context.buildSettingsAppenderOptions, [])
|
||||
XCTAssertEqual(context.binaries.swiftc, "/binaries/swiftc")
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,8 @@ class XcodeProjBuildSettingsIntegrateAppenderTests: XCTestCase {
|
||||
mode: mode,
|
||||
repoRoot: rootURL,
|
||||
fakeSrcRoot: fakeRootURL,
|
||||
sdksExclude: []
|
||||
sdksExclude: [],
|
||||
options: []
|
||||
)
|
||||
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
|
||||
let resultURL = try XCTUnwrap(result["XCRC_FAKE_SRCROOT"] as? String)
|
||||
@@ -64,7 +65,8 @@ class XcodeProjBuildSettingsIntegrateAppenderTests: XCTestCase {
|
||||
mode: mode,
|
||||
repoRoot: rootURL,
|
||||
fakeSrcRoot: fakeRootURL,
|
||||
sdksExclude: []
|
||||
sdksExclude: [],
|
||||
options: []
|
||||
)
|
||||
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
|
||||
let resultURL: String = try XCTUnwrap(result["XCRC_FAKE_SRCROOT"] as? String)
|
||||
@@ -79,7 +81,8 @@ class XcodeProjBuildSettingsIntegrateAppenderTests: XCTestCase {
|
||||
mode: mode,
|
||||
repoRoot: rootURL,
|
||||
fakeSrcRoot: fakeRootURL,
|
||||
sdksExclude: []
|
||||
sdksExclude: [],
|
||||
options: []
|
||||
)
|
||||
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
|
||||
let ldPlusPlus: String = try XCTUnwrap(result["LDPLUSPLUS"] as? String)
|
||||
@@ -93,7 +96,8 @@ class XcodeProjBuildSettingsIntegrateAppenderTests: XCTestCase {
|
||||
mode: mode,
|
||||
repoRoot: rootURL,
|
||||
fakeSrcRoot: "/",
|
||||
sdksExclude: ["watchOS*"]
|
||||
sdksExclude: ["watchOS*"],
|
||||
options: []
|
||||
)
|
||||
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
|
||||
let ldPlusPlusWatchOS: String = try XCTUnwrap(result["LDPLUSPLUS[sdk=watchOS*]"] as? String)
|
||||
@@ -107,7 +111,8 @@ class XcodeProjBuildSettingsIntegrateAppenderTests: XCTestCase {
|
||||
mode: mode,
|
||||
repoRoot: rootURL,
|
||||
fakeSrcRoot: "/",
|
||||
sdksExclude: ["watchOS*"]
|
||||
sdksExclude: ["watchOS*"],
|
||||
options: []
|
||||
)
|
||||
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
|
||||
let libtoolWatchOS: String = try XCTUnwrap(result["LIBTOOL[sdk=watchOS*]"] as? String)
|
||||
@@ -121,7 +126,8 @@ class XcodeProjBuildSettingsIntegrateAppenderTests: XCTestCase {
|
||||
mode: mode,
|
||||
repoRoot: rootURL,
|
||||
fakeSrcRoot: "/",
|
||||
sdksExclude: ["watchOS*", "watchsimulator*"]
|
||||
sdksExclude: ["watchOS*", "watchsimulator*"],
|
||||
options: []
|
||||
)
|
||||
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
|
||||
let ldPlusPlusWatchOS: String = try XCTUnwrap(result["LDPLUSPLUS[sdk=watchOS*]"] as? String)
|
||||
@@ -137,7 +143,8 @@ class XcodeProjBuildSettingsIntegrateAppenderTests: XCTestCase {
|
||||
mode: mode,
|
||||
repoRoot: rootURL,
|
||||
fakeSrcRoot: "/",
|
||||
sdksExclude: ["watchOS*", "watchsimulator*"]
|
||||
sdksExclude: ["watchOS*", "watchsimulator*"],
|
||||
options: []
|
||||
)
|
||||
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
|
||||
let disabledWatchOS: String = try XCTUnwrap(result["XCRC_DISABLED[sdk=watchOS*]"] as? String)
|
||||
@@ -146,4 +153,34 @@ class XcodeProjBuildSettingsIntegrateAppenderTests: XCTestCase {
|
||||
XCTAssertEqual(disabledWatchOS, "YES")
|
||||
XCTAssertEqual(disabledWatchSimulator, "YES")
|
||||
}
|
||||
|
||||
func testExcludesSwiftFrontendIntegrationForSpecificOption() throws {
|
||||
let mode: Mode = .consumer
|
||||
let appender = XcodeProjBuildSettingsIntegrateAppender(
|
||||
mode: mode,
|
||||
repoRoot: rootURL,
|
||||
fakeSrcRoot: "/",
|
||||
sdksExclude: [],
|
||||
options: [.disableSwiftDriverIntegration]
|
||||
)
|
||||
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
|
||||
let useSwiftIntegrationDriver: String = try XCTUnwrap(result["SWIFT_USE_INTEGRATED_DRIVER"] as? String)
|
||||
|
||||
XCTAssertEqual(useSwiftIntegrationDriver, "NO")
|
||||
}
|
||||
|
||||
func testDoesntExcludesSwiftFrontendIntegrationForEmptyOptions() throws {
|
||||
let mode: Mode = .consumer
|
||||
let appender = XcodeProjBuildSettingsIntegrateAppender(
|
||||
mode: mode,
|
||||
repoRoot: rootURL,
|
||||
fakeSrcRoot: "/",
|
||||
sdksExclude: [],
|
||||
options: []
|
||||
)
|
||||
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
|
||||
let useSwiftIntegrationDriver: String? = result["SWIFT_USE_INTEGRATED_DRIVER"] as? String
|
||||
|
||||
XCTAssertNil(useSwiftIntegrationDriver)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,4 +89,66 @@ class PrepareTests: XCTestCase {
|
||||
|
||||
XCTAssertEqual(result, .preparedFor(sha: .init(sha: "2", age: 0), recommendedCacheAddress: remoteURL))
|
||||
}
|
||||
|
||||
func testFailsForMissingCommonShaWhenConfiguredToGrefullyDisable() throws {
|
||||
git = GitClientFake(shaHistory: [], primaryBranchIndex: 0)
|
||||
config.gracefullyHandleMissingCommonSha = true
|
||||
|
||||
let prepare = Prepare(
|
||||
context: try PrepareContext(config, offline: false),
|
||||
gitClient: git,
|
||||
networkClients: [],
|
||||
ccBuilder: CCWrapperBuilderFake(),
|
||||
fileAccessor: FileManager.default,
|
||||
globalCacheSwitcher: globalCacheSwitcher,
|
||||
cacheInvalidator: CacheInvalidatorFake()
|
||||
)
|
||||
|
||||
let result = try prepare.prepare()
|
||||
|
||||
XCTAssertEqual(result, .failed)
|
||||
}
|
||||
|
||||
func testDisablesGlobalCacheForMissingCommonShaWhenConfiguredToGrefullyDisable() throws {
|
||||
git = GitClientFake(shaHistory: [], primaryBranchIndex: 0)
|
||||
config.gracefullyHandleMissingCommonSha = true
|
||||
try globalCacheSwitcher.enable(sha: "starting_state")
|
||||
|
||||
let prepare = Prepare(
|
||||
context: try PrepareContext(config, offline: false),
|
||||
gitClient: git,
|
||||
networkClients: [],
|
||||
ccBuilder: CCWrapperBuilderFake(),
|
||||
fileAccessor: FileManager.default,
|
||||
globalCacheSwitcher: globalCacheSwitcher,
|
||||
cacheInvalidator: CacheInvalidatorFake()
|
||||
)
|
||||
|
||||
_ = try prepare.prepare()
|
||||
|
||||
XCTAssertEqual(globalCacheSwitcher.state, .disabled)
|
||||
}
|
||||
|
||||
func testThrowsForMissingCommonSha() throws {
|
||||
git = GitClientFake(shaHistory: [], primaryBranchIndex: 0)
|
||||
config.gracefullyHandleMissingCommonSha = false
|
||||
|
||||
let prepare = Prepare(
|
||||
context: try PrepareContext(config, offline: false),
|
||||
gitClient: git,
|
||||
networkClients: [],
|
||||
ccBuilder: CCWrapperBuilderFake(),
|
||||
fileAccessor: FileManager.default,
|
||||
globalCacheSwitcher: globalCacheSwitcher,
|
||||
cacheInvalidator: CacheInvalidatorFake()
|
||||
)
|
||||
|
||||
XCTAssertThrowsError(try prepare.prepare()) { error in
|
||||
switch error {
|
||||
case GitClientError.noCommonShaWithPrimaryRepo: break
|
||||
default:
|
||||
XCTFail("Not expected error")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
// 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
|
||||
|
||||
// swiftlint:disable type_body_length
|
||||
class SwiftFrontendArgInputTests: FileXCTestCase {
|
||||
private var compile: Bool = true
|
||||
private var emitModule: Bool = false
|
||||
private var objcHeaderOutput: String?
|
||||
private var moduleName: String?
|
||||
private var target: String?
|
||||
private var primaryInputPaths: [String] = []
|
||||
private var inputPaths: [String] = []
|
||||
private var outputPaths: [String] = []
|
||||
private var dependenciesPaths: [String] = []
|
||||
private var diagnosticsPaths: [String] = []
|
||||
private var sourceInfoPath: String?
|
||||
private var docPath: String?
|
||||
private var supplementaryOutputFileMap: String?
|
||||
|
||||
private var config: XCRemoteCacheConfig!
|
||||
private var input: SwiftFrontendArgInput!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
let workingDir = try prepareTempDir()
|
||||
let remoteCommitFile = workingDir.appendingPathComponent("arc.rc")
|
||||
config = XCRemoteCacheConfig(remoteCommitFile: remoteCommitFile.path, sourceRoot: workingDir.path)
|
||||
config.recommendedCacheAddress = "http://test.com"
|
||||
|
||||
buildInput()
|
||||
}
|
||||
|
||||
private func buildInput() {
|
||||
input = SwiftFrontendArgInput(
|
||||
compile: compile,
|
||||
emitModule: emitModule,
|
||||
objcHeaderOutput: objcHeaderOutput,
|
||||
moduleName: moduleName,
|
||||
target: target,
|
||||
primaryInputPaths: primaryInputPaths,
|
||||
inputPaths: inputPaths,
|
||||
outputPaths: outputPaths,
|
||||
dependenciesPaths: dependenciesPaths,
|
||||
diagnosticsPaths: diagnosticsPaths,
|
||||
sourceInfoPath: sourceInfoPath,
|
||||
docPath: docPath,
|
||||
supplementaryOutputFileMap: supplementaryOutputFileMap)
|
||||
}
|
||||
|
||||
private func assertGenerationError(_ expectedError: SwiftFrontendArgInputError) {
|
||||
XCTAssertThrowsError(try input.generateSwiftcContext(config: config)) { error in
|
||||
guard let e = error as? SwiftFrontendArgInputError else {
|
||||
XCTFail("Received invalid error \(error). Expected: \(expectedError)")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(e, expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func testFailsForNoStep() throws {
|
||||
compile = false
|
||||
emitModule = false
|
||||
buildInput()
|
||||
|
||||
assertGenerationError(SwiftFrontendArgInputError.bothCompilationAndEmitAction)
|
||||
}
|
||||
|
||||
func testFailsIfNoCompilationFiles() throws {
|
||||
buildInput()
|
||||
|
||||
assertGenerationError(SwiftFrontendArgInputError.noCompilationInputs)
|
||||
}
|
||||
|
||||
func testFailsIfNoTarget() throws {
|
||||
inputPaths = ["/file1"]
|
||||
buildInput()
|
||||
|
||||
assertGenerationError(SwiftFrontendArgInputError.emitMissingTarget)
|
||||
}
|
||||
|
||||
func testFailsIfNoModuleName() throws {
|
||||
inputPaths = ["/file1"]
|
||||
target = "Target"
|
||||
buildInput()
|
||||
|
||||
assertGenerationError(SwiftFrontendArgInputError.emitMissingModuleName)
|
||||
}
|
||||
|
||||
func testFailsIfNoCompileHasNoPrimaryInputs() throws {
|
||||
inputPaths = ["/file1"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
buildInput()
|
||||
|
||||
assertGenerationError(SwiftFrontendArgInputError.noPrimaryFileCompilationInputs)
|
||||
}
|
||||
|
||||
func testFailsIfDependenciesAreMissing() throws {
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
primaryInputPaths = ["/file1", "/file2"]
|
||||
dependenciesPaths = ["/file1.d"]
|
||||
buildInput()
|
||||
|
||||
assertGenerationError(SwiftFrontendArgInputError.dependenciesOuputCountDoesntMatch(expected: 2, parsed: 1))
|
||||
}
|
||||
|
||||
func testDoesntFailForMissingDependenciesIfNoDependencies() throws {
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
primaryInputPaths = ["/file1", "/file2"]
|
||||
dependenciesPaths = []
|
||||
buildInput()
|
||||
|
||||
assertGenerationError(SwiftFrontendArgInputError.outputsOuputCountDoesntMatch(expected: 2, parsed: 0))
|
||||
}
|
||||
|
||||
func testFailsIfDiagnosticsAreMissing() throws {
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
primaryInputPaths = ["/file1", "/file2"]
|
||||
diagnosticsPaths = ["/file1.d"]
|
||||
buildInput()
|
||||
|
||||
assertGenerationError(SwiftFrontendArgInputError.diagnosticsOuputCountDoesntMatch(expected: 2, parsed: 1))
|
||||
}
|
||||
|
||||
func testDoesntFailForMissingDdiagnosticsIfNoDiagnostics() throws {
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
primaryInputPaths = ["/file1", "/file2"]
|
||||
diagnosticsPaths = []
|
||||
buildInput()
|
||||
|
||||
assertGenerationError(SwiftFrontendArgInputError.outputsOuputCountDoesntMatch(expected: 2, parsed: 0))
|
||||
}
|
||||
|
||||
func testFailsIfOutputsAreMissing() throws {
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
primaryInputPaths = ["/file1", "/file2"]
|
||||
outputPaths = ["/file1.o"]
|
||||
buildInput()
|
||||
|
||||
assertGenerationError(SwiftFrontendArgInputError.outputsOuputCountDoesntMatch(expected: 2, parsed: 1))
|
||||
}
|
||||
|
||||
func testSetsCompilationSubsetForCompilation() throws {
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
primaryInputPaths = ["/file1"]
|
||||
outputPaths = ["/file1.o"]
|
||||
buildInput()
|
||||
|
||||
let context = try input.generateSwiftcContext(config: config)
|
||||
|
||||
XCTAssertEqual(context.steps, .init(
|
||||
compileFilesScope: .subset(["/file1"]),
|
||||
emitModule: .none
|
||||
))
|
||||
}
|
||||
|
||||
func testBuildCompilationFilesInputs() throws {
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
primaryInputPaths = ["/file1"]
|
||||
outputPaths = ["/file1.o"]
|
||||
dependenciesPaths = ["/file1.d"]
|
||||
buildInput()
|
||||
|
||||
let context = try input.generateSwiftcContext(config: config)
|
||||
|
||||
XCTAssertEqual(context.inputs, .map([
|
||||
"/file1": SwiftFileCompilationInfo(
|
||||
file: "/file1",
|
||||
dependencies: "/file1.d",
|
||||
object: "/file1.o",
|
||||
swiftDependencies: nil),
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
func testRecognizesArchFromOuputFirstPaths() throws {
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
primaryInputPaths = ["/file1"]
|
||||
outputPaths = ["/TARGET_TEMP_DIR/Object-normal/arm64/file1.o"]
|
||||
dependenciesPaths = ["/file1.d"]
|
||||
buildInput()
|
||||
|
||||
let context = try input.generateSwiftcContext(config: config)
|
||||
|
||||
XCTAssertEqual(context.arch, "arm64")
|
||||
}
|
||||
|
||||
func testPassesExtraParams() throws {
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
primaryInputPaths = ["/file1"]
|
||||
outputPaths = ["/file1.o"]
|
||||
dependenciesPaths = ["/file1.d"]
|
||||
buildInput()
|
||||
|
||||
let context = try input.generateSwiftcContext(config: config)
|
||||
|
||||
XCTAssertEqual(context.moduleName, "Module")
|
||||
XCTAssertEqual(context.target, "Target")
|
||||
XCTAssertEqual(context.compilationFiles, .list(inputPaths))
|
||||
XCTAssertEqual(context.mode, .consumer(commit: .unavailable))
|
||||
}
|
||||
|
||||
func testEmitModuleFailsForMissingOutput() throws {
|
||||
emitModule = true
|
||||
compile = false
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
outputPaths = []
|
||||
buildInput()
|
||||
|
||||
assertGenerationError(SwiftFrontendArgInputError.emitModulOuputCountIsNot1(parsed: 0))
|
||||
}
|
||||
|
||||
func testEmitModuleFailsForMissingObjcHeader() throws {
|
||||
emitModule = true
|
||||
compile = false
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
outputPaths = ["/Module.swiftmodule"]
|
||||
buildInput()
|
||||
|
||||
assertGenerationError(SwiftFrontendArgInputError.emitModuleMissingObjcHeaderPath)
|
||||
}
|
||||
|
||||
func testEmitModuleFailsForExcessiveDiagnostics() throws {
|
||||
emitModule = true
|
||||
compile = false
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
outputPaths = ["/Module.swiftmodule"]
|
||||
objcHeaderOutput = "/file-Swift.h"
|
||||
diagnosticsPaths = ["/file.diag", "/file2.diag"]
|
||||
buildInput()
|
||||
|
||||
assertGenerationError(SwiftFrontendArgInputError.emitModuleDiagnosticsOuputCountIsHigherThan1(parsed: 2))
|
||||
}
|
||||
|
||||
func testEmitModuleFailsForExcessiveDependencies() throws {
|
||||
emitModule = true
|
||||
compile = false
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
outputPaths = ["/Module.swiftmodule"]
|
||||
objcHeaderOutput = "/file-Swift.h"
|
||||
dependenciesPaths = ["/file.d", "/file2.d"]
|
||||
buildInput()
|
||||
|
||||
assertGenerationError(SwiftFrontendArgInputError.emitModuleDependenciesOuputCountIsHigherThan1(parsed: 2))
|
||||
}
|
||||
|
||||
func testEmitModuleSetsStep() throws {
|
||||
emitModule = true
|
||||
compile = false
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
outputPaths = ["/Module.swiftmodule"]
|
||||
objcHeaderOutput = "/file-Swift.h"
|
||||
diagnosticsPaths = ["/file.dia"]
|
||||
dependenciesPaths = ["/file.d"]
|
||||
buildInput()
|
||||
|
||||
let context = try input.generateSwiftcContext(config: config)
|
||||
|
||||
XCTAssertEqual(context.steps, .init(
|
||||
compileFilesScope: .none,
|
||||
emitModule: .init(
|
||||
objcHeaderOutput: "/file-Swift.h",
|
||||
modulePathOutput: "/Module.swiftmodule",
|
||||
dependencies: "/file.d"))
|
||||
)
|
||||
}
|
||||
|
||||
func testEmitModuleSetsAllIntpus() throws {
|
||||
emitModule = true
|
||||
compile = false
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
outputPaths = ["/Module.swiftmodule"]
|
||||
objcHeaderOutput = "/file-Swift.h"
|
||||
buildInput()
|
||||
|
||||
let context = try input.generateSwiftcContext(config: config)
|
||||
|
||||
XCTAssertEqual(context.compilationFiles, .list(inputPaths))
|
||||
}
|
||||
|
||||
func testEmitModuleRecognizesArchFromObjCHeader() throws {
|
||||
emitModule = true
|
||||
compile = false
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
outputPaths = ["file.swiftmodule"]
|
||||
objcHeaderOutput = "/TARGET_TEMP_DIR/Object-normal/arm64/file-Swift.h"
|
||||
buildInput()
|
||||
|
||||
let context = try input.generateSwiftcContext(config: config)
|
||||
|
||||
XCTAssertEqual(context.arch, "arm64")
|
||||
}
|
||||
|
||||
func testEmitModulePassesExtraParams() throws {
|
||||
emitModule = true
|
||||
compile = false
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
outputPaths = ["/Module.swiftmodule"]
|
||||
objcHeaderOutput = "/file-Swift.h"
|
||||
buildInput()
|
||||
|
||||
let context = try input.generateSwiftcContext(config: config)
|
||||
|
||||
XCTAssertEqual(context.moduleName, "Module")
|
||||
XCTAssertEqual(context.target, "Target")
|
||||
XCTAssertEqual(context.compilationFiles, .list(inputPaths))
|
||||
XCTAssertEqual(context.mode, .consumer(commit: .unavailable))
|
||||
}
|
||||
}
|
||||
// swiftlint:enable type_body_length
|
||||
@@ -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.
|
||||
|
||||
@testable import XCRemoteCache
|
||||
import XCTest
|
||||
|
||||
|
||||
class SwiftFrontendContextTests: XCTestCase {
|
||||
|
||||
private var swiftcContext: SwiftcContext!
|
||||
private let targetTempDir: URL = "/temp"
|
||||
|
||||
override func setUp() async throws {
|
||||
swiftcContext = try SwiftcContext(
|
||||
config: .init(sourceRoot: ""),
|
||||
input: .init(
|
||||
objcHeaderOutput: "",
|
||||
moduleName: "",
|
||||
modulePathOutput: "\(targetTempDir.path)/Objects-normal/$ARCH/some.file",
|
||||
filemap: "",
|
||||
target: "",
|
||||
fileList: ""
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testBuildsBuildLockInTargetTemp() throws {
|
||||
let env = [
|
||||
"LLBUILD_BUILD_ID": "1",
|
||||
]
|
||||
|
||||
let frontendContext = try SwiftFrontendContext(swiftcContext, env: env)
|
||||
|
||||
XCTAssertEqual(frontendContext.invocationLockFile, targetTempDir.appendingPathComponent("1.lock"))
|
||||
}
|
||||
|
||||
func testInitializerFailsIfLlBuildIdIsMissingInEnv() throws {
|
||||
XCTAssertThrowsError(try SwiftFrontendContext(swiftcContext, env: [:]))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
// 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
|
||||
|
||||
|
||||
final class SwiftFrontendOrchestratorTests: FileXCTestCase {
|
||||
private let prohibitedAccessor = DisallowedExclusiveFileAccessor()
|
||||
private var nonEmptyFile: URL!
|
||||
private var emptyFile: URL!
|
||||
private let maxLocking: TimeInterval = 10
|
||||
|
||||
override func setUp() async throws {
|
||||
nonEmptyFile = try prepareTempDir().appendingPathComponent("lock.lock")
|
||||
try fileManager.write(toPath: nonEmptyFile.path, contents: "done".data(using: .utf8))
|
||||
emptyFile = try prepareTempDir().appendingPathComponent("lock_empty.lock")
|
||||
try fileManager.write(toPath: emptyFile.path, contents: .init())
|
||||
}
|
||||
|
||||
func testRunsCriticalSectionImmediatelyForProducer() throws {
|
||||
let orchestrator = CommonSwiftFrontendOrchestrator(
|
||||
mode: .producer,
|
||||
action: .compile,
|
||||
lockAccessor: prohibitedAccessor,
|
||||
maxLockTimeout: maxLocking
|
||||
)
|
||||
|
||||
var invoked = false
|
||||
try orchestrator.run {
|
||||
invoked = true
|
||||
}
|
||||
XCTAssertTrue(invoked)
|
||||
}
|
||||
|
||||
func testRunsCriticalSectionImmediatelyForDisabledConsumer() throws {
|
||||
let orchestrator = CommonSwiftFrontendOrchestrator(
|
||||
mode: .consumer(commit: .unavailable),
|
||||
action: .compile,
|
||||
lockAccessor: prohibitedAccessor,
|
||||
maxLockTimeout: maxLocking
|
||||
)
|
||||
|
||||
var invoked = false
|
||||
try orchestrator.run {
|
||||
invoked = true
|
||||
}
|
||||
XCTAssertTrue(invoked)
|
||||
}
|
||||
|
||||
func testRunsEmitModuleLogicInExclusiveLock() throws {
|
||||
let lock = FakeExclusiveFileAccessor()
|
||||
let orchestrator = CommonSwiftFrontendOrchestrator(
|
||||
mode: .consumer(commit: .available(commit: "")),
|
||||
action: .emitModule,
|
||||
lockAccessor: lock,
|
||||
maxLockTimeout: maxLocking
|
||||
)
|
||||
|
||||
var invoked = false
|
||||
try orchestrator.run {
|
||||
XCTAssertTrue(lock.isLocked)
|
||||
invoked = true
|
||||
}
|
||||
XCTAssertTrue(invoked)
|
||||
}
|
||||
|
||||
func testCompilationInvokesCriticalSectionOnlyForNonEmptyLockFile() throws {
|
||||
let lock = FakeExclusiveFileAccessor(pattern: [.empty, .nonEmptyForRead(nonEmptyFile)])
|
||||
let orchestrator = CommonSwiftFrontendOrchestrator(
|
||||
mode: .consumer(commit: .available(commit: "")),
|
||||
action: .compile,
|
||||
lockAccessor: lock,
|
||||
maxLockTimeout: maxLocking
|
||||
)
|
||||
|
||||
var invoked = false
|
||||
try orchestrator.run {
|
||||
XCTAssertTrue(lock.isLocked)
|
||||
invoked = true
|
||||
}
|
||||
XCTAssertTrue(invoked)
|
||||
}
|
||||
|
||||
func testExecutesActionWithoutLockIfLockingFileIsEmptyForALongTime() throws {
|
||||
let lock = FakeExclusiveFileAccessor(pattern: [])
|
||||
let orchestrator = CommonSwiftFrontendOrchestrator(
|
||||
mode: .consumer(commit: .available(commit: "")),
|
||||
action: .compile,
|
||||
lockAccessor: lock,
|
||||
maxLockTimeout: 0
|
||||
)
|
||||
|
||||
var invoked = false
|
||||
try orchestrator.run {
|
||||
XCTAssertFalse(lock.isLocked)
|
||||
invoked = true
|
||||
}
|
||||
XCTAssertTrue(invoked)
|
||||
}
|
||||
|
||||
func testExecutesCriticalSectionAfterWriting() throws {
|
||||
let lock = FakeExclusiveFileAccessor(pattern: [.nonEmptyForWrite(emptyFile)])
|
||||
let orchestrator = CommonSwiftFrontendOrchestrator(
|
||||
mode: .consumer(commit: .available(commit: "")),
|
||||
action: .emitModule,
|
||||
lockAccessor: lock,
|
||||
maxLockTimeout: 0
|
||||
)
|
||||
|
||||
var invoked = false
|
||||
try orchestrator.run {
|
||||
XCTAssertEqual(fileManager.contents(atPath: emptyFile.path), "done".data(using: .utf8))
|
||||
invoked = true
|
||||
}
|
||||
XCTAssertTrue(invoked)
|
||||
}
|
||||
}
|
||||
@@ -25,20 +25,25 @@ class SwiftcContextTests: FileXCTestCase {
|
||||
private var config: XCRemoteCacheConfig!
|
||||
private var input: SwiftcArgInput!
|
||||
private var remoteCommitFile: URL!
|
||||
private var modulePathOutput: URL!
|
||||
private var fileMapUrl: URL!
|
||||
private var fileListUrl: URL!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
let workingDir = try prepareTempDir()
|
||||
remoteCommitFile = workingDir.appendingPathComponent("arc.rc")
|
||||
let modulePathOutput = workingDir.appendingPathComponent("mpo")
|
||||
modulePathOutput = workingDir.appendingPathComponent("mpo")
|
||||
fileMapUrl = workingDir.appendingPathComponent("filemap")
|
||||
fileListUrl = workingDir.appendingPathComponent("filelist")
|
||||
config = XCRemoteCacheConfig(remoteCommitFile: remoteCommitFile.path, sourceRoot: workingDir.path)
|
||||
input = SwiftcArgInput(
|
||||
objcHeaderOutput: "Target-Swift.h",
|
||||
moduleName: "",
|
||||
moduleName: "Module",
|
||||
modulePathOutput: modulePathOutput.path,
|
||||
filemap: "",
|
||||
filemap: fileMapUrl.path,
|
||||
target: "",
|
||||
fileList: ""
|
||||
fileList: fileListUrl.path
|
||||
)
|
||||
try fileManager.write(toPath: remoteCommitFile.path, contents: "123".data(using: .utf8))
|
||||
}
|
||||
@@ -77,4 +82,29 @@ class SwiftcContextTests: FileXCTestCase {
|
||||
|
||||
XCTAssertEqual(context.mode, .producer)
|
||||
}
|
||||
|
||||
func testStepsContainEmitingModuleAndAllCompilationScope() throws {
|
||||
let context = try SwiftcContext(config: config, input: input)
|
||||
|
||||
XCTAssertEqual(context.steps, .init(
|
||||
compileFilesScope: .all,
|
||||
emitModule: .init(
|
||||
objcHeaderOutput: "Target-Swift.h",
|
||||
modulePathOutput: modulePathOutput,
|
||||
dependencies: nil)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testReadsInputsFromFileMap() throws {
|
||||
let context = try SwiftcContext(config: config, input: input)
|
||||
|
||||
XCTAssertEqual(context.inputs, .fileMap(fileMapUrl.path))
|
||||
}
|
||||
|
||||
func testReadsCompilationFilesFromFileList() throws {
|
||||
let context = try SwiftcContext(config: config, input: input)
|
||||
|
||||
XCTAssertEqual(context.compilationFiles, .fileList(fileListUrl.path))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,19 +34,21 @@ class SwiftcFilemapInputEditorTests: FileXCTestCase {
|
||||
)
|
||||
private let sampleInfoContentData = #"{"":{"swift-dependencies":"/"}}"#.data(using: .utf8)!
|
||||
private var inputFile: URL!
|
||||
private var editor: SwiftcFilemapInputEditor!
|
||||
private var editorJson: SwiftcFilemapInputEditor!
|
||||
private var editorYaml: SwiftcFilemapInputEditor!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
try prepareTempDir()
|
||||
inputFile = workingDirectory!.appendingPathComponent("swift.json")
|
||||
editor = SwiftcFilemapInputEditor(inputFile, fileManager: fileManager)
|
||||
editorJson = SwiftcFilemapInputEditor(inputFile, fileFormat: .json, fileManager: fileManager)
|
||||
editorYaml = SwiftcFilemapInputEditor(inputFile, fileFormat: .yaml, fileManager: fileManager)
|
||||
}
|
||||
|
||||
func testReading() throws {
|
||||
try fileManager.spt_writeToFile(atPath: inputFile.path, contents: sampleInfoContentData)
|
||||
|
||||
let readInfo = try editor.read()
|
||||
let readInfo = try editorJson.read()
|
||||
|
||||
XCTAssertEqual(readInfo, sampleInfo)
|
||||
}
|
||||
@@ -80,13 +82,13 @@ class SwiftcFilemapInputEditorTests: FileXCTestCase {
|
||||
])
|
||||
try fileManager.spt_writeToFile(atPath: inputFile.path, contents: infoContentData)
|
||||
|
||||
let readInfo = try editor.read()
|
||||
let readInfo = try editorJson.read()
|
||||
|
||||
XCTAssertEqual(readInfo, expectedInfo)
|
||||
}
|
||||
|
||||
func testWritingSavesContent() throws {
|
||||
try editor.write(sampleInfo)
|
||||
try editorJson.write(sampleInfo)
|
||||
|
||||
let savedContent = try Data(contentsOf: inputFile)
|
||||
let content = try JSONSerialization.jsonObject(with: savedContent, options: []) as? [String: Any]
|
||||
@@ -108,7 +110,7 @@ class SwiftcFilemapInputEditorTests: FileXCTestCase {
|
||||
),
|
||||
])
|
||||
|
||||
try editor.write(extendedInfo)
|
||||
try editorJson.write(extendedInfo)
|
||||
|
||||
let savedContent = try Data(contentsOf: inputFile)
|
||||
let content = try JSONSerialization.jsonObject(with: savedContent, options: []) as? [String: Any]
|
||||
@@ -119,12 +121,50 @@ class SwiftcFilemapInputEditorTests: FileXCTestCase {
|
||||
func testModifyingFileCompilationInfo() throws {
|
||||
try fileManager.spt_writeToFile(atPath: inputFile.path, contents: sampleInfoContentData)
|
||||
|
||||
let originalInfo = try editor.read()
|
||||
let originalInfo = try editorJson.read()
|
||||
var modifiedInfo = originalInfo
|
||||
modifiedInfo.files = [file]
|
||||
try editor.write(modifiedInfo)
|
||||
let finalInfo = try editor.read()
|
||||
try editorJson.write(modifiedInfo)
|
||||
let finalInfo = try editorJson.read()
|
||||
|
||||
XCTAssertEqual(finalInfo, modifiedInfo)
|
||||
}
|
||||
|
||||
func testReadingSupplementaryInfoWithOptionalProperties() throws {
|
||||
let infoContentData = #"""
|
||||
"/file1.swift":
|
||||
swift-dependencies: "/file1.swiftdeps"
|
||||
dependencies: "/file1.d"
|
||||
"/file2.swift":
|
||||
dependencies: "/file2.d"
|
||||
object: "/file2.o"
|
||||
swift-dependencies: "/file2.swiftdeps"
|
||||
"""#.data(using: .utf8)!
|
||||
let expectedInfo = SwiftCompilationInfo(
|
||||
info: SwiftModuleCompilationInfo(
|
||||
dependencies: nil,
|
||||
swiftDependencies: nil
|
||||
),
|
||||
files: [
|
||||
SwiftFileCompilationInfo(
|
||||
file: "/file1.swift",
|
||||
dependencies: "/file1.d",
|
||||
object: nil,
|
||||
swiftDependencies: "/file1.swiftdeps"
|
||||
),
|
||||
SwiftFileCompilationInfo(
|
||||
file: "/file2.swift",
|
||||
dependencies: "/file2.d",
|
||||
object: "/file2.o",
|
||||
swiftDependencies: "/file2.swiftdeps"
|
||||
),
|
||||
])
|
||||
try fileManager.spt_writeToFile(atPath: inputFile.path, contents: infoContentData)
|
||||
|
||||
let readInfo = try editorYaml.read()
|
||||
|
||||
// `files` order doesn't match
|
||||
XCTAssertEqual(readInfo.info, expectedInfo.info)
|
||||
XCTAssertEqual(Set(readInfo.files), Set(expectedInfo.files))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,4 +231,45 @@ class SwiftcOrchestratorTests: XCTestCase {
|
||||
XCTAssertEqual(artifactBuilder.addedObjCHeaders, [:])
|
||||
}
|
||||
|
||||
|
||||
func testNotSetObjCHeaderIsNotCreated() throws {
|
||||
let swiftc = SwiftcMock(mockingResult: .success)
|
||||
let orchestrator = SwiftcOrchestrator(
|
||||
mode: .producer,
|
||||
swiftc: swiftc,
|
||||
swiftcCommand: "",
|
||||
objcHeaderOutput: nil,
|
||||
moduleOutput: moduleOutputURL,
|
||||
arch: "arch",
|
||||
artifactBuilder: artifactBuilder,
|
||||
producerFallbackCommandProcessors: [],
|
||||
invocationStorage: invocationStorage,
|
||||
shellOut: shellOutSpy
|
||||
)
|
||||
|
||||
try orchestrator.run()
|
||||
|
||||
XCTAssertEqual(artifactBuilder.addedObjCHeaders, [:])
|
||||
}
|
||||
|
||||
func testNotSetModuleOutputIsNotCreated() throws {
|
||||
let swiftc = SwiftcMock(mockingResult: .success)
|
||||
let orchestrator = SwiftcOrchestrator(
|
||||
mode: .producer,
|
||||
swiftc: swiftc,
|
||||
swiftcCommand: "",
|
||||
objcHeaderOutput: objcHeaderURL,
|
||||
moduleOutput: nil,
|
||||
arch: "arch",
|
||||
artifactBuilder: artifactBuilder,
|
||||
producerFallbackCommandProcessors: [],
|
||||
invocationStorage: invocationStorage,
|
||||
shellOut: shellOutSpy
|
||||
)
|
||||
|
||||
try orchestrator.run()
|
||||
|
||||
XCTAssertEqual(artifactBuilder.addedModuleDefinitions, [:])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ class SwiftcTests: FileXCTestCase {
|
||||
private var workingDir: URL!
|
||||
private var remoteCommitLocation: URL!
|
||||
private let sampleRemoteCommit = "bdb321"
|
||||
private var allowedInputDeterminer = FilenameBasedAllowedInputDeterminer([])
|
||||
|
||||
|
||||
override func setUpWithError() throws {
|
||||
@@ -62,7 +63,7 @@ class SwiftcTests: FileXCTestCase {
|
||||
|
||||
input = SwiftcArgInput(
|
||||
objcHeaderOutput: "Target-Swift.h",
|
||||
moduleName: "",
|
||||
moduleName: "Target",
|
||||
modulePathOutput: modulePathOutput.path,
|
||||
filemap: "",
|
||||
target: "",
|
||||
@@ -93,7 +94,8 @@ class SwiftcTests: FileXCTestCase {
|
||||
fileManager: FileManager.default,
|
||||
dependenciesWriterFactory: dependenciesWriterFactory,
|
||||
touchFactory: touchFactory,
|
||||
plugins: []
|
||||
plugins: [],
|
||||
allowedInputDeterminer: allowedInputDeterminer
|
||||
)
|
||||
|
||||
let compilation = try swiftc.mockCompilation()
|
||||
@@ -115,7 +117,8 @@ class SwiftcTests: FileXCTestCase {
|
||||
fileManager: FileManager.default,
|
||||
dependenciesWriterFactory: dependenciesWriterFactory,
|
||||
touchFactory: touchFactory,
|
||||
plugins: []
|
||||
plugins: [],
|
||||
allowedInputDeterminer: allowedInputDeterminer
|
||||
)
|
||||
|
||||
let compilation = try swiftc.mockCompilation()
|
||||
@@ -137,7 +140,8 @@ class SwiftcTests: FileXCTestCase {
|
||||
fileManager: FileManager.default,
|
||||
dependenciesWriterFactory: dependenciesWriterFactory,
|
||||
touchFactory: touchFactory,
|
||||
plugins: []
|
||||
plugins: [],
|
||||
allowedInputDeterminer: allowedInputDeterminer
|
||||
)
|
||||
|
||||
_ = try swiftc.mockCompilation()
|
||||
@@ -161,7 +165,8 @@ class SwiftcTests: FileXCTestCase {
|
||||
fileManager: FileManager.default,
|
||||
dependenciesWriterFactory: dependenciesWriterFactory,
|
||||
touchFactory: touchFactory,
|
||||
plugins: []
|
||||
plugins: [],
|
||||
allowedInputDeterminer: allowedInputDeterminer
|
||||
)
|
||||
|
||||
_ = try swiftc.mockCompilation()
|
||||
@@ -187,7 +192,8 @@ class SwiftcTests: FileXCTestCase {
|
||||
fileManager: FileManager.default,
|
||||
dependenciesWriterFactory: dependenciesWriterFactory,
|
||||
touchFactory: touchFactory,
|
||||
plugins: []
|
||||
plugins: [],
|
||||
allowedInputDeterminer: allowedInputDeterminer
|
||||
)
|
||||
|
||||
_ = try swiftc.mockCompilation()
|
||||
@@ -216,7 +222,8 @@ class SwiftcTests: FileXCTestCase {
|
||||
fileManager: FileManager.default,
|
||||
dependenciesWriterFactory: dependenciesWriterFactory,
|
||||
touchFactory: touchFactory,
|
||||
plugins: []
|
||||
plugins: [],
|
||||
allowedInputDeterminer: allowedInputDeterminer
|
||||
)
|
||||
|
||||
_ = try swiftc.mockCompilation()
|
||||
@@ -226,6 +233,30 @@ class SwiftcTests: FileXCTestCase {
|
||||
XCTAssertEqual(writerSpy.wroteSkipForSha, sampleRemoteCommit)
|
||||
}
|
||||
|
||||
func testAllowsNotAllowedInputFileThatAreAllowedByDeterminer() throws {
|
||||
inputFileListReader = ListReaderFake(files: [URL(fileURLWithPath: "specialFile.swift")])
|
||||
allowedInputDeterminer = FilenameBasedAllowedInputDeterminer(["specialFile.swift"])
|
||||
let swiftc = Swiftc(
|
||||
inputFileListReader: inputFileListReader,
|
||||
markerReader: markerReader,
|
||||
allowedFilesListScanner: allowedFilesListScanner,
|
||||
artifactOrganizer: artifactOrganizer,
|
||||
inputReader: swiftcInputReader,
|
||||
context: context,
|
||||
markerWriter: markerWriter,
|
||||
productsGenerator: productsGenerator,
|
||||
fileManager: FileManager.default,
|
||||
dependenciesWriterFactory: dependenciesWriterFactory,
|
||||
touchFactory: touchFactory,
|
||||
plugins: [],
|
||||
allowedInputDeterminer: allowedInputDeterminer
|
||||
)
|
||||
|
||||
let result = try swiftc.mockCompilation()
|
||||
|
||||
XCTAssertEqual(result, .success)
|
||||
}
|
||||
|
||||
func testRCTouchesOutputFile() throws {
|
||||
let compilationURL = URL(fileURLWithPath: "old.swift")
|
||||
inputFileListReader = ListReaderFake(files: [compilationURL])
|
||||
@@ -265,7 +296,8 @@ class SwiftcTests: FileXCTestCase {
|
||||
fileManager: FileManager.default,
|
||||
dependenciesWriterFactory: dependenciesWriterFactory,
|
||||
touchFactory: touchFactory,
|
||||
plugins: []
|
||||
plugins: [],
|
||||
allowedInputDeterminer: allowedInputDeterminer
|
||||
)
|
||||
|
||||
_ = try swiftc.mockCompilation()
|
||||
@@ -276,7 +308,7 @@ class SwiftcTests: FileXCTestCase {
|
||||
|
||||
func testCompilationUsesArchSpecificSwiftmoduleFiles() throws {
|
||||
let artifactRoot = URL(fileURLWithPath: "/cachedArtifact")
|
||||
let artifactObjCHeader = URL(fileURLWithPath: "/cachedArtifact/include/archTest/Target-Swift.h")
|
||||
let artifactObjCHeader = URL(fileURLWithPath: "/cachedArtifact/include/archTest/Target/Target-Swift.h")
|
||||
let artifactSwiftmodule = URL(fileURLWithPath: "/cachedArtifact/swiftmodule/archTest/Target.swiftmodule")
|
||||
let artifactSwiftdoc = URL(fileURLWithPath: "/cachedArtifact/swiftmodule/archTest/Target.swiftdoc")
|
||||
let artifactSwiftSourceInfo = URL(
|
||||
@@ -305,7 +337,8 @@ class SwiftcTests: FileXCTestCase {
|
||||
fileManager: FileManager.default,
|
||||
dependenciesWriterFactory: dependenciesWriterFactory,
|
||||
touchFactory: touchFactory,
|
||||
plugins: []
|
||||
plugins: [],
|
||||
allowedInputDeterminer: allowedInputDeterminer
|
||||
)
|
||||
|
||||
_ = try swiftc.mockCompilation()
|
||||
@@ -340,7 +373,8 @@ class SwiftcTests: FileXCTestCase {
|
||||
fileManager: FileManager.default,
|
||||
dependenciesWriterFactory: dependenciesWriterFactory,
|
||||
touchFactory: touchFactory,
|
||||
plugins: [plugin]
|
||||
plugins: [plugin],
|
||||
allowedInputDeterminer: allowedInputDeterminer
|
||||
)
|
||||
|
||||
_ = try swiftc.mockCompilation()
|
||||
@@ -382,7 +416,8 @@ class SwiftcTests: FileXCTestCase {
|
||||
fileManager: FileManager.default,
|
||||
dependenciesWriterFactory: FileDependenciesWriter.init,
|
||||
touchFactory: touchFactory,
|
||||
plugins: []
|
||||
plugins: [],
|
||||
allowedInputDeterminer: allowedInputDeterminer
|
||||
)
|
||||
|
||||
_ = try swiftc.mockCompilation()
|
||||
@@ -399,6 +434,57 @@ class SwiftcTests: FileXCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
func testGeneratesDFilesAndIndividualFilesWithAdditionallyAllowedFiles() throws {
|
||||
let outputFilesDir = workingDir.appendingPathComponent("outputFiles")
|
||||
try fileManager.spt_createEmptyDir(outputFilesDir)
|
||||
let moduleDFile = outputFilesDir.appendingPathComponent("master.d")
|
||||
let fileDFile = outputFilesDir.appendingPathComponent("magicalFile.d")
|
||||
let input = SwiftCompilationInfo(
|
||||
info: SwiftModuleCompilationInfo(
|
||||
dependencies: moduleDFile,
|
||||
swiftDependencies: outputFilesDir.appendingPathComponent("master.swiftdeps")
|
||||
),
|
||||
files: [
|
||||
SwiftFileCompilationInfo(
|
||||
file: "/magicalFile.swift",
|
||||
dependencies: fileDFile,
|
||||
object: outputFilesDir.appendingPathComponent("maficalFile.o"),
|
||||
swiftDependencies: nil
|
||||
),
|
||||
]
|
||||
)
|
||||
inputFileListReader = ListReaderFake(files: ["/magicalFile.swift"])
|
||||
swiftcInputReader = SwiftcInputReaderStub(info: input)
|
||||
markerReader = ListReaderFake(files: [])
|
||||
allowedInputDeterminer = FilenameBasedAllowedInputDeterminer(["magicalFile.swift"])
|
||||
let swiftc = Swiftc(
|
||||
inputFileListReader: inputFileListReader,
|
||||
markerReader: markerReader,
|
||||
allowedFilesListScanner: allowedFilesListScanner,
|
||||
artifactOrganizer: artifactOrganizer,
|
||||
inputReader: swiftcInputReader,
|
||||
context: context,
|
||||
markerWriter: markerWriter,
|
||||
productsGenerator: productsGenerator,
|
||||
fileManager: FileManager.default,
|
||||
dependenciesWriterFactory: FileDependenciesWriter.init,
|
||||
touchFactory: touchFactory,
|
||||
plugins: [],
|
||||
allowedInputDeterminer: allowedInputDeterminer
|
||||
)
|
||||
|
||||
_ = try swiftc.mockCompilation()
|
||||
|
||||
XCTAssertEqual(
|
||||
try FileDependenciesReader(moduleDFile, accessor: .default).readFilesAndDependencies(),
|
||||
["dependencies": ["/magicalFile.swift"]]
|
||||
)
|
||||
XCTAssertEqual(
|
||||
try FileDependenciesReader(fileDFile, accessor: .default).readFilesAndDependencies(),
|
||||
["dependencies": ["/magicalFile.swift"]]
|
||||
)
|
||||
}
|
||||
|
||||
func testSkipsGeneratingDFilesWhenNotProvidedInCompilationInfo() throws {
|
||||
let outputFilesDir = workingDir.appendingPathComponent("outputFiles")
|
||||
try fileManager.spt_createEmptyDir(outputFilesDir)
|
||||
@@ -429,7 +515,8 @@ class SwiftcTests: FileXCTestCase {
|
||||
fileManager: FileManager.default,
|
||||
dependenciesWriterFactory: FileDependenciesWriter.init,
|
||||
touchFactory: touchFactory,
|
||||
plugins: []
|
||||
plugins: [],
|
||||
allowedInputDeterminer: allowedInputDeterminer
|
||||
)
|
||||
|
||||
XCTAssertNoThrow(try swiftc.mockCompilation())
|
||||
@@ -465,7 +552,8 @@ class SwiftcTests: FileXCTestCase {
|
||||
fileManager: FileManager.default,
|
||||
dependenciesWriterFactory: FileDependenciesWriter.init,
|
||||
touchFactory: touchFactory,
|
||||
plugins: []
|
||||
plugins: [],
|
||||
allowedInputDeterminer: allowedInputDeterminer
|
||||
)
|
||||
|
||||
XCTAssertNoThrow(try swiftc.mockCompilation())
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// 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 AssetsFileDependenciesReaderTests: FileXCTestCase {
|
||||
private static let resourcesSubdirectory = "TestData/Dependencies/AssetsFileDependenciesReaderTests"
|
||||
private let dirAccessorFake = DirAccessorFake()
|
||||
|
||||
private func pathForTestData(name: String) throws -> URL {
|
||||
return try XCTUnwrap(Bundle.module.url(
|
||||
forResource: name,
|
||||
withExtension: "",
|
||||
subdirectory: AssetsFileDependenciesReaderTests.resourcesSubdirectory
|
||||
))
|
||||
}
|
||||
|
||||
func testParsingSampleFile() throws {
|
||||
let file = try pathForTestData(name: "assetcatalog_dependencies_sample")
|
||||
let fileData = try Data(contentsOf: file)
|
||||
let xcassetsPath: URL = "/StandaloneApp/Assets.xcassets"
|
||||
let contentsJson = xcassetsPath.appendingPathComponent("Contents.json")
|
||||
|
||||
try dirAccessorFake.write(toPath: file.path, contents: fileData)
|
||||
try dirAccessorFake.write(toPath: contentsJson.path, contents: Data())
|
||||
|
||||
let reader = AssetsFileDependenciesReader(file, dirAccessor: dirAccessorFake)
|
||||
|
||||
let dependencies = try reader.findDependencies()
|
||||
|
||||
XCTAssertEqual(dependencies, [contentsJson.path])
|
||||
}
|
||||
|
||||
func testThrowsWhenFileIsMissing() throws {
|
||||
let file: URL = "/nonExistingFile"
|
||||
|
||||
let reader = AssetsFileDependenciesReader(file, dirAccessor: dirAccessorFake)
|
||||
|
||||
XCTAssertThrowsError(try reader.findDependencies()) { error in
|
||||
guard case DependenciesReaderError.readingError = error else {
|
||||
XCTFail("Invalid error \(error)")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testReturnsEmptyArrayIsFileIsMalformed() throws {
|
||||
let file: URL = "/nonExistingFile"
|
||||
try dirAccessorFake.write(toPath: file.path, contents: Data())
|
||||
let reader = AssetsFileDependenciesReader(file, dirAccessor: dirAccessorFake)
|
||||
|
||||
let dependencies = try reader.findDependencies()
|
||||
|
||||
XCTAssertEqual(dependencies, [])
|
||||
}
|
||||
}
|
||||
@@ -20,8 +20,14 @@
|
||||
@testable import XCRemoteCache
|
||||
import XCTest
|
||||
|
||||
class PhaseCacheModeControllerTests: XCTestCase {
|
||||
class PhaseCacheModeControllerTests: FileXCTestCase {
|
||||
private var rootDir: URL!
|
||||
private let sampleURL = URL(fileURLWithPath: "")
|
||||
private var simpleController: PhaseCacheModeController!
|
||||
|
||||
override func setUp() async throws {
|
||||
rootDir = try prepareTempDir()
|
||||
}
|
||||
|
||||
func testDisablesForSpecifiedSha() {
|
||||
let dependenciesReader = DependenciesReaderFake(dependencies: ["skipForSha": ["dbd123"]])
|
||||
@@ -34,6 +40,7 @@ class PhaseCacheModeControllerTests: XCTestCase {
|
||||
dependenciesWriter: FileDependenciesWriter.init,
|
||||
dependenciesReader: { _, _ in dependenciesReader },
|
||||
markerWriter: FileMarkerWriter.init,
|
||||
llbuildLockFile: "/file",
|
||||
fileManager: FileManager.default
|
||||
)
|
||||
|
||||
@@ -51,6 +58,7 @@ class PhaseCacheModeControllerTests: XCTestCase {
|
||||
dependenciesWriter: FileDependenciesWriter.init,
|
||||
dependenciesReader: { _, _ in dependenciesReader },
|
||||
markerWriter: FileMarkerWriter.init,
|
||||
llbuildLockFile: "/tmp/lock",
|
||||
fileManager: FileManager.default
|
||||
)
|
||||
|
||||
@@ -68,6 +76,7 @@ class PhaseCacheModeControllerTests: XCTestCase {
|
||||
dependenciesWriter: FileDependenciesWriter.init,
|
||||
dependenciesReader: { _, _ in dependenciesReader },
|
||||
markerWriter: FileMarkerWriter.init,
|
||||
llbuildLockFile: "/tmp/lock",
|
||||
fileManager: FileManager.default
|
||||
)
|
||||
|
||||
@@ -85,6 +94,7 @@ class PhaseCacheModeControllerTests: XCTestCase {
|
||||
dependenciesWriter: { _, _ in dependenciesWriter },
|
||||
dependenciesReader: { _, _ in DependenciesReaderFake(dependencies: [:]) },
|
||||
markerWriter: { _, _ in MarkerWriterSpy() },
|
||||
llbuildLockFile: "/tmp/lock",
|
||||
fileManager: FileManager.default
|
||||
)
|
||||
|
||||
@@ -105,6 +115,7 @@ class PhaseCacheModeControllerTests: XCTestCase {
|
||||
dependenciesWriter: { _, _ in dependenciesWriter },
|
||||
dependenciesReader: { _, _ in DependenciesReaderFake(dependencies: [:]) },
|
||||
markerWriter: { _, _ in MarkerWriterSpy() },
|
||||
llbuildLockFile: "/tmp/lock",
|
||||
fileManager: FileManager.default
|
||||
)
|
||||
|
||||
@@ -125,6 +136,7 @@ class PhaseCacheModeControllerTests: XCTestCase {
|
||||
dependenciesWriter: { _, _ in DependenciesWriterSpy() },
|
||||
dependenciesReader: { _, _ in DependenciesReaderFake(dependencies: [:]) },
|
||||
markerWriter: { _, _ in markerWriter },
|
||||
llbuildLockFile: "/tmp/lock",
|
||||
fileManager: FileManager.default
|
||||
)
|
||||
|
||||
@@ -142,6 +154,7 @@ class PhaseCacheModeControllerTests: XCTestCase {
|
||||
dependenciesWriter: { _, _ in DependenciesWriterSpy() },
|
||||
dependenciesReader: { _, _ in DependenciesReaderFake(dependencies: [:]) },
|
||||
markerWriter: { _, _ in MarkerWriterSpy() },
|
||||
llbuildLockFile: "/tmp/lock",
|
||||
fileManager: FileManager.default
|
||||
)
|
||||
|
||||
@@ -163,6 +176,7 @@ class PhaseCacheModeControllerTests: XCTestCase {
|
||||
dependenciesWriter: { _, _ in dependenciesWriter },
|
||||
dependenciesReader: { _, _ in DependenciesReaderFake(dependencies: [:]) },
|
||||
markerWriter: { _, _ in markerWriterSpy },
|
||||
llbuildLockFile: "/tmp/lock",
|
||||
fileManager: FileManager.default
|
||||
)
|
||||
|
||||
@@ -174,4 +188,48 @@ class PhaseCacheModeControllerTests: XCTestCase {
|
||||
}
|
||||
XCTAssertEqual(Set(deps), expectedMarkerFiles)
|
||||
}
|
||||
|
||||
func testDeletesLockOnEnable() throws {
|
||||
let lockURL = rootDir.appendingPathComponent("1.lock")
|
||||
try fileManager.spt_createEmptyFile(lockURL)
|
||||
let dependenciesReader = DependenciesReaderFake(dependencies: [:])
|
||||
simpleController = PhaseCacheModeController(
|
||||
tempDir: sampleURL,
|
||||
mergeCommitFile: sampleURL,
|
||||
phaseDependencyPath: "",
|
||||
markerPath: "",
|
||||
forceCached: false,
|
||||
dependenciesWriter: { _, _ in DependenciesWriterSpy() },
|
||||
dependenciesReader: { _, _ in dependenciesReader },
|
||||
markerWriter: { _, _ in MarkerWriterSpy() },
|
||||
llbuildLockFile: lockURL,
|
||||
fileManager: FileManager.default
|
||||
)
|
||||
|
||||
try simpleController.enable(allowedInputFiles: [], dependencies: [])
|
||||
|
||||
XCTAssertFalse(fileManager.fileExists(atPath: lockURL.path))
|
||||
}
|
||||
|
||||
func testDeletesLockOnDisable() throws {
|
||||
let lockURL = rootDir.appendingPathComponent("1.lock")
|
||||
try fileManager.spt_createEmptyFile(lockURL)
|
||||
let dependenciesReader = DependenciesReaderFake(dependencies: [:])
|
||||
simpleController = PhaseCacheModeController(
|
||||
tempDir: sampleURL,
|
||||
mergeCommitFile: sampleURL,
|
||||
phaseDependencyPath: "",
|
||||
markerPath: "",
|
||||
forceCached: false,
|
||||
dependenciesWriter: { _, _ in DependenciesWriterSpy() },
|
||||
dependenciesReader: { _, _ in dependenciesReader },
|
||||
markerWriter: { _, _ in MarkerWriterSpy() },
|
||||
llbuildLockFile: lockURL,
|
||||
fileManager: FileManager.default
|
||||
)
|
||||
|
||||
try simpleController.disable()
|
||||
|
||||
XCTAssertFalse(fileManager.fileExists(atPath: lockURL.path))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// 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 StaticFileListReaderTests: XCTestCase {
|
||||
func testCanAlwaysRead() throws {
|
||||
let reader = StaticFileListReader(list: [])
|
||||
|
||||
XCTAssertTrue(reader.canRead())
|
||||
}
|
||||
|
||||
func testListsPassedUrls() throws {
|
||||
let url: URL = "/file"
|
||||
let reader = StaticFileListReader(list: [url])
|
||||
|
||||
XCTAssertEqual(try reader.listFilesURLs(), [url])
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import XCTest
|
||||
class TargetDependenciesReaderTests: XCTestCase {
|
||||
|
||||
private let workingURL: URL = "/test"
|
||||
private let assetsWorkingURL: URL = "/assetsTest"
|
||||
private var dirAccessor: DirAccessorFake!
|
||||
private var reader: TargetDependenciesReader!
|
||||
|
||||
@@ -35,9 +36,15 @@ class TargetDependenciesReaderTests: XCTestCase {
|
||||
let fakeDependency = url.deletingPathExtension().appendingPathExtension("swift")
|
||||
return DependenciesReaderFake(dependencies: ["": [fakeDependency.path]])
|
||||
}
|
||||
let assetsFakeDependencyReaderFactory: (URL) -> DependenciesReader = { url in
|
||||
let fakeDependency = url.deletingLastPathComponent().appendingPathComponent("Contents.json")
|
||||
return DependenciesReaderFake(dependencies: ["": [fakeDependency.path]])
|
||||
}
|
||||
reader = TargetDependenciesReader(
|
||||
workingURL,
|
||||
fileDependeciesReaderFactory: swiftFakeDependencyReaderFactory,
|
||||
compilationOutputDir: workingURL,
|
||||
assetsCatalogOutputDir: assetsWorkingURL,
|
||||
fileDependenciesReaderFactory: swiftFakeDependencyReaderFactory,
|
||||
assetsDependenciesReaderFactory: assetsFakeDependencyReaderFactory,
|
||||
dirScanner: dirAccessor
|
||||
)
|
||||
}
|
||||
@@ -70,4 +77,13 @@ class TargetDependenciesReaderTests: XCTestCase {
|
||||
|
||||
XCTAssertEqual(deps, ["/test/some-master.swift"])
|
||||
}
|
||||
|
||||
func testFindsAssetsCatalogDependencies() throws {
|
||||
let assetsContentFile: URL = "/assetsTest/assetcatalog_dependencies"
|
||||
try dirAccessor.write(toPath: assetsContentFile.path, contents: Data())
|
||||
|
||||
let deps = try reader.findDependencies()
|
||||
|
||||
XCTAssertEqual(deps, ["/assetsTest/Contents.json"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// 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
|
||||
@testable import XCRemoteCache
|
||||
|
||||
// FileAcccessor that fails if one wants to acquire a lock
|
||||
class DisallowedExclusiveFileAccessor: ExclusiveFileAccessor {
|
||||
func exclusiveAccess<T>(block: (FileHandle) throws -> (T)) throws -> T {
|
||||
throw "Invoked ProhibitedExclusiveFileAccessor"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@testable import XCRemoteCache
|
||||
|
||||
// Thread-unsafe, in-memory lock
|
||||
class FakeExclusiveFileAccessor: ExclusiveFileAccessor {
|
||||
private(set) var isLocked = false
|
||||
private var pattern: [LockFileContent]
|
||||
|
||||
enum LockFileContent {
|
||||
case empty
|
||||
case nonEmptyForRead(URL)
|
||||
case nonEmptyForWrite(URL)
|
||||
|
||||
func fileHandle() throws -> FileHandle {
|
||||
switch self {
|
||||
case .empty: return FileHandle.nullDevice
|
||||
case .nonEmptyForRead(let url): return try FileHandle(forReadingFrom: url)
|
||||
case .nonEmptyForWrite(let url): return try FileHandle(forWritingTo: url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(pattern: [LockFileContent] = []) {
|
||||
// keep in the reversed order to always pop
|
||||
self.pattern = pattern.reversed()
|
||||
}
|
||||
|
||||
func exclusiveAccess<T>(block: (FileHandle) throws -> (T)) throws -> T {
|
||||
if isLocked {
|
||||
throw "FakeExclusiveFileAccessor lock is already locked"
|
||||
}
|
||||
defer {
|
||||
isLocked = false
|
||||
}
|
||||
isLocked = true
|
||||
let fileHandle = try (pattern.popLast() ?? .empty).fileHandle()
|
||||
return try block(fileHandle)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -34,7 +34,10 @@ class GitClientFake: GitClient {
|
||||
}
|
||||
|
||||
func getCommonPrimarySha() throws -> String {
|
||||
shaHistory[primaryBranchIndex].sha
|
||||
guard shaHistory.count > primaryBranchIndex else {
|
||||
throw GitClientError.noCommonShaWithPrimaryRepo(remoteName: "testing", error: "SampleError")
|
||||
}
|
||||
return shaHistory[primaryBranchIndex].sha
|
||||
}
|
||||
|
||||
func getShaDate(sha: String) throws -> Date {
|
||||
|
||||
@@ -123,7 +123,8 @@ module CocoapodsXCRemoteCacheModifier
|
||||
exclude_build_configurations,
|
||||
final_target,
|
||||
fake_src_root,
|
||||
exclude_sdks_configurations
|
||||
exclude_sdks_configurations,
|
||||
enable_swift_driver_integration
|
||||
)
|
||||
srcroot_relative_xc_location = parent_dir(xc_location, repo_distance)
|
||||
# location of the entrite CocoaPods project, relative to SRCROOT
|
||||
@@ -137,14 +138,15 @@ module CocoapodsXCRemoteCacheModifier
|
||||
elsif mode == 'producer' || mode == 'producer-fast'
|
||||
config.build_settings.delete('CC') if config.build_settings.key?('CC')
|
||||
end
|
||||
reset_build_setting(config.build_settings, 'SWIFT_EXEC', "$SRCROOT/#{srcroot_relative_xc_location}/xcswiftc", exclude_sdks_configurations)
|
||||
swiftc_name = enable_swift_driver_integration ? 'swiftc' : 'xcswiftc'
|
||||
reset_build_setting(config.build_settings, 'SWIFT_EXEC', "$SRCROOT/#{srcroot_relative_xc_location}/#{swiftc_name}", exclude_sdks_configurations)
|
||||
reset_build_setting(config.build_settings, 'LIBTOOL', "$SRCROOT/#{srcroot_relative_xc_location}/xclibtool", exclude_sdks_configurations)
|
||||
# Setting LIBTOOL to '' breaks SwiftDriver intengration so resetting it to the original value 'libtool' for all excluded configurations
|
||||
add_build_setting_for_sdks(config.build_settings, 'LIBTOOL', 'libtool', exclude_sdks_configurations)
|
||||
reset_build_setting(config.build_settings, 'LD', "$SRCROOT/#{srcroot_relative_xc_location}/xcld", exclude_sdks_configurations)
|
||||
reset_build_setting(config.build_settings, 'LDPLUSPLUS', "$SRCROOT/#{srcroot_relative_xc_location}/xcldplusplus", exclude_sdks_configurations)
|
||||
reset_build_setting(config.build_settings, 'LIPO', "$SRCROOT/#{srcroot_relative_xc_location}/xclipo", exclude_sdks_configurations)
|
||||
reset_build_setting(config.build_settings, 'SWIFT_USE_INTEGRATED_DRIVER', 'NO', exclude_sdks_configurations)
|
||||
reset_build_setting(config.build_settings, 'SWIFT_USE_INTEGRATED_DRIVER', 'NO', exclude_sdks_configurations) unless enable_swift_driver_integration
|
||||
|
||||
reset_build_setting(config.build_settings, 'XCREMOTE_CACHE_FAKE_SRCROOT', fake_src_root, exclude_sdks_configurations)
|
||||
reset_build_setting(config.build_settings, 'XCRC_PLATFORM_PREFERRED_ARCH', "$(LINK_FILE_LIST_$(CURRENT_VARIANT)_$(PLATFORM_PREFERRED_ARCH):dir:standardizepath:file:default=arm64)", exclude_sdks_configurations)
|
||||
@@ -498,6 +500,7 @@ module CocoapodsXCRemoteCacheModifier
|
||||
check_platform = @@configuration['check_platform']
|
||||
fake_src_root = @@configuration['fake_src_root']
|
||||
exclude_sdks_configurations = @@configuration['exclude_sdks_configurations'] || []
|
||||
enable_swift_driver_integration = @@configuration['enable_swift_driver_integration'] || false
|
||||
|
||||
xccc_location_absolute = "#{user_proj_directory}/#{xccc_location}"
|
||||
xcrc_location_absolute = "#{user_proj_directory}/#{xcrc_location}"
|
||||
@@ -521,7 +524,7 @@ module CocoapodsXCRemoteCacheModifier
|
||||
next if target.name.start_with?("Pods-")
|
||||
next if target.name.end_with?("Tests")
|
||||
next if exclude_targets.include?(target.name)
|
||||
enable_xcremotecache(target, 1, xcrc_location, xccc_location, mode, exclude_build_configurations, final_target,fake_src_root, exclude_sdks_configurations)
|
||||
enable_xcremotecache(target, 1, xcrc_location, xccc_location, mode, exclude_build_configurations, final_target,fake_src_root, exclude_sdks_configurations, enable_swift_driver_integration)
|
||||
end
|
||||
|
||||
# Create .rcinfo into `Pods` directory as that .xcodeproj reads configuration from .xcodeproj location
|
||||
@@ -534,7 +537,7 @@ module CocoapodsXCRemoteCacheModifier
|
||||
next if target.source_build_phase.files_references.empty?
|
||||
next if target.name.end_with?("Tests")
|
||||
next if exclude_targets.include?(target.name)
|
||||
enable_xcremotecache(target, 1, xcrc_location, xccc_location, mode, exclude_build_configurations, final_target,fake_src_root, exclude_sdks_configurations)
|
||||
enable_xcremotecache(target, 1, xcrc_location, xccc_location, mode, exclude_build_configurations, final_target,fake_src_root, exclude_sdks_configurations, enable_swift_driver_integration)
|
||||
end
|
||||
generated_project.save()
|
||||
end
|
||||
@@ -575,7 +578,7 @@ module CocoapodsXCRemoteCacheModifier
|
||||
# Attach XCRC to the app targets
|
||||
user_project.targets.each do |target|
|
||||
next if exclude_targets.include?(target.name)
|
||||
enable_xcremotecache(target, 0, xcrc_location, xccc_location, mode, exclude_build_configurations, final_target,fake_src_root, exclude_sdks_configurations)
|
||||
enable_xcremotecache(target, 0, xcrc_location, xccc_location, mode, exclude_build_configurations, final_target,fake_src_root, exclude_sdks_configurations, enable_swift_driver_integration)
|
||||
end
|
||||
|
||||
# Set Target sourcemap
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
# limitations under the License.
|
||||
|
||||
module CocoapodsXcremotecache
|
||||
VERSION = "0.0.16"
|
||||
VERSION = "0.0.17"
|
||||
end
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Architectural designs
|
||||
|
||||
1. [Swift Driver Integration](./SwiftDriverIntegration.md)
|
||||
@@ -0,0 +1,48 @@
|
||||
## Swift Driver Integration
|
||||
|
||||
### Pre Swift Driver Integration
|
||||
|
||||
Historically (prior to Xcode 14), Swift compilation step was invoked by Xcode as a single external process. Xcode was calling `swiftc` and passing all required parameters (like all input files, output destinations, header paths etc.), and reading its standard output to recognize the status/state of a compilation. Essentially, there were two build systems: "the big one" from Xcode and "small one" by Swift.
|
||||
|
||||
That design was easy to mock in the XCRemoteCache, where the `xcswiftc` wrapper was first inspecting if the cached artifact can be reused (e.g. no new input `.swift` files were added to the list of compilation files) and based on that either continuing with the local compilation (cache miss) or mocking the compilation and existing early (cache hit).
|
||||
|
||||
<p>
|
||||
<img src="./../img/pre-driver.png#gh-light-mode-only">
|
||||
<img src="./../img/pre-driver-dark.png#gh-dark-mode-only">
|
||||
</p>
|
||||
|
||||
### Swift Driver Integration Design
|
||||
|
||||
With the upgraded design (aka Swift Driver Integration), Xcode splits the work into `n` subprocesses (when `n` is ~CPU), each responsible to compile a subset of files/actions. To align with that, XCRemoteCache meeds to specify a single place to identify if the cached artifact is applicable. `swift-frontend` has been picked for that - process responsible for module emitting. By reviewing Xcode's behavior, it has been found that this process is scheduled very early in the workflow timeline (with some approximation, we could say it is scheduled as a first step) so it seems as best candidate for the "pre-work".
|
||||
|
||||
As the same executable `swift-frontend` is invoked multiple times for the same target (e.g. to emit module, multiple batches of compilation etc.), XCRemoteCaches uses a file lock-based synchronization. Each `xcswift-frontend` (the wrapper for `swift-frontend`) tries to acquire a unique lock file. The lock has a name `$LLBUILD_BUILD_ID.lock`, which is unique for each build, placed in the `Intermediate` directory. `xcswift-frontend` process reads its content to find if the "pre-work" from the emit-module has already been done - if not, it releases a lock a gives a way to other processes (presumably the "emit-module") to do the required work. As a lock file is unique per target and a build (it is actually unique per target compilation, placed in `TARGET_TEMP_DIR`), initially the file is empty.
|
||||
|
||||
Note the emit module step holds a shared lock for the time of the entire process lifetime, so only once the "pre-work" is finished, all other `xcswift-frontend` processes can continue their job (with either noop or fallbacking to the `swift-frontend` in case a cache miss). Non emit-module steps (compilation steps) acquire a lock only for a very short period - to read the content of that file, thus multiple batches of compilation can run in parallel.
|
||||
|
||||
<p>
|
||||
<img src="./../img/driver.png#gh-light-mode-only">
|
||||
<img src="./../img/driver-dark.png#gh-dark-mode-only">
|
||||
</p>
|
||||
|
||||
<img src="./../img/sample-driver-timeline.png" width="600px">
|
||||
|
||||
### Sample timelines
|
||||
|
||||
#### Emit Module acquires a lock first (common)
|
||||
|
||||
<p>
|
||||
<img src="./../img/driver-scenario1.png#gh-light-mode-only">
|
||||
<img src="./../img/driver-scenario1-dark.png#gh-dark-mode-only">
|
||||
</p>
|
||||
|
||||
#### A compilation step acquires a lock first (uncommon but possible)
|
||||
|
||||
<p>
|
||||
<img src="./../img/driver-scenario2.png#gh-light-mode-only">
|
||||
<img src="./../img/driver-scenario2-dark.png#gh-dark-mode-only">
|
||||
</p>
|
||||
|
||||
### Other considerations/open questions
|
||||
|
||||
* For mixed targets (ObjC&Swift), Xcode triggers `.m` compilation steps **after** the module emitting to ensure that the `-Swift.h` is available for clang compilation. That means, the synchronization algorithm will postpone any `clang` invocations until the Swift "pre-work" is done. Therefore, mixed targets should behave the same way as in the non Swift Driver Integration flow
|
||||
* For the WMO (Whole Module Optimization) mode, all compilation steps are combined into a single `swift-frontend` process. As the emit-module step is still invoked first, the WMO flow build can be considered as a special case of the algorithm described above (where there is only one compilation invocation). Therefore, the presented algorithm will work for the WMO mode out of the box.
|
||||
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 298 KiB |
@@ -376,7 +376,7 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "ditto \"${SCRIPT_INPUT_FILE_0}\" \"${SCRIPT_OUTPUT_FILE_0}\"\n[ -f \"${SCRIPT_INPUT_FILE_1}\" ] && ditto \"${SCRIPT_INPUT_FILE_1}\" \"${SCRIPT_OUTPUT_FILE_1}\" || rm \"${SCRIPT_OUTPUT_FILE_1}\"\n\n";
|
||||
shellScript = "ditto \"${SCRIPT_INPUT_FILE_0}\" \"${SCRIPT_OUTPUT_FILE_0}\"\n[ -f \"${SCRIPT_INPUT_FILE_1}\" ] && ditto \"${SCRIPT_INPUT_FILE_1}\" \"${SCRIPT_OUTPUT_FILE_1}\" || rm -f \"${SCRIPT_OUTPUT_FILE_1}\"\n\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
require 'json'
|
||||
require "ostruct"
|
||||
require 'yaml'
|
||||
|
||||
desc 'Support for E2E tests: building XCRemoteCache-enabled xcodeproj using xcodebuild'
|
||||
namespace :e2e do
|
||||
# Name of the configuration used in both standalone and CocoaPods tests
|
||||
CONFIGURATION = 'Debug'
|
||||
# Supported only in standalone
|
||||
CONFIGURATIONS_EXCLUDE = 'Release'
|
||||
COCOAPODS_DIR = 'cocoapods-plugin'
|
||||
COCOAPODS_GEMSPEC_FILENAME = "cocoapods-xcremotecache.gemspec"
|
||||
E2E_COCOAPODS_SAMPLE_DIR = 'e2eTests/XCRemoteCacheSample'
|
||||
@@ -21,12 +26,21 @@ namespace :e2e do
|
||||
'primary_branch' => GIT_BRANCH,
|
||||
'mode' => 'consumer',
|
||||
'final_target' => 'XCRemoteCacheSample',
|
||||
'artifact_maximum_age' => 0
|
||||
'artifact_maximum_age' => 0,
|
||||
|
||||
}.freeze
|
||||
# A list of configurations to merge with SHARED_COCOAPODS_CONFIG to run tests with
|
||||
CONFIGS = {
|
||||
'no_swift_driver' => {},
|
||||
'swift_driver' => {
|
||||
'enable_swift_driver_integration' => true
|
||||
}
|
||||
}.freeze
|
||||
DEFAULT_EXPECTATIONS = {
|
||||
'misses' => 0,
|
||||
'hit_rate' => 100
|
||||
}.freeze
|
||||
EXCLUDED_ARCHS = 'x86_64'
|
||||
|
||||
Stats = Struct.new(:hits, :misses, :hit_rate)
|
||||
|
||||
@@ -39,9 +53,14 @@ namespace :e2e do
|
||||
start_nginx
|
||||
configure_git
|
||||
|
||||
# Run scenarios for all Podfile scenarios
|
||||
for podfile_path in Dir.glob('e2eTests/**/*.Podfile')
|
||||
run_cocoapods_scenario(podfile_path)
|
||||
for config_name, custom_config in CONFIGS
|
||||
config = SHARED_COCOAPODS_CONFIG.merge(custom_config)
|
||||
puts "Running E2E tests for config: #{config_name}"
|
||||
|
||||
# Run scenarios for all Podfile scenarios
|
||||
for podfile_path in Dir.glob('e2eTests/**/*.Podfile')
|
||||
run_cocoapods_scenario(config, podfile_path)
|
||||
end
|
||||
end
|
||||
# Revert all side effects
|
||||
clean
|
||||
@@ -52,19 +71,33 @@ namespace :e2e do
|
||||
clean_server
|
||||
start_nginx
|
||||
configure_git
|
||||
CONFIGS.each do |config_name, config|
|
||||
puts "Running Standalone tests for config: #{config_name}"
|
||||
run_standalone_scenario(config, config_name)
|
||||
end
|
||||
end
|
||||
|
||||
def self.run_standalone_scenario(config, config_name)
|
||||
# Prepare binaries for the standalone mode
|
||||
prepare_for_standalone(E2E_STANDALONE_SAMPLE_DIR)
|
||||
|
||||
puts 'Building standalone producer...'
|
||||
####### Producer #########
|
||||
clean_git
|
||||
|
||||
Dir.chdir(E2E_STANDALONE_SAMPLE_DIR) do
|
||||
clean_git
|
||||
system 'git checkout -f .'
|
||||
# Include the config in the "shared" configuration that is commited-in to '.rcinfo'
|
||||
rcinfo_path = '.rcinfo'
|
||||
rcinfo = YAML.load(File.read(rcinfo_path)).merge(config)
|
||||
File.open(rcinfo_path, 'w') {|f| f.write rcinfo.to_yaml }
|
||||
|
||||
# Run integrate the project
|
||||
system("pwd")
|
||||
system("#{XCRC_BINARIES}/xcprepare integrate --input StandaloneApp.xcodeproj --mode producer --final-producer-target StandaloneApp")
|
||||
system("#{XCRC_BINARIES}/xcprepare integrate --input StandaloneApp.xcodeproj --mode producer --final-producer-target StandaloneApp --configurations-exclude #{CONFIGURATIONS_EXCLUDE}")
|
||||
# Build the project to fill in the cache
|
||||
build_project(nil, "StandaloneApp.xcodeproj", 'WatchExtension', 'watch', 'watchOS')
|
||||
build_project(nil, "StandaloneApp.xcodeproj", 'StandaloneApp')
|
||||
build_project(nil, "StandaloneApp.xcodeproj", 'WatchExtension', 'watch', 'watchOS', CONFIGURATION)
|
||||
build_project(nil, "StandaloneApp.xcodeproj", 'StandaloneApp', 'iphone', 'iOS', CONFIGURATION)
|
||||
system("#{XCRC_BINARIES}/xcprepare stats --reset --format json")
|
||||
end
|
||||
|
||||
@@ -72,22 +105,25 @@ namespace :e2e do
|
||||
|
||||
####### Consumer #########
|
||||
# new dir to emulate different srcroot
|
||||
consumer_srcroot = "#{E2E_STANDALONE_SAMPLE_DIR}_consumer"
|
||||
consumer_srcroot = "#{E2E_STANDALONE_SAMPLE_DIR}_consumer_#{config_name}"
|
||||
system("mv #{E2E_STANDALONE_SAMPLE_DIR} #{consumer_srcroot}")
|
||||
at_exit { puts("reverting #{E2E_STANDALONE_SAMPLE_DIR}"); system("mv #{consumer_srcroot} #{E2E_STANDALONE_SAMPLE_DIR}") }
|
||||
begin
|
||||
prepare_for_standalone(consumer_srcroot)
|
||||
Dir.chdir(consumer_srcroot) do
|
||||
system("#{XCRC_BINARIES}/xcprepare integrate --input StandaloneApp.xcodeproj --mode consumer --final-producer-target StandaloneApp --consumer-eligible-configurations #{CONFIGURATION} --configurations-exclude #{CONFIGURATIONS_EXCLUDE}")
|
||||
build_project(nil, "StandaloneApp.xcodeproj", 'WatchExtension', 'watch', 'watchOS', CONFIGURATION, {'derivedDataPath' => "#{DERIVED_DATA_PATH}_consumer_#{config_name}"})
|
||||
build_project(nil, "StandaloneApp.xcodeproj", 'StandaloneApp', 'iphone', 'iOS', CONFIGURATION, {'derivedDataPath' => "#{DERIVED_DATA_PATH}_consumer_#{config_name}"})
|
||||
valide_hit_rate(OpenStruct.new(DEFAULT_EXPECTATIONS))
|
||||
|
||||
prepare_for_standalone(consumer_srcroot)
|
||||
Dir.chdir(consumer_srcroot) do
|
||||
system("#{XCRC_BINARIES}/xcprepare integrate --input StandaloneApp.xcodeproj --mode consumer")
|
||||
build_project(nil, "StandaloneApp.xcodeproj", 'WatchExtension', 'watch', 'watchOS', {'derivedDataPath' => "#{DERIVED_DATA_PATH}_consumer"})
|
||||
build_project(nil, "StandaloneApp.xcodeproj", 'StandaloneApp', 'iphone', 'iOS', {'derivedDataPath' => "#{DERIVED_DATA_PATH}_consumer"})
|
||||
valide_hit_rate(OpenStruct.new(DEFAULT_EXPECTATIONS))
|
||||
|
||||
puts 'Building standalone consumer with local change...'
|
||||
# Extra: validate local compilation of the Standalone ObjC code
|
||||
system("echo '' >> StandaloneApp/StandaloneObjc.m")
|
||||
build_project(nil, "StandaloneApp.xcodeproj", 'WatchExtension', 'watch', 'watchOS', {'derivedDataPath' => "#{DERIVED_DATA_PATH}_consumer_local"})
|
||||
build_project(nil, "StandaloneApp.xcodeproj", 'StandaloneApp', 'iphone', 'iOS', {'derivedDataPath' => "#{DERIVED_DATA_PATH}_consumer_local"})
|
||||
puts 'Building standalone consumer with local change...'
|
||||
# Extra: validate local compilation of the Standalone ObjC code
|
||||
system("echo '' >> StandaloneApp/StandaloneObjc.m")
|
||||
build_project(nil, "StandaloneApp.xcodeproj", 'WatchExtension', 'watch', 'watchOS', CONFIGURATION, {'derivedDataPath' => "#{DERIVED_DATA_PATH}_consumer_local_#{config_name}"})
|
||||
build_project(nil, "StandaloneApp.xcodeproj", 'StandaloneApp', 'iphone', 'iOS', CONFIGURATION, {'derivedDataPath' => "#{DERIVED_DATA_PATH}_consumer_local_#{config_name}"})
|
||||
end
|
||||
ensure
|
||||
puts("reverting #{E2E_STANDALONE_SAMPLE_DIR}")
|
||||
system("mv #{consumer_srcroot} #{E2E_STANDALONE_SAMPLE_DIR}")
|
||||
end
|
||||
|
||||
# Revert all side effects
|
||||
@@ -149,9 +185,9 @@ namespace :e2e do
|
||||
end
|
||||
|
||||
# xcremotecache configuration to add to Podfile
|
||||
def self.cocoapods_configuration_string(extra_configs = {})
|
||||
def self.cocoapods_configuration_string(config, extra_configs = {})
|
||||
configuration_lines = ['xcremotecache({']
|
||||
all_properties = SHARED_COCOAPODS_CONFIG.merge(extra_configs)
|
||||
all_properties = config.merge(extra_configs)
|
||||
config_lines = all_properties.map {|key, value| " \"#{key}\" => #{value.inspect},"}
|
||||
configuration_lines.push(*config_lines)
|
||||
configuration_lines << '})'
|
||||
@@ -167,18 +203,18 @@ namespace :e2e do
|
||||
end
|
||||
end
|
||||
|
||||
def self.build_project(workspace, project, scheme, sdk = 'iphone', platform = 'iOS', extra_args = {})
|
||||
def self.build_project(workspace, project, scheme, sdk = 'iphone', platform = 'iOS', configuration = 'Debug', extra_args = {})
|
||||
xcodebuild_args = {
|
||||
'workspace' => workspace,
|
||||
'project' => project,
|
||||
'scheme' => scheme,
|
||||
'configuration' => 'Debug',
|
||||
'configuration' => configuration,
|
||||
'sdk' => "#{sdk}simulator",
|
||||
'destination' => "generic/platform=#{platform} Simulator",
|
||||
'derivedDataPath' => DERIVED_DATA_PATH,
|
||||
}.merge(extra_args).compact
|
||||
xcodebuild_vars = {
|
||||
'EXCLUDED_ARCHS' => 'arm64'
|
||||
'EXCLUDED_ARCHS' => EXCLUDED_ARCHS
|
||||
}
|
||||
args = ['set -o pipefail;', 'xcodebuild']
|
||||
args.push(*xcodebuild_args.map {|k,v| "-#{k} '#{v}'"})
|
||||
@@ -193,9 +229,9 @@ namespace :e2e do
|
||||
end
|
||||
end
|
||||
|
||||
def self.build_project_cocoapods(sdk = 'iphone', platform = 'iOS', extra_args = {})
|
||||
def self.build_project_cocoapods(sdk = 'iphone', platform = 'iOS', configuration = 'Debug', extra_args = {})
|
||||
system('pod install')
|
||||
build_project('XCRemoteCacheSample.xcworkspace', nil, 'XCRemoteCacheSample', sdk, platform, extra_args)
|
||||
build_project('XCRemoteCacheSample.xcworkspace', nil, 'XCRemoteCacheSample', sdk, platform, configuration, extra_args)
|
||||
end
|
||||
|
||||
def self.read_stats
|
||||
@@ -223,12 +259,12 @@ namespace :e2e do
|
||||
puts("Hit rate: #{status.hit_rate}% (#{status.hits}/#{all_targets})")
|
||||
end
|
||||
|
||||
def self.run_cocoapods_scenario(template_path)
|
||||
def self.run_cocoapods_scenario(config, template_path)
|
||||
# Optional file, which adds extra cocoapods configs to a template
|
||||
template_config_path = "#{template_path}.config"
|
||||
extra_config = File.exist?(template_config_path) ? JSON.load(File.read(template_config_path)) : {}
|
||||
producer_configuration = cocoapods_configuration_string({'mode' => 'producer'}.merge(extra_config))
|
||||
consumer_configuration = cocoapods_configuration_string(extra_config)
|
||||
producer_configuration = cocoapods_configuration_string(config, {'mode' => 'producer'}.merge(extra_config))
|
||||
consumer_configuration = cocoapods_configuration_string(config, extra_config)
|
||||
expectations = build_expectations(template_path)
|
||||
|
||||
puts("****** Scenario: #{template_path}")
|
||||
@@ -238,7 +274,7 @@ namespace :e2e do
|
||||
dump_podfile(producer_configuration, template_path)
|
||||
puts('Building producer ...')
|
||||
Dir.chdir(E2E_COCOAPODS_SAMPLE_DIR) do
|
||||
build_project_cocoapods
|
||||
build_project_cocoapods('iphone', 'iOS', CONFIGURATION)
|
||||
# reset XCRemoteCache stats
|
||||
system("#{XCRC_BINARIES}/xcprepare stats --reset --format json")
|
||||
end
|
||||
@@ -248,7 +284,7 @@ namespace :e2e do
|
||||
dump_podfile(consumer_configuration, template_path)
|
||||
puts('Building consumer ...')
|
||||
Dir.chdir(E2E_COCOAPODS_SAMPLE_DIR) do
|
||||
build_project_cocoapods('iphone', 'iOS', {'derivedDataPath' => "#{DERIVED_DATA_PATH}_consumer"})
|
||||
build_project_cocoapods('iphone', 'iOS', CONFIGURATION, {'derivedDataPath' => "#{DERIVED_DATA_PATH}_consumer"})
|
||||
valide_hit_rate(expectations)
|
||||
end
|
||||
end
|
||||
|
||||