Compare commits

...

39 Commits

Author SHA1 Message Date
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
Bartosz Polaczyk dfb4039404 Merge pull request #198 from polac24/bartosz/20230413-override-exclusions
Set libtool Build Setting for excluded sdks
2023-04-14 20:21:37 -07:00
Bartosz Polaczyk b28613a2ef Fix formatting 2023-04-13 18:47:54 -07:00
Bartosz Polaczyk 1535b762bc Cleanup unnecessary unless 2023-04-13 18:33:10 -07:00
Bartosz Polaczyk e6816846c3 Add original LIBTOOL to excluded SDKs 2023-04-13 18:27:27 -07:00
Bartosz Polaczyk b89d98f411 Reset LIBTOOL to libtool for excluded sdks 2023-04-12 17:43:22 -07:00
Bartosz Polaczyk a0d3d1b0b9 Reset SWIFT_EXEC to swiftc for excluded sdks 2023-04-11 22:33:20 -07:00
Bartosz Polaczyk f432917505 Merge pull request #197 from polac24/fix-exclude-sdk
Reset cflags and swiftflags on each cocoapods integration
2023-04-11 18:49:32 -07:00
Bartosz Polaczyk 5d297a4fb2 Revert incorrect OTHER_SWIFT_FLAGS format 2023-04-10 22:04:45 -07:00
Bartosz Polaczyk b0d5f1660e Reset cflags and swiftflags on each cocoapods integration 2023-04-10 19:30:32 -07:00
Bartosz Polaczyk baea2de79a Merge pull request #196 from polac24/exclude-sdks
Add support for excluded sdks
2023-04-10 19:20:09 -07:00
Bartosz Polaczyk d2803f4ad5 Fix linter 2023-04-09 11:40:46 -07:00
Bartosz Polaczyk ab017367b2 Add disabled support in Marking 2023-04-09 11:35:50 -07:00
Bartosz Polaczyk 30cb648641 Add disabled support in XCPostbuild 2023-04-09 11:06:50 -07:00
Bartosz Polaczyk 3330ca45f8 Cleanup README.md 2023-04-09 09:33:42 -07:00
Bartosz Polaczyk d3193b15a8 Fix hook 2023-04-09 09:30:42 -07:00
Bartosz Polaczyk 7b14f2c9ff Revert e2e cleanups 2023-04-09 09:24:24 -07:00
Bartosz Polaczyk 4933b454a5 Reset build settings in cocoapods 2023-04-09 09:23:49 -07:00
Bartosz Polaczyk 79ffdce295 Fix standalone validation 2023-04-08 20:03:30 -07:00
Bartosz Polaczyk 376e6a17c5 Fix linting 2023-04-08 19:47:42 -07:00
Bartosz Polaczyk a4d1849821 Fix linting 2023-04-08 19:36:35 -07:00
Bartosz Polaczyk 325fb07080 Add explicit switch 2023-04-08 19:27:58 -07:00
Bartosz Polaczyk a2afe62751 Fix configs 2023-04-08 17:51:09 -07:00
Bartosz Polaczyk bbaa374e12 Fix configs jsons 2023-04-08 17:46:41 -07:00
Bartosz Polaczyk 39a259ff49 Fix hook 2023-04-08 17:39:12 -07:00
Bartosz Polaczyk a2b9cbf332 Add cocoapods E2E tests 2023-04-08 17:29:35 -07:00
Bartosz Polaczyk 5710594bc4 Bump version to 0.0.16 2023-04-08 04:37:35 -07:00
Bartosz Polaczyk 7d123792b8 Add cocoapods support 2023-04-08 04:37:14 -07:00
Bartosz Polaczyk 5856dbec77 Add skippedSDKs 2023-04-08 04:19:28 -07:00
Bartosz Polaczyk 600310f44b Merge pull request #194 from polac24/space-aws
Encode path in AWS V4 signing requests
2023-04-03 07:24:02 -07:00
Bartosz Polaczyk aae2b3289c Use addingPercentEncoding for path escaping 2023-03-31 18:33:39 -07:00
Bartosz Polaczyk 11eabdab3d Merge pull request #188 from polac24/bartosz/20230310-lipo
Introduce xclipo to mock lipo
2023-03-21 09:55:30 -04:00
Bartosz Polaczyk de24e609ef Delete leftover comment 2023-03-11 10:34:24 -05:00
Bartosz Polaczyk 850983cbde Fix formatting 2023-03-11 10:21:42 -05:00
Bartosz Polaczyk 0064335cc7 Introduce WatchApp to the Standalone E2E project 2023-03-11 10:16:47 -05:00
Bartosz Polaczyk 1ddadcb361 Add xclipo 2023-03-11 10:15:29 -05:00
49 changed files with 991 additions and 125 deletions
+2 -2
View File
@@ -51,8 +51,8 @@
"repositoryURL": "https://github.com/tuist/XcodeProj.git",
"state": {
"branch": null,
"revision": "c75c3acc25460195cfd203a04dde165395bf00e0",
"version": "8.7.1"
"revision": "fae27b48bc14ff3fd9b02902e48c4665ce5a0793",
"version": "8.9.0"
}
},
{
+15 -2
View File
@@ -16,7 +16,7 @@ let package = Package(
.package(url: "https://github.com/marmelroy/Zip.git", from: "2.1.2"),
.package(url: "https://github.com/jpsim/Yams.git", from: "5.0.0"),
.package(url: "https://github.com/apple/swift-argument-parser", from: "0.0.1"),
.package(url: "https://github.com/tuist/XcodeProj.git", from: "8.7.1"),
.package(url: "https://github.com/tuist/XcodeProj.git", from: "8.9.0"),
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
],
targets: [
@@ -40,6 +40,10 @@ let package = Package(
name: "xclibtool",
dependencies: ["XCRemoteCache", "xclibtoolSupport"]
),
.target(
name: "xclipo",
dependencies: ["XCRemoteCache"]
),
.target(
name: "xcpostbuild",
dependencies: ["XCRemoteCache"]
@@ -62,7 +66,16 @@ let package = Package(
.target(
// Wrapper target that builds all binaries but does nothing in runtime
name: "Aggregator",
dependencies: ["xcprebuild", "xcswiftc", "xclibtool", "xcpostbuild", "xcprepare", "xcld", "xcldplusplus"]
dependencies: [
"xcprebuild",
"xcswiftc",
"xclibtool",
"xcpostbuild",
"xcprepare",
"xcld",
"xcldplusplus",
"xclipo",
]
),
.testTarget(
name: "XCRemoteCacheTests",
+4 -1
View File
@@ -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>
@@ -195,6 +196,7 @@ Configure Xcode targets that **should use** XCRemoteCache:
* `CC` - `xccc_file` from your `.rcinfo` configuration (e.g. `xcremotecache/xccc`)
* `SWIFT_EXEC` - location of `xcprepare` (e.g. `xcremotecache/xcswiftc`)
* `LIBTOOL` - location of `xclibtool` (e.g. `xcremotecache/xclibtool`)
* `LIPO` - location of `xclipo` (e.g. `xcremotecache/xclipo`)
* `LD` - location of `xcld` (e.g. `xcremotecache/xcld`)
* `LDPLUSPLUS` - location of `xcldplusplus` (e.g. `xcremotecache/xcldplusplus`)
* `XCRC_PLATFORM_PREFERRED_ARCH` - `$(LINK_FILE_LIST_$(CURRENT_VARIANT)_$(PLATFORM_PREFERRED_ARCH):dir:standardizepath:file:default=arm64)`
@@ -267,7 +269,7 @@ $ xcremotecache/xcprepare mark --configuration Debug --platform iphonesimulator
That command creates an empty file on a remote server which informs that for given sha, configuration, platform, Xcode versions etc. all artifacts are available.
_Note that for the `producer` mode, the prebuild build phase and `xccc`, `xcld`, `xcldplusplus`, `xclibtool` wrappers become no-op, so it is recommended to not add them for the `producer` mode._
_Note that for the `producer` mode, the prebuild build phase and `xccc`, `xcld`, `xcldplusplus`, `xclibtool`, `xclipo` wrappers become no-op, so it is recommended to not add them for the `producer` mode._
##### 7. Generalize `-Swift.h` (Optional only if using static library with a bridging header with public `NS_ENUM` exposed from ObjC)
@@ -356,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
+1 -1
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']
EXECUTABLE_NAMES = ['xclibtool', 'xcpostbuild', 'xcprebuild', 'xcprepare', 'xcswiftc', 'xcld', 'xcldplusplus', 'xclipo']
PROJECT_NAME = 'XCRemoteCache'
SWIFTLINT_ENABLED = true
@@ -19,31 +19,42 @@
import Foundation
enum XCLibtoolCreateUniversalBinaryError: Error {
enum XCCreateUniversalBinaryError: Error {
/// Missing ar libraries that should constitute an universal build
case missingInputLibrary
}
/// Wrapper for `libtool` call for creating an universal binary
class XCLibtoolCreateUniversalBinary: XCLibtoolLogic {
/// Wrapper for `libtool`/`lipo` call for creating an universal binary
class XCCreateUniversalBinary: XCLibtoolLogic {
private let output: URL
private let tempDir: URL
private let firstInputURL: URL
private let toolName: String
private let fallbackCommand: String
init(output: String, inputs: [String]) throws {
init(
output: String,
inputs: [String],
toolName: String,
fallbackCommand: String
) throws {
self.output = URL(fileURLWithPath: output)
guard let firstInput = inputs.first else {
throw XCLibtoolCreateUniversalBinaryError.missingInputLibrary
throw XCCreateUniversalBinaryError.missingInputLibrary
}
let firstInputURL = URL(fileURLWithPath: firstInput)
// inputs are place in $TARGET_TEMP_DIR/Objects-normal/$ARCH/Binary/$TARGET_NAME.a
// TODO: find better (stable) technique to determine `$TARGET_TEMP_DIR`
errorLog("\(firstInputURL.absoluteString)")
tempDir = firstInputURL
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
self.firstInputURL = firstInputURL
self.toolName = toolName
self.fallbackCommand = fallbackCommand
}
func run() {
@@ -55,7 +66,7 @@ class XCLibtoolCreateUniversalBinary: XCLibtoolLogic {
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileReader: fileManager)
.readConfiguration()
} catch {
errorLog("Libtool initialization failed with error: \(error). Fallbacking to libtool")
errorLog("\(toolName) initialization failed with error: \(error). Fallbacking to \(fallbackCommand)")
fallbackToDefault()
}
@@ -74,22 +85,21 @@ class XCLibtoolCreateUniversalBinary: XCLibtoolLogic {
// that these are already an universal binary
try fileManager.spt_forceLinkItem(at: firstInputURL, to: output)
} catch {
errorLog("Libtool failed with error: \(error). Fallbacking to libtool")
errorLog("\(toolName) failed with error: \(error). Fallbacking to \(fallbackCommand)")
do {
try fileManager.removeItem(at: markerURL)
fallbackToDefault()
} catch {
exit(1, "FATAL: libtool failed with error: \(error)")
exit(1, "FATAL: \(fallbackCommand) failed with error: \(error)")
}
}
}
private func fallbackToDefault() -> Never {
let args = ProcessInfo().arguments
let command = "libtool"
let paramList = [command] + args.dropFirst()
let paramList = [fallbackCommand] + args.dropFirst()
let cargs = paramList.map { strdup($0) } + [nil]
execvp(command, cargs)
execvp(fallbackCommand, cargs)
/// C-function `execv` returns only when the command fails
exit(1)
@@ -44,7 +44,12 @@ public class XCLibtool {
stepDescription: "Libtool"
)
case .createUniversalBinary(let output, let inputs):
logic = try XCLibtoolCreateUniversalBinary(output: output, inputs: inputs)
logic = try XCCreateUniversalBinary(
output: output,
inputs: inputs,
toolName: "Libtool",
fallbackCommand: "libtool"
)
}
}
@@ -0,0 +1,50 @@
// 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
/// Wrapper for a `lipo` tool that creates a fat archive
public class XCLipo {
private let logic: XCLibtoolLogic
public init(
output: String,
inputs: [String],
fallbackCommand: String,
stepDescription: String
) throws {
errorLog("\(output)")
errorLog("\(inputs.joined(separator: ","))")
logic = try XCCreateUniversalBinary(
output: output,
inputs: inputs,
toolName: stepDescription,
fallbackCommand: fallbackCommand
)
}
/// Handles a `-create` action which is responsible to create a fat archive
/// If remote cache can reuse artifacts from a remote cache, it just links any of input
/// files to the destination (output) location because the binary in XCRC artifact already
/// contains a fat library
/// If a remote artifact cannot be reused, a fallback to the `lipo` command is performed
public func run() {
logic.run()
}
}
@@ -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(
@@ -34,23 +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["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
@@ -62,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"
}
}
}
@@ -52,6 +52,7 @@ extension IntegrateContext {
cc: binariesDir.appendingPathComponent("xccc"),
swiftc: binariesDir.appendingPathComponent("xcswiftc"),
libtool: binariesDir.appendingPathComponent("xclibtool"),
lipo: binariesDir.appendingPathComponent("xclipo"),
ld: binariesDir.appendingPathComponent("xcld"),
ldplusplus: binariesDir.appendingPathComponent("xcldplusplus"),
prebuild: binariesDir.appendingPathComponent("xcprebuild"),
@@ -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 {
@@ -25,6 +25,7 @@ struct XCRCBinariesPaths {
let cc: URL
let swiftc: URL
let libtool: URL
let lipo: URL
let ld: URL
let ldplusplus: URL
let prebuild: URL
@@ -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"
}
}
@@ -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"
}
}
+71
View File
@@ -0,0 +1,71 @@
// 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 `lipo` program that links any of input binaries to the destination paths
/// Fallbacks to a standard `lipo` when the Ramote cache is not applicable (e.g. modified sources)
public class XCLipoMain {
public init() { }
public func main() {
let args = ProcessInfo().arguments
var output: String?
var create = false
var inputs: [String] = []
var i = 1
while i < args.count {
switch args[i] {
case "-output":
output = args[i + 1]
i += 1
case "-create":
create = true
default:
inputs.append(args[i])
}
i += 1
}
let lipoCommand = "lipo"
guard let output = output, !inputs.isEmpty, create else {
print("Fallbacking to compilation using \(lipoCommand).")
let args = ProcessInfo().arguments
let paramList = [lipoCommand] + args.dropFirst()
let cargs = paramList.map { strdup($0) } + [nil]
execvp(lipoCommand, cargs)
/// C-function `execv` returns only when the command fails
exit(1)
}
do {
try XCLipo(
output: output,
inputs: inputs,
fallbackCommand: lipoCommand,
stepDescription: "xclipo"
).run()
} catch {
exit(1, "Failed with: \(error). Args: \(args)")
}
}
}
+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
XCLipoMain().main()
+6
View File
@@ -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)
}
}
@@ -34,6 +34,7 @@ class XcodeProjBuildSettingsIntegrateAppenderTests: XCTestCase {
cc: binariesDir.appendingPathComponent("xccc"),
swiftc: binariesDir.appendingPathComponent("xcswiftc"),
libtool: binariesDir.appendingPathComponent("xclibtool"),
lipo: binariesDir.appendingPathComponent("lipo"),
ld: binariesDir.appendingPathComponent("xcld"),
ldplusplus: binariesDir.appendingPathComponent("xcldplusplus"),
prebuild: binariesDir.appendingPathComponent("xcprebuild"),
@@ -44,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)
@@ -54,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)
@@ -64,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&param=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 {
+3 -2
View File
@@ -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
@@ -3,9 +3,9 @@
# Licensed 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.
@@ -29,42 +29,44 @@ module CocoapodsXCRemoteCacheModifier
FAT_ARCHIVE_NAME_INFIX = 'arm64-x86_64'
XCRC_COOCAPODS_ROOT_KEY = 'XCRC_COOCAPODS_ROOT'
# List of plugins' user properties that should not be copied to .rcinfo
# List of plugins' user properties that should not be copied to .rcinfo
CUSTOM_CONFIGURATION_KEYS = [
'enabled',
'enabled',
'xcrc_location',
'exclude_targets',
'exclude_build_configurations',
'final_target',
'check_build_configuration',
'check_platform',
'check_build_configuration',
'check_platform',
'modify_lldb_init',
'fake_src_root',
'exclude_sdks_configurations'
]
class XCRemoteCache
@@configuration = nil
def self.configure(c)
def self.configure(c)
@@configuration = c
end
def self.set_configuration_default_values
default_values = {
'mode' => 'consumer',
'enabled' => true,
'enabled' => true,
'xcrc_location' => "XCRC",
'exclude_build_configurations' => [],
'check_build_configuration' => 'Debug',
'check_platform' => 'iphonesimulator',
'modify_lldb_init' => true,
'check_platform' => 'iphonesimulator',
'modify_lldb_init' => true,
'xccc_file' => "#{BIN_DIR}/xccc",
'remote_commit_file' => "#{BIN_DIR}/arc.rc",
'exclude_targets' => [],
'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
@@ -75,12 +77,12 @@ module CocoapodsXCRemoteCacheModifier
def self.validate_configuration()
required_values = [
'cache_addresses',
'cache_addresses',
'primary_repo',
'check_build_configuration',
'check_platform'
]
missing_configuration_values = required_values.select { |v| !@@configuration.key?(v) }
unless missing_configuration_values.empty?
throw "XCRemoteCache not fully configured. Make sure all required fields are provided. Missing fields are: #{missing_configuration_values.join(', ')}."
@@ -105,13 +107,24 @@ module CocoapodsXCRemoteCacheModifier
end
# @param target [Target] target to apply XCRemoteCache
# @param repo_distance [Integer] distance from the git repo root to the target's $SRCROOT
# @param repo_distance [Integer] distance from the git repo root to the target's $SRCROOT
# @param xc_location [String] path to the dir with all XCRemoteCache binaries, relative to the repo root
# @param xc_cc_path [String] path to the XCRemoteCache clang wrapper, relative to the repo root
# @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,22 +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['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
@@ -156,7 +174,7 @@ module CocoapodsXCRemoteCacheModifier
prebuild_script.dependency_file = "$(TARGET_TEMP_DIR)/prebuild.d"
# Move prebuild (last element) to the position before compile sources phase (to make it real 'prebuild')
if !existing_prebuild_script
if !existing_prebuild_script
compile_phase_index = target.build_phases.index(target.source_build_phase)
target.build_phases.insert(compile_phase_index, target.build_phases.delete(prebuild_script))
end
@@ -184,7 +202,7 @@ module CocoapodsXCRemoteCacheModifier
]
postbuild_script.dependency_file = "$(TARGET_TEMP_DIR)/postbuild.d"
# Move postbuild (last element) to the position after compile sources phase (to make it real 'postbuild')
if !existing_postbuild_script
if !existing_postbuild_script
compile_phase_index = target.build_phases.index(target.source_build_phase)
target.build_phases.insert(compile_phase_index + 1, target.build_phases.delete(postbuild_script))
end
@@ -214,6 +232,7 @@ module CocoapodsXCRemoteCacheModifier
config.build_settings.delete('CC') if config.build_settings.key?('CC')
config.build_settings.delete('SWIFT_EXEC') if config.build_settings.key?('SWIFT_EXEC')
config.build_settings.delete('LIBTOOL') if config.build_settings.key?('LIBTOOL')
config.build_settings.delete('LIPO') if config.build_settings.key?('LIPO')
config.build_settings.delete('LD') if config.build_settings.key?('LD')
config.build_settings.delete('LDPLUSPLUS') if config.build_settings.key?('LDPLUSPLUS')
config.build_settings.delete('SWIFT_USE_INTEGRATED_DRIVER') if config.build_settings.key?('SWIFT_USE_INTEGRATED_DRIVER')
@@ -226,9 +245,9 @@ module CocoapodsXCRemoteCacheModifier
end
# User project is not generated from scratch (contrary to `Pods`), delete all previous XCRemoteCache phases
target.build_phases.delete_if {|phase|
target.build_phases.delete_if {|phase|
# Some phases (e.g. PBXSourcesBuildPhase) don't have strict name check respond_to?
if phase.respond_to?(:name)
if phase.respond_to?(:name)
phase.name != nil && phase.name.start_with?("[XCRC]")
end
}
@@ -240,9 +259,9 @@ module CocoapodsXCRemoteCacheModifier
end
def self.download_xcrc_if_needed(local_location)
required_binaries = ['xcld', 'xcldplusplus', 'xclibtool', 'xcpostbuild', 'xcprebuild', 'xcprepare', 'xcswiftc']
required_binaries = ['xcld', 'xcldplusplus', 'xclibtool', 'xclipo', 'xcpostbuild', 'xcprebuild', 'xcprepare', 'xcswiftc']
binaries_exist = required_binaries.reduce(true) do |exists, filename|
file_path = File.join(local_location, filename)
file_path = File.join(local_location, filename)
exists = exists && File.exist?(file_path)
end
@@ -256,13 +275,13 @@ module CocoapodsXCRemoteCacheModifier
if !system("unzip #{local_package_location} -d #{local_location}")
throw "Unzipping XCRemoteCache failed"
end
end
end
def self.download_latest_xcrc_release(local_package_location)
release_url = 'https://api.github.com/repos/spotify/XCRemoteCache/releases/latest'
asset_url = nil
URI.open(release_url) do |f|
assets_array = JSON.parse(f.read)['assets']
# Pick fat archive
@@ -270,7 +289,7 @@ module CocoapodsXCRemoteCacheModifier
asset_url = asset_array['url']
end
if asset_url.nil?
if asset_url.nil?
throw "Downloading XCRemoteCache failed"
end
@@ -281,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)
@@ -293,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)
@@ -303,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|
@@ -336,7 +381,7 @@ module CocoapodsXCRemoteCacheModifier
File.open(lldbinit_path) { |file|
while(line = file.gets) != nil
line = line.strip
if line == LLDB_INIT_COMMENT
if line == LLDB_INIT_COMMENT
# skip current and next lines
file.gets
next
@@ -351,7 +396,7 @@ module CocoapodsXCRemoteCacheModifier
def self.add_lldbinit_rewrite(lines_content, user_proj_directory,fake_src_root)
all_lines = lines_content.clone
all_lines << LLDB_INIT_COMMENT
all_lines << "settings set target.source-map #{fake_src_root} #{user_proj_directory}"
all_lines << "settings set target.source-map #{fake_src_root} #{user_proj_directory}"
all_lines << ""
all_lines
end
@@ -413,8 +458,8 @@ module CocoapodsXCRemoteCacheModifier
# Remote cache is still disabled - no need to force Pods projects/targets regeneration
next
end
# Force rebuilding all Pods project, because XCRC build steps and settings need to be added to Pods project/targets
# Force rebuilding all Pods project, because XCRC build steps and settings need to be added to Pods project/targets
# It is relevant only when 'incremental_installation' is enabled, otherwise installed_cache_path does not exist on a disk
installed_cache_path = installer_context.sandbox.project_installation_cache_path
if !was_previously_enabled && File.exist?(installed_cache_path)
@@ -431,7 +476,7 @@ module CocoapodsXCRemoteCacheModifier
user_project = installer_context.umbrella_targets[0].user_project
begin
begin
user_proj_directory = File.dirname(user_project.path)
set_configuration_default_values
@@ -452,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}"
@@ -475,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
@@ -488,18 +534,18 @@ 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
# Manual Pods/.rcinfo generation
# all paths in .rcinfo are relative to the root so paths used in Pods.xcodeproj need to be aligned
pods_path = Pathname.new(pods_proj_directory)
root_path = Pathname.new(user_proj_directory)
root_path_to_pods = root_path.relative_path_from(pods_path)
pods_rcinfo = root_rcinfo.merge({
'remote_commit_file' => "#{root_path_to_pods}/#{remote_commit_file}",
'xccc_file' => "#{root_path_to_pods}/#{xccc_location}"
@@ -511,7 +557,7 @@ module CocoapodsXCRemoteCacheModifier
# Enabled/disable XCRemoteCache for the main (user) project
begin
# TODO: Do not compile xcc again. `xcprepare` compiles it in pre-install anyway
# TODO: Do not compile xcc again. `xcprepare` compiles it in pre-install anyway
prepare_result = YAML.load`#{xcrc_location_absolute}/xcprepare --configuration #{check_build_configuration} --platform #{check_platform}`
unless prepare_result['result'] || mode != 'consumer'
# Uninstall the XCRemoteCache for the consumer mode
@@ -529,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
@@ -3,9 +3,9 @@
# Licensed 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.
@@ -13,5 +13,5 @@
# limitations under the License.
module CocoapodsXcremotecache
VERSION = "0.0.14"
VERSION = "0.0.16"
end
@@ -16,6 +16,8 @@
36201A2A2843B3D3002FF70F /* MixedTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36201A292843B3D3002FF70F /* MixedTarget.swift */; };
36201A362843B435002FF70F /* libMixedTarget.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 36201A272843B3D3002FF70F /* libMixedTarget.a */; };
36201A392843BDDC002FF70F /* StandaloneObjc.m in Sources */ = {isa = PBXBuildFile; fileRef = 36201A382843BDDC002FF70F /* StandaloneObjc.m */; };
4E10D63029BBFD8000A8655C /* WatchExtensionExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E10D62F29BBFD8000A8655C /* WatchExtensionExtension.swift */; };
4E10D63229BBFD8000A8655C /* WatchExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E10D63129BBFD8000A8655C /* WatchExtension.swift */; };
4EE6CF4929B6C1A000AEE1B4 /* StaticFramework.h in Headers */ = {isa = PBXBuildFile; fileRef = 4EE6CF4829B6C1A000AEE1B4 /* StaticFramework.h */; settings = {ATTRIBUTES = (Public, ); }; };
4EE6CF5329B6C1AF00AEE1B4 /* StaticFrameworkFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE6CF5229B6C1AF00AEE1B4 /* StaticFrameworkFile.swift */; };
/* End PBXBuildFile section */
@@ -28,6 +30,13 @@
remoteGlobalIDString = 36201A262843B3D3002FF70F;
remoteInfo = MixedTarget;
};
4E10D63729BBFD8E00A8655C /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 36201A042843B3C3002FF70F /* Project object */;
proxyType = 1;
remoteGlobalIDString = 4EE6CF4529B6C1A000AEE1B4;
remoteInfo = StaticFramework;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -67,9 +76,10 @@
36201A302843B414002FF70F /* SomeObjC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SomeObjC.h; sourceTree = "<group>"; };
36201A372843BDDC002FF70F /* StandaloneObjc.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = StandaloneObjc.h; sourceTree = "<group>"; };
36201A382843BDDC002FF70F /* StandaloneObjc.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = StandaloneObjc.m; sourceTree = "<group>"; };
4E628CA229B8066500AF2DB0 /* SandaloneWatchAppExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SandaloneWatchAppExtension.swift; sourceTree = "<group>"; };
4E628CA429B8066500AF2DB0 /* SandaloneWatchApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SandaloneWatchApp.swift; sourceTree = "<group>"; };
4E628CA629B8066500AF2DB0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
4E10D62D29BBFD8000A8655C /* WatchExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.extensionkit-extension"; includeInIndex = 0; path = WatchExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
4E10D62F29BBFD8000A8655C /* WatchExtensionExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchExtensionExtension.swift; sourceTree = "<group>"; };
4E10D63129BBFD8000A8655C /* WatchExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchExtension.swift; sourceTree = "<group>"; };
4E10D63329BBFD8000A8655C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
4EE6CF4629B6C1A000AEE1B4 /* StaticFramework.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StaticFramework.framework; sourceTree = BUILT_PRODUCTS_DIR; };
4EE6CF4829B6C1A000AEE1B4 /* StaticFramework.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = StaticFramework.h; sourceTree = "<group>"; };
4EE6CF5229B6C1AF00AEE1B4 /* StaticFrameworkFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StaticFrameworkFile.swift; sourceTree = "<group>"; };
@@ -91,6 +101,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
4E10D62A29BBFD8000A8655C /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
4EE6CF4329B6C1A000AEE1B4 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@@ -107,7 +124,7 @@
36201A0E2843B3C3002FF70F /* StandaloneApp */,
36201A282843B3D3002FF70F /* MixedTarget */,
4EE6CF4729B6C1A000AEE1B4 /* StaticFramework */,
4E628CA129B8066500AF2DB0 /* SandaloneWatchApp */,
4E10D62E29BBFD8000A8655C /* WatchExtension */,
36201A0D2843B3C3002FF70F /* Products */,
36201A352843B435002FF70F /* Frameworks */,
);
@@ -119,6 +136,7 @@
36201A0C2843B3C3002FF70F /* StandaloneApp.app */,
36201A272843B3D3002FF70F /* libMixedTarget.a */,
4EE6CF4629B6C1A000AEE1B4 /* StaticFramework.framework */,
4E10D62D29BBFD8000A8655C /* WatchExtension.appex */,
);
name = Products;
sourceTree = "<group>";
@@ -156,14 +174,14 @@
name = Frameworks;
sourceTree = "<group>";
};
4E628CA129B8066500AF2DB0 /* SandaloneWatchApp */ = {
4E10D62E29BBFD8000A8655C /* WatchExtension */ = {
isa = PBXGroup;
children = (
4E628CA229B8066500AF2DB0 /* SandaloneWatchAppExtension.swift */,
4E628CA429B8066500AF2DB0 /* SandaloneWatchApp.swift */,
4E628CA629B8066500AF2DB0 /* Info.plist */,
4E10D62F29BBFD8000A8655C /* WatchExtensionExtension.swift */,
4E10D63129BBFD8000A8655C /* WatchExtension.swift */,
4E10D63329BBFD8000A8655C /* Info.plist */,
);
path = SandaloneWatchApp;
path = WatchExtension;
sourceTree = "<group>";
};
4EE6CF4729B6C1A000AEE1B4 /* StaticFramework */ = {
@@ -226,6 +244,24 @@
productReference = 36201A272843B3D3002FF70F /* libMixedTarget.a */;
productType = "com.apple.product-type.library.static";
};
4E10D62C29BBFD8000A8655C /* WatchExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = 4E10D63429BBFD8000A8655C /* Build configuration list for PBXNativeTarget "WatchExtension" */;
buildPhases = (
4E10D62929BBFD8000A8655C /* Sources */,
4E10D62A29BBFD8000A8655C /* Frameworks */,
4E10D62B29BBFD8000A8655C /* Resources */,
);
buildRules = (
);
dependencies = (
4E10D63829BBFD8E00A8655C /* PBXTargetDependency */,
);
name = WatchExtension;
productName = WatchExtension;
productReference = 4E10D62D29BBFD8000A8655C /* WatchExtension.appex */;
productType = "com.apple.product-type.extensionkit-extension";
};
4EE6CF4529B6C1A000AEE1B4 /* StaticFramework */ = {
isa = PBXNativeTarget;
buildConfigurationList = 4EE6CF5129B6C1A000AEE1B4 /* Build configuration list for PBXNativeTarget "StaticFramework" */;
@@ -262,6 +298,9 @@
CreatedOnToolsVersion = 13.2.1;
LastSwiftMigration = 1320;
};
4E10D62C29BBFD8000A8655C = {
CreatedOnToolsVersion = 14.2;
};
4EE6CF4529B6C1A000AEE1B4 = {
CreatedOnToolsVersion = 14.2;
LastSwiftMigration = 1420;
@@ -284,6 +323,7 @@
36201A0B2843B3C3002FF70F /* StandaloneApp */,
36201A262843B3D3002FF70F /* MixedTarget */,
4EE6CF4529B6C1A000AEE1B4 /* StaticFramework */,
4E10D62C29BBFD8000A8655C /* WatchExtension */,
);
};
/* End PBXProject section */
@@ -299,6 +339,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
4E10D62B29BBFD8000A8655C /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
4EE6CF4429B6C1A000AEE1B4 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@@ -353,6 +400,15 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
4E10D62929BBFD8000A8655C /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
4E10D63029BBFD8000A8655C /* WatchExtensionExtension.swift in Sources */,
4E10D63229BBFD8000A8655C /* WatchExtension.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
4EE6CF4229B6C1A000AEE1B4 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@@ -369,6 +425,11 @@
target = 36201A262843B3D3002FF70F /* MixedTarget */;
targetProxy = 36201A332843B431002FF70F /* PBXContainerItemProxy */;
};
4E10D63829BBFD8E00A8655C /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 4EE6CF4529B6C1A000AEE1B4 /* StaticFramework */;
targetProxy = 4E10D63729BBFD8E00A8655C /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
@@ -607,6 +668,62 @@
};
name = Release;
};
4E10D63529BBFD8000A8655C /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = WatchExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = WatchExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
"@executable_path/../../../../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.xcremotecache.WatchExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 4;
WATCHOS_DEPLOYMENT_TARGET = 4.0;
};
name = Debug;
};
4E10D63629BBFD8000A8655C /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = WatchExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = WatchExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
"@executable_path/../../../../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.xcremotecache.WatchExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 4;
WATCHOS_DEPLOYMENT_TARGET = 4.0;
};
name = Release;
};
4EE6CF4F29B6C1A000AEE1B4 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@@ -710,6 +827,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
4E10D63429BBFD8000A8655C /* Build configuration list for PBXNativeTarget "WatchExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
4E10D63529BBFD8000A8655C /* Debug */,
4E10D63629BBFD8000A8655C /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
4EE6CF5129B6C1A000AEE1B4 /* Build configuration list for PBXNativeTarget "StaticFramework" */ = {
isa = XCConfigurationList;
buildConfigurations = (
@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1420"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4E10D61129BBF2FA00A8655C"
BuildableName = "SampleWatchApp.app"
BlueprintName = "SampleWatchApp"
ReferencedContainer = "container:StandaloneApp.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4E10D61129BBF2FA00A8655C"
BuildableName = "SampleWatchApp.app"
BlueprintName = "SampleWatchApp"
ReferencedContainer = "container:StandaloneApp.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4E10D61129BBF2FA00A8655C"
BuildableName = "SampleWatchApp.app"
BlueprintName = "SampleWatchApp"
ReferencedContainer = "container:StandaloneApp.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4E10D61129BBF2FA00A8655C"
BuildableName = "SampleWatchApp.app"
BlueprintName = "SampleWatchApp"
ReferencedContainer = "container:StandaloneApp.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>EXAppExtensionAttributes</key>
<dict>
<key>EXExtensionPointIdentifier</key>
<string>com.apple.appintents-extension</string>
</dict>
</dict>
</plist>
@@ -0,0 +1,2 @@
import AppIntents
@@ -0,0 +1,4 @@
import AppIntents
struct WatchExtensionExtension: AppIntentsExtension {
}
+9
View File
@@ -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
+8
View File
@@ -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
}
+8
View File
@@ -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*"]
}
+43 -12
View File
@@ -1,4 +1,5 @@
require 'json'
require "ostruct"
desc 'Support for E2E tests: building XCRemoteCache-enabled xcodeproj using xcodebuild'
namespace :e2e do
@@ -21,7 +22,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)
@@ -58,7 +63,7 @@ namespace :e2e do
system("pwd")
system("#{XCRC_BINARIES}/xcprepare integrate --input StandaloneApp.xcodeproj --mode producer --final-producer-target StandaloneApp")
# Build the project to fill in the cache
build_project(nil, "StandaloneApp.xcodeproj", 'StaticFramework', 'watch', 'watchOS')
build_project(nil, "StandaloneApp.xcodeproj", 'WatchExtension', 'watch', 'watchOS')
build_project(nil, "StandaloneApp.xcodeproj", 'StandaloneApp')
system("#{XCRC_BINARIES}/xcprepare stats --reset --format json")
end
@@ -74,14 +79,14 @@ 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", 'StaticFramework', 'watch', 'watchOS', {'derivedDataPath' => "#{DERIVED_DATA_PATH}_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
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", 'StaticFramework', 'watch', 'watchOS', {'derivedDataPath' => "#{DERIVED_DATA_PATH}_consumer_local"})
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"})
end
@@ -198,22 +203,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}")
@@ -233,7 +249,7 @@ namespace :e2e do
puts('Building consumer ...')
Dir.chdir(E2E_COCOAPODS_SAMPLE_DIR) do
build_project_cocoapods('iphone', 'iOS', {'derivedDataPath' => "#{DERIVED_DATA_PATH}_consumer"})
valide_hit_rate
valide_hit_rate(expectations)
end
end
@@ -241,4 +257,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