Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d837f6e14b | |||
| 5528d507b0 | |||
| 352e72f44c | |||
| 1c67b79a7a | |||
| c7de203741 | |||
| b7e18916e6 | |||
| dfb4039404 | |||
| b28613a2ef | |||
| 1535b762bc | |||
| e6816846c3 | |||
| b89d98f411 | |||
| a0d3d1b0b9 | |||
| f432917505 | |||
| 5d297a4fb2 | |||
| b0d5f1660e | |||
| baea2de79a | |||
| d2803f4ad5 | |||
| ab017367b2 | |||
| 30cb648641 | |||
| 3330ca45f8 | |||
| d3193b15a8 | |||
| 7b14f2c9ff | |||
| 4933b454a5 | |||
| 79ffdce295 | |||
| 376e6a17c5 | |||
| a4d1849821 | |||
| 325fb07080 | |||
| a2afe62751 | |||
| bbaa374e12 | |||
| 39a259ff49 | |||
| a2b9cbf332 | |||
| 5710594bc4 | |||
| 7d123792b8 | |||
| 5856dbec77 | |||
| 600310f44b | |||
| aae2b3289c |
@@ -152,6 +152,7 @@ xcremotecache/xcprepare integrate --input <yourProject.xcodeproj> --mode consume
|
||||
| `--lldb-init` | LLDBInit mode. Appends to .lldbinit a command required for debugging. Supported values: 'none' (do not append to .lldbinit), 'user' (append to ~/.lldbinit) | `user` | ⬜️ |
|
||||
| `--fake-src-root` | An arbitrary source location shared between producers and consumers. Should be unique for a project. | `/xxxxxxxxxx` | ⬜️ |
|
||||
| `--output` | Save the project with integrated XCRemoteCache to a separate location. | N/A | ⬜️ |
|
||||
| `--sdks-exclude` | comma separated list of sdks to not integrate XCRemoteCache (e.g. "watchos*, watchsimulator*"). (Experimental) | `""` | ⬜️ |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -357,6 +358,7 @@ 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` | ⬜️ |
|
||||
|
||||
## Backend cache server
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,8 @@ public struct PostbuildContext {
|
||||
let irrelevantDependenciesPaths: [String]
|
||||
/// Location of public headers. Not always available (e.g. static libraries)
|
||||
var publicHeadersFolderPath: URL?
|
||||
/// XCRemoteCache is explicitly disabled
|
||||
let disabled: Bool
|
||||
}
|
||||
|
||||
extension PostbuildContext {
|
||||
@@ -146,5 +148,6 @@ extension PostbuildContext {
|
||||
// generated and it is up to a project configuration to place it in a common location (e.g. static library)
|
||||
publicHeadersFolderPath = builtProductsDir.appendingPathComponent(publicHeadersPath)
|
||||
}
|
||||
disabled = try env.readEnv(key: "XCRC_DISABLED") ?? false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,7 +274,12 @@ public class XCPostbuild {
|
||||
)
|
||||
|
||||
// Trigger build completion
|
||||
if try modeController.isEnabled() {
|
||||
if context.disabled {
|
||||
infoLog("XCRC fully disabled for \(context.targetName), \(context.platform), \(context.configuration)")
|
||||
// Cutoff the process is disabled, but generate an "empty" list of dependencies
|
||||
try? modeController.disable()
|
||||
return
|
||||
} else if try modeController.isEnabled() {
|
||||
// Decorate .swiftmodule in the product dir with fingerprint(s) overrides from a cache artifact
|
||||
try postbuildAction.performBuildCompletion()
|
||||
} else if context.mode == .consumer {
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
import Foundation
|
||||
|
||||
enum PrebuildResult: Equatable {
|
||||
case disabled
|
||||
case incompatible
|
||||
case compatible(localDependencies: [URL])
|
||||
}
|
||||
@@ -57,6 +58,9 @@ class Prebuild {
|
||||
|
||||
// swiftlint:disable:next function_body_length
|
||||
public func perform() throws -> PrebuildResult {
|
||||
guard !context.disabled else {
|
||||
return .disabled
|
||||
}
|
||||
guard case .available(let commit) = context.remoteCommit else {
|
||||
return .incompatible
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ public struct PrebuildContext {
|
||||
/// location of the json file that define virtual files system overlay
|
||||
/// (mappings of the virtual location file -> local file path)
|
||||
let overlayHeadersPath: URL
|
||||
/// XCRemoteCache is explicitly disabled
|
||||
let disabled: Bool
|
||||
}
|
||||
|
||||
extension PrebuildContext {
|
||||
@@ -69,5 +71,6 @@ extension PrebuildContext {
|
||||
thinnedTargets = thinFocusedTargetsString?.split(separator: ",").map(String.init)
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,6 +202,9 @@ public class XCPrebuild {
|
||||
case .compatible(localDependencies: let dependencies):
|
||||
// TODO: pass `allowedInputFiles` observed in the build time
|
||||
try modeController.enable(allowedInputFiles: dependencies, dependencies: dependencies)
|
||||
case .disabled:
|
||||
infoLog("XCRemoteCache is explicitly disabled")
|
||||
try modeController.disable()
|
||||
}
|
||||
} catch {
|
||||
disableRemoteCache(
|
||||
|
||||
+43
-12
@@ -34,24 +34,33 @@ class XcodeProjBuildSettingsIntegrateAppender: BuildSettingsIntegrateAppender {
|
||||
private let mode: Mode
|
||||
private let repoRoot: URL
|
||||
private let fakeSrcRoot: URL
|
||||
private let sdksExclude: [String]
|
||||
|
||||
init(mode: Mode, repoRoot: URL, fakeSrcRoot: URL) {
|
||||
init(mode: Mode, repoRoot: URL, fakeSrcRoot: URL, sdksExclude: [String]) {
|
||||
self.mode = mode
|
||||
self.repoRoot = repoRoot
|
||||
self.fakeSrcRoot = fakeSrcRoot
|
||||
self.sdksExclude = sdksExclude
|
||||
}
|
||||
|
||||
func appendToBuildSettings(buildSettings: BuildSettings, wrappers: XCRCBinariesPaths) -> BuildSettings {
|
||||
var result = buildSettings
|
||||
result["SWIFT_EXEC"] = wrappers.swiftc.path
|
||||
result["SWIFT_USE_INTEGRATED_DRIVER"] = "NO"
|
||||
setBuildSetting(buildSettings: &result, key: "SWIFT_EXEC", value: wrappers.swiftc.path )
|
||||
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 {
|
||||
result["CC"] = wrappers.cc.path
|
||||
result["LD"] = wrappers.ld.path
|
||||
result["LIBTOOL"] = wrappers.libtool.path
|
||||
result["LIPO"] = wrappers.lipo.path
|
||||
result["LDPLUSPLUS"] = wrappers.ldplusplus.path
|
||||
setBuildSetting(buildSettings: &result, key: "CC", value: wrappers.cc.path )
|
||||
setBuildSetting(buildSettings: &result, key: "LD", value: wrappers.ld.path )
|
||||
// Setting LIBTOOL to '' breaks SwiftDriver intengration so resetting it to the original value
|
||||
// 'libtool' for all excluded configurations
|
||||
setBuildSetting(
|
||||
buildSettings: &result,
|
||||
key: "LIBTOOL",
|
||||
value: wrappers.libtool.path,
|
||||
excludedValue: "libtool"
|
||||
)
|
||||
setBuildSetting(buildSettings: &result, key: "LIPO", value: wrappers.lipo.path )
|
||||
setBuildSetting(buildSettings: &result, key: "LDPLUSPLUS", value: wrappers.ldplusplus.path )
|
||||
}
|
||||
|
||||
let existingSwiftFlags = result["OTHER_SWIFT_FLAGS"] as? String
|
||||
@@ -63,14 +72,36 @@ class XcodeProjBuildSettingsIntegrateAppender: BuildSettingsIntegrateAppender {
|
||||
swiftFlags.assignFlag(key: "debug-prefix-map", value: "\(repoRoot.path)=$(XCRC_FAKE_SRCROOT)")
|
||||
clangFlags.assignFlag(key: "debug-prefix-map", value: "\(repoRoot.path)=$(XCRC_FAKE_SRCROOT)")
|
||||
|
||||
result["OTHER_SWIFT_FLAGS"] = swiftFlags.settingValue
|
||||
result["OTHER_CFLAGS"] = clangFlags.settingValue
|
||||
setBuildSetting(buildSettings: &result, key: "OTHER_SWIFT_FLAGS", value: swiftFlags.settingValue )
|
||||
setBuildSetting(buildSettings: &result, key: "OTHER_CFLAGS", value: clangFlags.settingValue )
|
||||
|
||||
result["XCRC_FAKE_SRCROOT"] = "\(fakeSrcRoot.path)"
|
||||
result["XCRC_PLATFORM_PREFERRED_ARCH"] =
|
||||
setBuildSetting(buildSettings: &result, key: "XCRC_FAKE_SRCROOT", value: "\(fakeSrcRoot.path)" )
|
||||
setBuildSetting(buildSettings: &result, key: "XCRC_PLATFORM_PREFERRED_ARCH", value:
|
||||
"""
|
||||
$(LINK_FILE_LIST_$(CURRENT_VARIANT)_$(PLATFORM_PREFERRED_ARCH):dir:standardizepath:file:default=arm64)
|
||||
"""
|
||||
)
|
||||
|
||||
explicitlyDisableSDKs(buildSettings: &result)
|
||||
return result
|
||||
}
|
||||
|
||||
private func setBuildSetting(buildSettings: inout BuildSettings, key: String, value: String?, excludedValue: String = "") {
|
||||
buildSettings[key] = value
|
||||
guard value != nil else {
|
||||
// no need to exclude as the value will
|
||||
return
|
||||
}
|
||||
// Erase all overrides for a given sdk so a default toolchain is used
|
||||
for skippedSDK in sdksExclude {
|
||||
buildSettings["\(key)[sdk=\(skippedSDK)]"] = excludedValue
|
||||
}
|
||||
}
|
||||
|
||||
// For all exlcuded SDKs passes XCRC_DISABLED=TRUE, which will cut-off early the pre_build phase
|
||||
private func explicitlyDisableSDKs(buildSettings: inout BuildSettings) {
|
||||
for skippedSDK in sdksExclude {
|
||||
buildSettings["XCRC_DISABLED[sdk=\(skippedSDK)]"] = "YES"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ public class XCIntegrate {
|
||||
private let consumerEligiblePlatforms: String
|
||||
private let lldbMode: LLDBInitMode
|
||||
private let fakeSrcRoot: String
|
||||
private let sdksExclude: String
|
||||
private let output: String?
|
||||
|
||||
public init(
|
||||
@@ -48,6 +49,7 @@ public class XCIntegrate {
|
||||
consumerEligiblePlatforms: String,
|
||||
lldbMode: LLDBInitMode,
|
||||
fakeSrcRoot: String,
|
||||
sdksExclude: String,
|
||||
output: String?
|
||||
) {
|
||||
projectPath = input
|
||||
@@ -61,6 +63,7 @@ public class XCIntegrate {
|
||||
self.consumerEligiblePlatforms = consumerEligiblePlatforms
|
||||
self.lldbMode = lldbMode
|
||||
self.fakeSrcRoot = fakeSrcRoot
|
||||
self.sdksExclude = sdksExclude
|
||||
self.output = output
|
||||
}
|
||||
|
||||
@@ -98,7 +101,8 @@ public class XCIntegrate {
|
||||
let buildSettingsAppender = XcodeProjBuildSettingsIntegrateAppender(
|
||||
mode: context.mode,
|
||||
repoRoot: context.repoRoot,
|
||||
fakeSrcRoot: context.fakeSrcRoot
|
||||
fakeSrcRoot: context.fakeSrcRoot,
|
||||
sdksExclude: sdksExclude.integrateArrayArguments
|
||||
)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,10 +31,12 @@ public struct PrepareMarkContext {
|
||||
let recommendedCacheAddress: URL
|
||||
/// All remote servers to mark
|
||||
let cacheAddresses: [URL]
|
||||
/// XCRemoteCache is explicitly disabled
|
||||
let disabled: Bool
|
||||
}
|
||||
|
||||
extension PrepareMarkContext {
|
||||
init(_ config: XCRemoteCacheConfig) throws {
|
||||
init(_ config: XCRemoteCacheConfig, env: [String: String]) throws {
|
||||
let sourceRoot = URL(fileURLWithPath: config.sourceRoot, isDirectory: true)
|
||||
repoRoot = URL(fileURLWithPath: config.repoRoot, relativeTo: sourceRoot)
|
||||
guard let address = URL(string: config.recommendedCacheAddress) else {
|
||||
@@ -43,5 +45,6 @@ extension PrepareMarkContext {
|
||||
}
|
||||
recommendedCacheAddress = address
|
||||
cacheAddresses = try config.cacheAddresses.map(URL.build)
|
||||
disabled = try env.readEnv(key: "XCRC_DISABLED") ?? false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,12 +47,17 @@ public class XCPrepareMark {
|
||||
let xcodeVersion: String
|
||||
do {
|
||||
config = try XCRemoteCacheConfigReader(env: env, fileReader: fileManager).readConfiguration()
|
||||
context = try PrepareMarkContext(config)
|
||||
context = try PrepareMarkContext(config, env: env)
|
||||
xcodeVersion = try xcode ?? XcodeProbeImpl(shell: shellGetStdout).read().buildVersion
|
||||
} catch {
|
||||
exit(1, "FATAL: Prepare initialization failed with error: \(error)")
|
||||
}
|
||||
|
||||
guard !context.disabled else {
|
||||
infoLog("XCRemoteCache explicitly disabled for marking.")
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let sessionFactory = DefaultURLSessionFactory(config: config)
|
||||
var awsV4Signature: AWSV4Signature?
|
||||
|
||||
@@ -148,6 +148,9 @@ 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
|
||||
}
|
||||
|
||||
extension XCRemoteCacheConfig {
|
||||
@@ -206,6 +209,8 @@ extension XCRemoteCacheConfig {
|
||||
merge.disableVFSOverlay = scheme.disableVFSOverlay ?? disableVFSOverlay
|
||||
merge.customRewriteEnvs = scheme.customRewriteEnvs ?? customRewriteEnvs
|
||||
merge.irrelevantDependenciesPaths = scheme.irrelevantDependenciesPaths ?? irrelevantDependenciesPaths
|
||||
merge.gracefullyHandleMissingCommonSha =
|
||||
scheme.gracefullyHandleMissingCommonSha ?? gracefullyHandleMissingCommonSha
|
||||
return merge
|
||||
}
|
||||
|
||||
@@ -273,6 +278,7 @@ struct ConfigFileScheme: Decodable {
|
||||
let disableVFSOverlay: Bool?
|
||||
let customRewriteEnvs: [String]?
|
||||
let irrelevantDependenciesPaths: [String]?
|
||||
let gracefullyHandleMissingCommonSha: Bool?
|
||||
|
||||
// Yams library doesn't support encoding strategy, see https://github.com/jpsim/Yams/issues/84
|
||||
enum CodingKeys: String, CodingKey {
|
||||
@@ -323,6 +329,7 @@ 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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ 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
|
||||
}
|
||||
|
||||
@@ -34,7 +34,15 @@ struct CanonicalRequest {
|
||||
if url.path.isEmpty {
|
||||
path = "/"
|
||||
} else {
|
||||
path = url.path
|
||||
if let escapedPath = url.path.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) {
|
||||
path = escapedPath
|
||||
} else {
|
||||
path = "/"
|
||||
printWarning("""
|
||||
Escaping the path \(url.path) failed and a placeholder is used instead. \
|
||||
Make sure the path doesn't contain invalid characters.
|
||||
""")
|
||||
}
|
||||
}
|
||||
return
|
||||
"\(httpMethod)\n" +
|
||||
|
||||
@@ -55,4 +55,11 @@ extension Dictionary where Key == String, Value == String {
|
||||
}
|
||||
return value == "YES"
|
||||
}
|
||||
|
||||
func readEnv(key: String) throws -> Bool? {
|
||||
guard let value = self[key] else {
|
||||
return nil
|
||||
}
|
||||
return value == "YES"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -229,6 +229,11 @@ struct XCPrepareMain: ParsableCommand {
|
||||
)
|
||||
var fakeSrcRoot: String
|
||||
|
||||
@Option(name: .customLong("sdks-exclude"), default: "", help: """
|
||||
comma separated list of sdks to not integrate XCRemoteCache (e.g. "watchos*, watchsimulator*")
|
||||
""", transform: nonEmptyString)
|
||||
var sdksExclude: String
|
||||
|
||||
|
||||
func run() throws {
|
||||
XCIntegrate(
|
||||
@@ -243,6 +248,7 @@ struct XCPrepareMain: ParsableCommand {
|
||||
consumerEligiblePlatforms: consumerEligiblePlatforms,
|
||||
lldbMode: lldbInit,
|
||||
fakeSrcRoot: fakeSrcRoot,
|
||||
sdksExclude: sdksExclude,
|
||||
output: output
|
||||
).main()
|
||||
}
|
||||
|
||||
@@ -159,4 +159,31 @@ class PostbuildContextTests: FileXCTestCase {
|
||||
|
||||
XCTAssertNil(context.publicHeadersFolderPath)
|
||||
}
|
||||
|
||||
func testDisabledEnvIsFalseByDefault() throws {
|
||||
var envs = Self.SampleEnvs
|
||||
envs.removeValue(forKey: "XCRC_DISABLED")
|
||||
|
||||
let context = try PostbuildContext(config, env: envs)
|
||||
|
||||
XCTAssertFalse(context.disabled)
|
||||
}
|
||||
|
||||
func testDisabledIsTrueForYesEnv() throws {
|
||||
var envs = Self.SampleEnvs
|
||||
envs["XCRC_DISABLED"] = "YES"
|
||||
|
||||
let context = try PostbuildContext(config, env: envs)
|
||||
|
||||
XCTAssertTrue(context.disabled)
|
||||
}
|
||||
|
||||
func testDisabledIsFalseForNonYesEnv() throws {
|
||||
var envs = Self.SampleEnvs
|
||||
envs["XCRC_DISABLED"] = "NO"
|
||||
|
||||
let context = try PostbuildContext(config, env: envs)
|
||||
|
||||
XCTAssertFalse(context.disabled)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,8 @@ class PostbuildTests: FileXCTestCase {
|
||||
modeMarkerPath: "",
|
||||
overlayHeadersPath: "",
|
||||
irrelevantDependenciesPaths: [],
|
||||
publicHeadersFolderPath: nil
|
||||
publicHeadersFolderPath: nil,
|
||||
disabled: false
|
||||
)
|
||||
private var network = RemoteNetworkClientImpl(
|
||||
NetworkClientFake(fileManager: .default),
|
||||
|
||||
@@ -63,7 +63,8 @@ class PrebuildTests: FileXCTestCase {
|
||||
compilationHistoryFile: compilationHistory,
|
||||
turnOffRemoteCacheOnFirstTimeout: true,
|
||||
targetName: "",
|
||||
overlayHeadersPath: ""
|
||||
overlayHeadersPath: "",
|
||||
disabled: false
|
||||
)
|
||||
contextCached = PrebuildContext(
|
||||
targetTempDir: sampleURL,
|
||||
@@ -76,7 +77,8 @@ class PrebuildTests: FileXCTestCase {
|
||||
compilationHistoryFile: compilationHistory,
|
||||
turnOffRemoteCacheOnFirstTimeout: true,
|
||||
targetName: "",
|
||||
overlayHeadersPath: ""
|
||||
overlayHeadersPath: "",
|
||||
disabled: false
|
||||
)
|
||||
organizer = ArtifactOrganizerFake(artifactRoot: artifactsRoot, unzippedExtension: "unzip")
|
||||
globalCacheSwitcher = InMemoryGlobalCacheSwitcher()
|
||||
@@ -241,7 +243,8 @@ class PrebuildTests: FileXCTestCase {
|
||||
compilationHistoryFile: compilationHistory,
|
||||
turnOffRemoteCacheOnFirstTimeout: true,
|
||||
targetName: "",
|
||||
overlayHeadersPath: ""
|
||||
overlayHeadersPath: "",
|
||||
disabled: false
|
||||
)
|
||||
|
||||
let prebuild = Prebuild(
|
||||
@@ -272,7 +275,8 @@ class PrebuildTests: FileXCTestCase {
|
||||
compilationHistoryFile: compilationHistory,
|
||||
turnOffRemoteCacheOnFirstTimeout: true,
|
||||
targetName: "",
|
||||
overlayHeadersPath: ""
|
||||
overlayHeadersPath: "",
|
||||
disabled: false
|
||||
)
|
||||
metaContent = try generateMeta(fingerprint: generator.generate(), filekey: "1")
|
||||
let downloadedArtifactPackage = artifactsRoot.appendingPathComponent("1")
|
||||
@@ -335,7 +339,8 @@ class PrebuildTests: FileXCTestCase {
|
||||
compilationHistoryFile: compilationHistory,
|
||||
turnOffRemoteCacheOnFirstTimeout: false,
|
||||
targetName: "",
|
||||
overlayHeadersPath: ""
|
||||
overlayHeadersPath: "",
|
||||
disabled: false
|
||||
)
|
||||
try globalCacheSwitcher.enable(sha: "1")
|
||||
let prebuild = Prebuild(
|
||||
@@ -353,4 +358,34 @@ class PrebuildTests: FileXCTestCase {
|
||||
|
||||
XCTAssertEqual(globalCacheSwitcher.state, .enabled(sha: "1"))
|
||||
}
|
||||
|
||||
func testReturnsDisabledIfXCRCExplicitlyDisabled() throws {
|
||||
contextNonCached = PrebuildContext(
|
||||
targetTempDir: sampleURL,
|
||||
productsDir: sampleURL,
|
||||
moduleName: nil,
|
||||
remoteCommit: .unavailable,
|
||||
remoteCommitLocation: sampleURL,
|
||||
recommendedCacheAddress: sampleURL,
|
||||
forceCached: false,
|
||||
compilationHistoryFile: compilationHistory,
|
||||
turnOffRemoteCacheOnFirstTimeout: true,
|
||||
targetName: "",
|
||||
overlayHeadersPath: "",
|
||||
disabled: true
|
||||
)
|
||||
|
||||
let prebuild = Prebuild(
|
||||
context: contextNonCached,
|
||||
networkClient: remoteNetwork,
|
||||
remapper: remapper,
|
||||
fingerprintAccumulator: generator,
|
||||
artifactsOrganizer: organizer,
|
||||
globalCacheSwitcher: globalCacheSwitcher,
|
||||
metaReader: metaReader,
|
||||
artifactConsumerPrebuildPlugins: []
|
||||
)
|
||||
|
||||
XCTAssertEqual(try prebuild.perform(), .disabled)
|
||||
}
|
||||
}
|
||||
|
||||
+78
-3
@@ -45,7 +45,12 @@ class XcodeProjBuildSettingsIntegrateAppenderTests: XCTestCase {
|
||||
func testProducerSettingFakeSrcRoot() throws {
|
||||
let mode: Mode = .producer
|
||||
let fakeRootURL: URL = "/xxxxxxxxxxP"
|
||||
let appender = XcodeProjBuildSettingsIntegrateAppender(mode: mode, repoRoot: rootURL, fakeSrcRoot: fakeRootURL)
|
||||
let appender = XcodeProjBuildSettingsIntegrateAppender(
|
||||
mode: mode,
|
||||
repoRoot: rootURL,
|
||||
fakeSrcRoot: fakeRootURL,
|
||||
sdksExclude: []
|
||||
)
|
||||
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
|
||||
let resultURL = try XCTUnwrap(result["XCRC_FAKE_SRCROOT"] as? String)
|
||||
|
||||
@@ -55,7 +60,12 @@ class XcodeProjBuildSettingsIntegrateAppenderTests: XCTestCase {
|
||||
func testConsumerSettingFakeSrcRoot() throws {
|
||||
let mode: Mode = .consumer
|
||||
let fakeRootURL: URL = "/xxxxxxxxxxC"
|
||||
let appender = XcodeProjBuildSettingsIntegrateAppender(mode: mode, repoRoot: rootURL, fakeSrcRoot: fakeRootURL)
|
||||
let appender = XcodeProjBuildSettingsIntegrateAppender(
|
||||
mode: mode,
|
||||
repoRoot: rootURL,
|
||||
fakeSrcRoot: fakeRootURL,
|
||||
sdksExclude: []
|
||||
)
|
||||
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
|
||||
let resultURL: String = try XCTUnwrap(result["XCRC_FAKE_SRCROOT"] as? String)
|
||||
|
||||
@@ -65,10 +75,75 @@ class XcodeProjBuildSettingsIntegrateAppenderTests: XCTestCase {
|
||||
func testConsumerSettingLdPlusPlus() throws {
|
||||
let mode: Mode = .consumer
|
||||
let fakeRootURL: URL = "/xxxxxxxxxxC"
|
||||
let appender = XcodeProjBuildSettingsIntegrateAppender(mode: mode, repoRoot: rootURL, fakeSrcRoot: fakeRootURL)
|
||||
let appender = XcodeProjBuildSettingsIntegrateAppender(
|
||||
mode: mode,
|
||||
repoRoot: rootURL,
|
||||
fakeSrcRoot: fakeRootURL,
|
||||
sdksExclude: []
|
||||
)
|
||||
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
|
||||
let ldPlusPlus: String = try XCTUnwrap(result["LDPLUSPLUS"] as? String)
|
||||
|
||||
XCTAssertEqual(ldPlusPlus, binaries.ldplusplus.path)
|
||||
}
|
||||
|
||||
func testSinglesdksExcludeIsAppended() throws {
|
||||
let mode: Mode = .consumer
|
||||
let appender = XcodeProjBuildSettingsIntegrateAppender(
|
||||
mode: mode,
|
||||
repoRoot: rootURL,
|
||||
fakeSrcRoot: "/",
|
||||
sdksExclude: ["watchOS*"]
|
||||
)
|
||||
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
|
||||
let ldPlusPlusWatchOS: String = try XCTUnwrap(result["LDPLUSPLUS[sdk=watchOS*]"] as? String)
|
||||
|
||||
XCTAssertEqual(ldPlusPlusWatchOS, "")
|
||||
}
|
||||
|
||||
func testLibtoolIsSetForExcludedSdks() throws {
|
||||
let mode: Mode = .consumer
|
||||
let appender = XcodeProjBuildSettingsIntegrateAppender(
|
||||
mode: mode,
|
||||
repoRoot: rootURL,
|
||||
fakeSrcRoot: "/",
|
||||
sdksExclude: ["watchOS*"]
|
||||
)
|
||||
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
|
||||
let libtoolWatchOS: String = try XCTUnwrap(result["LIBTOOL[sdk=watchOS*]"] as? String)
|
||||
|
||||
XCTAssertEqual(libtoolWatchOS, "libtool")
|
||||
}
|
||||
|
||||
func testMultiplesdksExcludeAreAppended() throws {
|
||||
let mode: Mode = .consumer
|
||||
let appender = XcodeProjBuildSettingsIntegrateAppender(
|
||||
mode: mode,
|
||||
repoRoot: rootURL,
|
||||
fakeSrcRoot: "/",
|
||||
sdksExclude: ["watchOS*", "watchsimulator*"]
|
||||
)
|
||||
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
|
||||
let ldPlusPlusWatchOS: String = try XCTUnwrap(result["LDPLUSPLUS[sdk=watchOS*]"] as? String)
|
||||
let ldPlusPlusWatchSimulator: String = try XCTUnwrap(result["LDPLUSPLUS[sdk=watchsimulator*]"] as? String)
|
||||
|
||||
XCTAssertEqual(ldPlusPlusWatchOS, "")
|
||||
XCTAssertEqual(ldPlusPlusWatchSimulator, "")
|
||||
}
|
||||
|
||||
func testAddsDisabledFlagForExcludedSDKs() throws {
|
||||
let mode: Mode = .consumer
|
||||
let appender = XcodeProjBuildSettingsIntegrateAppender(
|
||||
mode: mode,
|
||||
repoRoot: rootURL,
|
||||
fakeSrcRoot: "/",
|
||||
sdksExclude: ["watchOS*", "watchsimulator*"]
|
||||
)
|
||||
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
|
||||
let disabledWatchOS: String = try XCTUnwrap(result["XCRC_DISABLED[sdk=watchOS*]"] as? String)
|
||||
let disabledWatchSimulator: String = try XCTUnwrap(result["XCRC_DISABLED[sdk=watchsimulator*]"] as? String)
|
||||
|
||||
XCTAssertEqual(disabledWatchOS, "YES")
|
||||
XCTAssertEqual(disabledWatchSimulator, "YES")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ class PrepareMarkContextTests: XCTestCase {
|
||||
let repoPath = "/AbsolutePath"
|
||||
config.repoRoot = repoPath
|
||||
|
||||
let context = try PrepareMarkContext(config)
|
||||
let context = try PrepareMarkContext(config, env: [:])
|
||||
|
||||
XCTAssertEqual(context.repoRoot.path, repoPath)
|
||||
}
|
||||
@@ -43,8 +43,20 @@ class PrepareMarkContextTests: XCTestCase {
|
||||
let repoPath = "."
|
||||
config.repoRoot = repoPath
|
||||
|
||||
let context = try PrepareMarkContext(config)
|
||||
let context = try PrepareMarkContext(config, env: [:])
|
||||
|
||||
XCTAssertEqual(context.repoRoot.path, "/Root")
|
||||
}
|
||||
|
||||
func testDisabledIsFalseByDefault() throws {
|
||||
let context = try PrepareMarkContext(config, env: [:])
|
||||
|
||||
XCTAssertFalse(context.disabled)
|
||||
}
|
||||
|
||||
func testDisabledIsTrueForYes() throws {
|
||||
let context = try PrepareMarkContext(config, env: ["XCRC_DISABLED": "YES"])
|
||||
|
||||
XCTAssertTrue(context.disabled)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,27 @@ class CanonicalRequestTest: XCTestCase {
|
||||
}
|
||||
|
||||
func testCanonicalRequest() {
|
||||
request.url = URL(
|
||||
string: "https://region.amazonaws.com/bucket/with%20space?param=value&hej=hej&abd=cde&test=-_.~"
|
||||
)!
|
||||
let canonicalRequest = CanonicalRequest(
|
||||
request: request
|
||||
)
|
||||
XCTAssertEqual(
|
||||
canonicalRequest.value,
|
||||
"GET\n" +
|
||||
"/bucket/with%20space\n" +
|
||||
"abd=cde&hej=hej¶m=value&test=-_.~\n" +
|
||||
"a-header2key:A-Header2Value\n" +
|
||||
"b-header3key:B-Header3Value\n" +
|
||||
"c-header4key:C Header 4 Value\n" +
|
||||
"x-header1key:X-Header1Value\n\n" +
|
||||
"a-header2key;b-header3key;c-header4key;x-header1key\n" +
|
||||
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
)
|
||||
}
|
||||
|
||||
func testCanonicalRequestWithEmptySpaceInPath() {
|
||||
let canonicalRequest = CanonicalRequest(
|
||||
request: request
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -28,7 +28,7 @@ plugin 'cocoapods-xcremotecache'
|
||||
2. Configure XCRemoteCache at the top of your `Podfile` definition:
|
||||
```ruby
|
||||
xcremotecache({
|
||||
'cache_addresses' => ['http://localhost:8080/cache/pods'],
|
||||
'cache_addresses' => ['http://localhost:8080/cache/pods'],
|
||||
'primary_repo' => 'https://your.primary.repo.git',
|
||||
'mode' => 'consumer'
|
||||
})
|
||||
@@ -48,13 +48,14 @@ An object that is passed to the `xcremotecache` can contain all properties suppo
|
||||
| `exclude_build_configurations` | Comma-separated list of configurations that shouldn't use XCRemoteCache | `[]`| ⬜️ |
|
||||
| `final_target` | A target name that is build at the end of the build chain. Relevant only for a 'producer' mode to mark a given sha as ready to use from cache | `Debug` | ⬜️ |
|
||||
| `check_build_configuration` | A build configuration for which the remote cache availability is performed. Relevant only for a 'consumer' mode | `Debug` | ⬜️ |
|
||||
| `check_platform` | A platform for which the remote cache availability is performed. Relevant only for a 'consumer' mode | `iphonesimulator` | ⬜️
|
||||
| `check_platform` | A platform for which the remote cache availability is performed. Relevant only for a 'consumer' mode | `iphonesimulator` | ⬜️
|
||||
| `modify_lldb_init` | Controls if the pod integration should modify `~/.lldbinit` | `true` | ⬜️ |
|
||||
| `xccc_file` | The path where should be placed the `xccc` binary (in the pod installation phase) | `{podfile_dir}/.rc/xccc` | ⬜️ |
|
||||
| `remote_commit_file` | The path of the file with the remote commit sha (in the pod installation phase) | `{podfile_dir}/.rc/arc.rc`| ⬜️ |
|
||||
| `prettify_meta_files` | A Boolean value that opts-in pretty JSON formatting for meta files | `false` | ⬜️ |
|
||||
| `fake_src_root` | An arbitrary source location shared between producers and consumers. Should be unique for a project. | `/xxxxxxxxxx` | ⬜️ |
|
||||
| `disable_certificate_verification` | A Boolean value that opts-in SSL certificate validation is disabled | `false` | ⬜️ |
|
||||
| `exclude_sdks_configurations` | array of sdks to not integrate XCRemoteCache (e.g. "watchos*, watchsimulator*") (Experimental) | `[]`| ⬜️ |
|
||||
|
||||
## Uninstalling
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ module CocoapodsXCRemoteCacheModifier
|
||||
'check_platform',
|
||||
'modify_lldb_init',
|
||||
'fake_src_root',
|
||||
'exclude_sdks_configurations'
|
||||
]
|
||||
|
||||
class XCRemoteCache
|
||||
@@ -64,7 +65,8 @@ module CocoapodsXCRemoteCacheModifier
|
||||
'prettify_meta_files' => false,
|
||||
'fake_src_root' => "/#{'x' * 10 }",
|
||||
'disable_certificate_verification' => false,
|
||||
'custom_rewrite_envs' => []
|
||||
'custom_rewrite_envs' => [],
|
||||
'exclude_sdks_configurations' => []
|
||||
}
|
||||
@@configuration.merge! default_values.select { |k, v| !@@configuration.key?(k) }
|
||||
# Always include XCRC_COOCAPODS_ROOT_KEY in custom_rewrite_envs
|
||||
@@ -111,7 +113,18 @@ module CocoapodsXCRemoteCacheModifier
|
||||
# @param mode [String] mode name ('consumer', 'producer', 'producer-fast' etc.)
|
||||
# @param exclude_build_configurations [String[]] list of targets that should have disabled remote cache
|
||||
# @param final_target [String] name of target that should trigger marking
|
||||
def self.enable_xcremotecache(target, repo_distance, xc_location, xc_cc_path, mode, exclude_build_configurations, final_target, fake_src_root)
|
||||
# @param exclude_sdks_configurations [String[]] list of sdks that should have disabled remote cache
|
||||
def self.enable_xcremotecache(
|
||||
target,
|
||||
repo_distance,
|
||||
xc_location,
|
||||
xc_cc_path,
|
||||
mode,
|
||||
exclude_build_configurations,
|
||||
final_target,
|
||||
fake_src_root,
|
||||
exclude_sdks_configurations
|
||||
)
|
||||
srcroot_relative_xc_location = parent_dir(xc_location, repo_distance)
|
||||
# location of the entrite CocoaPods project, relative to SRCROOT
|
||||
srcroot_relative_project_location = parent_dir('', repo_distance)
|
||||
@@ -120,23 +133,27 @@ module CocoapodsXCRemoteCacheModifier
|
||||
# apply only for relevant Configurations
|
||||
next if exclude_build_configurations.include?(config.name)
|
||||
if mode == 'consumer'
|
||||
config.build_settings['CC'] = ["$SRCROOT/#{parent_dir(xc_cc_path, repo_distance)}"]
|
||||
reset_build_setting(config.build_settings, 'CC', "$SRCROOT/#{parent_dir(xc_cc_path, repo_distance)}", exclude_sdks_configurations)
|
||||
elsif mode == 'producer' || mode == 'producer-fast'
|
||||
config.build_settings.delete('CC') if config.build_settings.key?('CC')
|
||||
end
|
||||
config.build_settings['SWIFT_EXEC'] = ["$SRCROOT/#{srcroot_relative_xc_location}/xcswiftc"]
|
||||
config.build_settings['LIBTOOL'] = ["$SRCROOT/#{srcroot_relative_xc_location}/xclibtool"]
|
||||
config.build_settings['LD'] = ["$SRCROOT/#{srcroot_relative_xc_location}/xcld"]
|
||||
config.build_settings['LDPLUSPLUS'] = ["$SRCROOT/#{srcroot_relative_xc_location}/xcldplusplus"]
|
||||
config.build_settings['LIPO'] = ["$SRCROOT/#{srcroot_relative_xc_location}/xclipo"]
|
||||
config.build_settings['SWIFT_USE_INTEGRATED_DRIVER'] = ['NO']
|
||||
reset_build_setting(config.build_settings, 'SWIFT_EXEC', "$SRCROOT/#{srcroot_relative_xc_location}/xcswiftc", 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)
|
||||
|
||||
config.build_settings['XCREMOTE_CACHE_FAKE_SRCROOT'] = fake_src_root
|
||||
config.build_settings['XCRC_PLATFORM_PREFERRED_ARCH'] = ["$(LINK_FILE_LIST_$(CURRENT_VARIANT)_$(PLATFORM_PREFERRED_ARCH):dir:standardizepath:file:default=arm64)"]
|
||||
config.build_settings[XCRC_COOCAPODS_ROOT_KEY] = ["$SRCROOT/#{srcroot_relative_project_location}"]
|
||||
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)
|
||||
reset_build_setting(config.build_settings, XCRC_COOCAPODS_ROOT_KEY, "$SRCROOT/#{srcroot_relative_project_location}", exclude_sdks_configurations)
|
||||
debug_prefix_map_replacement = '$(SRCROOT' + ':dir:standardizepath' * repo_distance + ')'
|
||||
add_cflags!(config.build_settings, '-fdebug-prefix-map', "#{debug_prefix_map_replacement}=$(XCREMOTE_CACHE_FAKE_SRCROOT)")
|
||||
add_swiftflags!(config.build_settings, '-debug-prefix-map', "#{debug_prefix_map_replacement}=$(XCREMOTE_CACHE_FAKE_SRCROOT)")
|
||||
add_cflags!(config.build_settings, '-fdebug-prefix-map', "#{debug_prefix_map_replacement}=$(XCREMOTE_CACHE_FAKE_SRCROOT)", exclude_sdks_configurations)
|
||||
add_swiftflags!(config.build_settings, '-debug-prefix-map', "#{debug_prefix_map_replacement}=$(XCREMOTE_CACHE_FAKE_SRCROOT)", exclude_sdks_configurations)
|
||||
delete_build_setting(config.build_settings, 'XCRC_DISABLED')
|
||||
add_build_setting_for_sdks(config.build_settings, 'XCRC_DISABLED', 'YES', exclude_sdks_configurations)
|
||||
end
|
||||
|
||||
# Prebuild
|
||||
@@ -283,9 +300,8 @@ module CocoapodsXCRemoteCacheModifier
|
||||
end
|
||||
end
|
||||
|
||||
def self.add_cflags!(options, key, value)
|
||||
return if options.fetch('OTHER_CFLAGS',[]).include?(value)
|
||||
options['OTHER_CFLAGS'] = remove_cflags!(options, key) << "#{key}=#{value}"
|
||||
def self.add_cflags!(options, key, value, exclude_sdks_configurations)
|
||||
reset_build_setting(options, 'OTHER_CFLAGS', remove_cflags!(options, key) << "#{key}=#{value}", exclude_sdks_configurations)
|
||||
end
|
||||
|
||||
def self.remove_cflags!(options, key)
|
||||
@@ -295,9 +311,8 @@ module CocoapodsXCRemoteCacheModifier
|
||||
options['OTHER_CFLAGS']
|
||||
end
|
||||
|
||||
def self.add_swiftflags!(options, key, value)
|
||||
return if options.fetch('OTHER_SWIFT_FLAGS','').include?(value)
|
||||
options['OTHER_SWIFT_FLAGS'] = remove_swiftflags!(options, key) + " #{key} #{value}"
|
||||
def self.add_swiftflags!(options, key, value, exclude_sdks_configurations)
|
||||
reset_build_setting(options, 'OTHER_SWIFT_FLAGS', remove_swiftflags!(options, key) + " #{key} #{value}", exclude_sdks_configurations)
|
||||
end
|
||||
|
||||
def self.remove_swiftflags!(options, key)
|
||||
@@ -305,6 +320,34 @@ module CocoapodsXCRemoteCacheModifier
|
||||
options['OTHER_SWIFT_FLAGS']
|
||||
end
|
||||
|
||||
def self.add_build_setting(build_settings, key, value, exclude_sdks_configurations)
|
||||
build_settings[key] = value
|
||||
for exclude_sdks_configuration in exclude_sdks_configurations
|
||||
build_settings["#{key}[sdk=#{exclude_sdks_configuration}]"] = [""]
|
||||
end
|
||||
end
|
||||
|
||||
# Deletes all previous build settings for a key, and sets a new value to all configurations
|
||||
# but the sdks in exclude_sdks_configurations
|
||||
def self.reset_build_setting(build_settings, key, value, exclude_sdks_configurations)
|
||||
delete_build_setting(build_settings, key)
|
||||
add_build_setting(build_settings, key, value, exclude_sdks_configurations)
|
||||
end
|
||||
|
||||
# Delete all build setting for a key, including settings like "[skd=*,arch=*]"
|
||||
def self.delete_build_setting(build_settings, key)
|
||||
for build_setting_key in build_settings.keys
|
||||
build_settings.delete(build_setting_key) if build_setting_key == key || build_setting_key.start_with?("#{key}[")
|
||||
end
|
||||
end
|
||||
|
||||
# Sets value for a key only for a subset of sdk configurations
|
||||
def self.add_build_setting_for_sdks(build_settings, key, value, sdk_configurations)
|
||||
for sdk_configuration in sdk_configurations
|
||||
build_settings["#{key}[sdk=#{sdk_configuration}]"] = value
|
||||
end
|
||||
end
|
||||
|
||||
# Uninstall the XCRemoteCache
|
||||
def self.disable_xcremotecache(user_project, pods_project = nil)
|
||||
user_project.targets.each do |target|
|
||||
@@ -454,6 +497,7 @@ module CocoapodsXCRemoteCacheModifier
|
||||
check_build_configuration = @@configuration['check_build_configuration']
|
||||
check_platform = @@configuration['check_platform']
|
||||
fake_src_root = @@configuration['fake_src_root']
|
||||
exclude_sdks_configurations = @@configuration['exclude_sdks_configurations'] || []
|
||||
|
||||
xccc_location_absolute = "#{user_proj_directory}/#{xccc_location}"
|
||||
xcrc_location_absolute = "#{user_proj_directory}/#{xcrc_location}"
|
||||
@@ -477,7 +521,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)
|
||||
enable_xcremotecache(target, 1, xcrc_location, xccc_location, mode, exclude_build_configurations, final_target,fake_src_root, exclude_sdks_configurations)
|
||||
end
|
||||
|
||||
# Create .rcinfo into `Pods` directory as that .xcodeproj reads configuration from .xcodeproj location
|
||||
@@ -490,7 +534,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)
|
||||
enable_xcremotecache(target, 1, xcrc_location, xccc_location, mode, exclude_build_configurations, final_target,fake_src_root, exclude_sdks_configurations)
|
||||
end
|
||||
generated_project.save()
|
||||
end
|
||||
@@ -531,7 +575,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)
|
||||
enable_xcremotecache(target, 0, xcrc_location, xccc_location, mode, exclude_build_configurations, final_target,fake_src_root, exclude_sdks_configurations)
|
||||
end
|
||||
|
||||
# Set Target sourcemap
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
# limitations under the License.
|
||||
|
||||
module CocoapodsXcremotecache
|
||||
VERSION = "0.0.15"
|
||||
VERSION = "0.0.16"
|
||||
end
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
### How to add a CocoaPods scenario
|
||||
|
||||
In order to run different test suites in cocoapods integration, you can create multiple `.Podfile` files as a "starting point" of the E2E test.
|
||||
|
||||
#### Files organization
|
||||
|
||||
1. File `{SUITE_NAME}.Podfile` contains a base Podfile that should be excercised
|
||||
1. [Optional] File `{SUITE_NAME}.Podfile.config` contains a hash of extra xcremote-cache-plugin parameters that should be added to the `xcremotecache()` configuration in a podfile
|
||||
1. [Optional] File `{SUITE_NAME}.Podfile.expectations` overrides a set of default validation steps that should be checked after consumer's build. Supported steps are: `hits`, `misses` or `hit_rate` (e.g. `100` for 100$). By default, 100% `hit_rate` and 0 `misses` are used - if you want to skip a validation step for a given suite, you can set it to `null` in a dictionary
|
||||
@@ -0,0 +1,8 @@
|
||||
plugin 'cocoapods-xcremotecache'
|
||||
|
||||
target 'XCRemoteCacheSample' do
|
||||
use_frameworks!
|
||||
|
||||
pod 'Firebase/Analytics'
|
||||
pod 'ReactiveSwift'
|
||||
end
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"exclude_sdks_configurations": ["iphoneos*", "iphonesimulator*"]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"hits": 0,
|
||||
"misses": 0,
|
||||
"hit_rate": null
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
plugin 'cocoapods-xcremotecache'
|
||||
|
||||
target 'XCRemoteCacheSample' do
|
||||
use_frameworks!
|
||||
|
||||
pod 'Firebase/Analytics'
|
||||
pod 'ReactiveSwift'
|
||||
end
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"exclude_sdks_configurations": ["watchos*", "watchsimulator*"]
|
||||
}
|
||||
+58
-23
@@ -1,7 +1,12 @@
|
||||
require 'json'
|
||||
require "ostruct"
|
||||
|
||||
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,7 +26,11 @@ namespace :e2e do
|
||||
'mode' => 'consumer',
|
||||
'final_target' => 'XCRemoteCacheSample',
|
||||
'artifact_maximum_age' => 0
|
||||
}
|
||||
}.freeze
|
||||
DEFAULT_EXPECTATIONS = {
|
||||
'misses' => 0,
|
||||
'hit_rate' => 100
|
||||
}.freeze
|
||||
|
||||
Stats = Struct.new(:hits, :misses, :hit_rate)
|
||||
|
||||
@@ -56,10 +65,10 @@ namespace :e2e do
|
||||
clean_git
|
||||
# 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
|
||||
|
||||
@@ -73,16 +82,16 @@ namespace :e2e do
|
||||
|
||||
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
|
||||
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"})
|
||||
build_project(nil, "StandaloneApp.xcodeproj", 'StandaloneApp', 'iphone', 'iOS', CONFIGURATION, {'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"})
|
||||
build_project(nil, "StandaloneApp.xcodeproj", 'WatchExtension', 'watch', 'watchOS', CONFIGURATION, {'derivedDataPath' => "#{DERIVED_DATA_PATH}_consumer_local"})
|
||||
build_project(nil, "StandaloneApp.xcodeproj", 'StandaloneApp', 'iphone', 'iOS', CONFIGURATION, {'derivedDataPath' => "#{DERIVED_DATA_PATH}_consumer_local"})
|
||||
end
|
||||
|
||||
# Revert all side effects
|
||||
@@ -162,12 +171,12 @@ 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,
|
||||
@@ -188,9 +197,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
|
||||
@@ -198,22 +207,33 @@ namespace :e2e do
|
||||
misses = stats_json_string.fetch('miss_count', 0)
|
||||
hits = stats_json_string.fetch('hit_count', 0)
|
||||
all_targets = misses + hits
|
||||
raise "Failure: No XCRemoteCache targets invoked" if all_targets == 0
|
||||
hit_rate = hits * 100 / all_targets
|
||||
hit_rate = all_targets == 0 ? nil : hits * 100 / all_targets
|
||||
Stats.new(hits, misses, hit_rate)
|
||||
end
|
||||
|
||||
# validate 100% hit rate
|
||||
def self.valide_hit_rate
|
||||
def self.valide_hit_rate(expectations)
|
||||
status = read_stats()
|
||||
all_targets = status.misses + status.hits
|
||||
raise "Failure: Hit rate is only #{status.hit_rate}% (#{all_targets})" if status.misses > 0
|
||||
unless expectations.misses.nil?
|
||||
raise "Failure: Unexpected misses: #{status.misses} (#{all_targets}). Expected #{expectations.misses}" if status.misses != expectations.misses
|
||||
end
|
||||
unless expectations.hit_rate.nil?
|
||||
raise "Failure: Hit rate is #{status.hit_rate}% (#{all_targets}). Expected #{expectations.hit_rate}%" if status.hit_rate != expectations.hit_rate
|
||||
end
|
||||
unless expectations.hits.nil?
|
||||
raise "Failure: Hits count is #{status.hit_rate}% (#{all_targets}). Expected #{expectations.hits}" if status.hits != expectations.hits
|
||||
end
|
||||
puts("Hit rate: #{status.hit_rate}% (#{status.hits}/#{all_targets})")
|
||||
end
|
||||
|
||||
def self.run_cocoapods_scenario(template_path)
|
||||
producer_configuration = cocoapods_configuration_string({'mode' => 'producer'})
|
||||
consumer_configuration = cocoapods_configuration_string()
|
||||
# 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)
|
||||
expectations = build_expectations(template_path)
|
||||
|
||||
puts("****** Scenario: #{template_path}")
|
||||
|
||||
@@ -222,7 +242,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
|
||||
@@ -232,8 +252,8 @@ 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"})
|
||||
valide_hit_rate
|
||||
build_project_cocoapods('iphone', 'iOS', CONFIGURATION, {'derivedDataPath' => "#{DERIVED_DATA_PATH}_consumer"})
|
||||
valide_hit_rate(expectations)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -241,4 +261,19 @@ namespace :e2e do
|
||||
clean_git
|
||||
system("ln -s $(pwd)/releases #{dir}/#{XCRC_BINARIES}")
|
||||
end
|
||||
|
||||
# Returns a hash of all expectations that should be validated for a template
|
||||
# The implementation assumes 100% hitrate and extra expecations can be provided in an optional file
|
||||
# #{template_path}.expectations
|
||||
def self.build_expectations(template_path)
|
||||
expectations = DEFAULT_EXPECTATIONS.dup
|
||||
return expectations if template_path.nil?
|
||||
|
||||
template_config_path = "#{template_path}.expectations"
|
||||
if File.exist?(template_config_path)
|
||||
extra_config = File.exist?(template_config_path) ? JSON.load(File.read(template_config_path)) : {}
|
||||
expectations.merge!(extra_config)
|
||||
end
|
||||
OpenStruct.new(expectations)
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user