Compare commits

..

32 Commits

Author SHA1 Message Date
Bartosz Polaczyk afb1f9e531 Merge pull request #215 from polac24/xcode-15-lightweight-support
Add support for Xcode 15
2023-06-14 02:56:02 -04:00
Bartosz Polaczyk 53e7ddd34c Fix linters 2023-06-13 21:57:31 -07:00
Bartosz Polaczyk 344b1f13ca Cleanup 2023-06-13 21:25:34 -07:00
Bartosz Polaczyk bc13f58e73 Add unit tests 2023-06-13 21:08:56 -07:00
Bartosz Polaczyk 47ad8a890b Fix swiftc allowed paths 2023-06-12 23:33:09 -07:00
Bartosz Polaczyk 801920dc87 Fix reference path 2023-06-12 23:25:37 -07:00
Bartosz Polaczyk 37e144c36a Add unit tests 2023-06-12 22:58:05 -07:00
Bartosz Polaczyk f389887e37 Read also dependencies from assetcatalog_dependencies output 2023-06-12 22:22:59 -07:00
Bartosz Polaczyk b74e002415 Merge pull request #212 from polac24/add-driver-tests
[SwiftDriverIntegraiton] Part IV: Add E2E tests for the swift driver integration #5
2023-06-07 10:07:04 -04:00
Bartosz Polaczyk 9a34a99f0b Merge pull request #210 from polac24/add-driver-sync
[SwiftDriver] Part III - Introduce synchronization between swift-frontend invocations
2023-06-06 02:23:21 -04:00
Bartosz Polaczyk f4eed9e8aa Apply review suggestions 2023-06-05 22:45:41 -07:00
Bartosz Polaczyk 811cc00f0c Add E2E tests for swift driver integration 2023-06-04 08:56:38 -07:00
Bartosz Polaczyk 56d9c208a1 Fix typos 2023-06-03 21:25:31 -07:00
Bartosz Polaczyk 8dffbd4162 Add missing unit tests 2023-06-03 17:50:50 -07:00
Bartosz Polaczyk 3853ce2bc2 Write to the shared frontend lock early 2023-06-03 09:32:53 -07:00
Bartosz Polaczyk 148c99d2f5 Add docs 2023-06-01 20:32:03 -07:00
Bartosz Polaczyk 65bf9156ec Merge pull request #209 from polac24/add-driver-parsing
Swift-driver integration, Part II: add Swift front-end parsing stage
2023-06-01 10:12:38 -04:00
Bartosz Polaczyk 22484b4daf Add syncing implementation 2023-05-31 19:55:30 -07:00
Bartosz Polaczyk 867bbb6265 Fix typos 2023-05-31 10:22:34 -04:00
Bartosz Polaczyk 532484c3ab Add comment 2023-05-30 21:38:22 -07:00
Bartosz Polaczyk 338cbd141a Fix linters 2023-05-30 21:08:05 -07:00
Bartosz Polaczyk f15dd8f98d Add Swift front-end mocking parsing stage 2023-05-30 20:53:48 -07:00
Bartosz Polaczyk ee31a3815f Merge pull request #208 from polac24/support-xcode14
Refactor existing implementation for swift-driver change
2023-05-30 09:58:36 -04:00
Bartosz Polaczyk 5749762b86 Fix compilation 2023-05-29 21:25:46 -07:00
Bartosz Polaczyk f8757b6ee7 Add unit tests 2023-05-29 21:01:53 -07:00
Bartosz Polaczyk c26aaf7d42 Refactor existing implementation for swift-driver change 2023-05-29 20:25:50 -07:00
Bartosz Polaczyk d837f6e14b Merge pull request #205 from polac24/bartosz/20230516-support-xcode143
Support Xcode14.3
2023-05-18 07:00:07 -07:00
Bartosz Polaczyk 5528d507b0 Support Xcode 14.3 2023-05-16 16:37:28 -07:00
Bartosz Polaczyk 352e72f44c Merge pull request #199 from polac24/add-graceful-missing-common-sha
Add support for graceful handling missing common sha
2023-04-20 10:04:08 -04:00
Bartosz Polaczyk 1c67b79a7a Apply suggestions from code review
Co-authored-by: Aleksander Grzyb <aleksander.grzyb@gmail.com>
2023-04-19 21:44:17 -04:00
Bartosz Polaczyk c7de203741 Fix linting 2023-04-18 19:51:10 -07:00
Bartosz Polaczyk b7e18916e6 Add support for graceful handling missing common sha 2023-04-18 19:41:53 -07:00
74 changed files with 2902 additions and 231 deletions
+5
View File
@@ -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",
+8 -1
View File
@@ -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 -3
View File
@@ -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)
}
}
+22
View File
@@ -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()
+31 -24
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
# Architectural designs
1. [Swift Driver Integration](./SwiftDriverIntegration.md)
+48
View File
@@ -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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

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 */
+70 -34
View File
@@ -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