Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bf90d518f4 | |||
| 634afb3f3f | |||
| c2b80c0112 | |||
| d0604e9042 | |||
| 297c1a90cb | |||
| 34cb54b675 | |||
| 14b2b3aceb | |||
| cbed913c63 | |||
| 63448ff0a0 | |||
| 64bccaed16 | |||
| 80a7abb4d5 | |||
| c50ee6f798 | |||
| 6e4bf25d1c | |||
| 71af03f227 | |||
| 0ebe6f5ceb | |||
| 29cba26c5d | |||
| 08b6115187 | |||
| bbbb0a5b0f | |||
| 758764ad95 | |||
| f332593076 | |||
| d0b2bc0f71 | |||
| e2f68c8f4e | |||
| dbff760716 | |||
| bb05b02bd8 | |||
| 5c568a1338 | |||
| 86273017b4 | |||
| 4f1f73132e | |||
| ec1ef567cb | |||
| 1c94e51059 | |||
| ba41e40bb0 | |||
| a0c88d9059 | |||
| 15173b9575 | |||
| fa82f920ad | |||
| 78031a3135 | |||
| 2156de1706 | |||
| ce2ef3ea69 | |||
| 3219ac24aa | |||
| d9ef32f24f | |||
| 3aaf483263 | |||
| cc691b8c67 | |||
| 4c95ff915f |
+2
-1
@@ -5,4 +5,5 @@
|
||||
DerivedData
|
||||
/.swiftpm/
|
||||
releases
|
||||
tmp/
|
||||
tmp/
|
||||
.idea/
|
||||
|
||||
@@ -5,6 +5,7 @@ disabled_rules:
|
||||
- superfluous_disable_command # Disabled since we disable some rules pre-emptively to avoid issues in the future
|
||||
- todo # Temporarily disabled. We have too many right now hiding real issues :(
|
||||
- nesting # Does not make sense anymore since Swift 4 uses nested `CodingKeys` enums for example
|
||||
- trailing_dot_in_comments # Triggers warnings for generated file headers
|
||||
|
||||
opt_in_rules:
|
||||
- anyobject_protocol
|
||||
|
||||
@@ -6,8 +6,9 @@ _XCRemoteCache is a remote cache tool for Xcode projects. It reuses target artif
|
||||
|
||||
[](https://github.com/spotify/XCRemoteCache/workflows/CI/badge.svg)
|
||||
[](LICENSE)
|
||||
[](https://slackin.spotify.com)
|
||||
|
||||
- [How and Why?](#how-and-why-)
|
||||
- [How and Why?](#how-and-why)
|
||||
* [Accurate target input files](#accurate-target-input-files)
|
||||
+ [New file added to the target](#new-file-added-to-the-target)
|
||||
* [Debug symbols](#debug-symbols)
|
||||
@@ -50,7 +51,7 @@ _XCRemoteCache is a remote cache tool for Xcode projects. It reuses target artif
|
||||
|
||||
The caching mechanism is based on remote artifacts that should be generated and uploaded to the cache server for each commit on a `master` branch, preferably as a part of CI/CD step. Xcode products are not portable between different Xcode versions, each XCRemoteCache artifact is linked with a specific Xcode build number that generated it. To support multiple Xcode versions, artifacts generation should happen for each Xcode version.
|
||||
|
||||
The artifact resue flow is as follows: XCRemoteCache performs a target precheck (aka prebuild) and if a fingerprint for local sources matches the one computed on a generation side, several compilation steps wrappers (e.g. `xcswiftc`, `xccc`, `xclibtool`) mock corresponding compilation step(s) and linking (or archiving) moves the cached build artifact to the expected location.
|
||||
The artifact reuse flow is as follows: XCRemoteCache performs a target precheck (aka prebuild) and if a fingerprint for local sources matches the one computed on a generation side, several compilation steps wrappers (e.g. `xcswiftc`, `xccc`, `xclibtool`) mock corresponding compilation step(s) and linking (or archiving) moves the cached build artifact to the expected location.
|
||||
|
||||
> Multiple commits that have the same target sources reuse artifact package on a remote server.
|
||||
|
||||
@@ -134,7 +135,7 @@ xcremotecache/xcprepare integrate --input <yourProject.xcodeproj> --mode consume
|
||||
| Argument | Description | Default | Required |
|
||||
| ------------- | ------------- | ------------- | ------------- |
|
||||
| `--input` | .xcodeproj location | N/A | ✅ |
|
||||
| `--mode` | mode. Supported values: `consumer`, `producer` | N/A | ✅ |
|
||||
| `--mode` | mode. Supported values: `consumer`, `producer`, `producer-fast`(experimental) | N/A | ✅ |
|
||||
| `--targets-include` | comma-separated list of targets to integrate XCRemoteCache. | `""` | ⬜️ |
|
||||
| `--targets-exclude` | comma-separated list of targets to not integrate XCRemoteCache. Takes priority over --targets-include. | `""` | ⬜️ |
|
||||
| `--configurations-include` | comma-separated list of configurations to integrate XCRemoteCache. | `""` | ⬜️ |
|
||||
@@ -371,7 +372,7 @@ Head over to our [cocoapods-plugin](cocoapods-plugin/README.md) docs to see how
|
||||
|
||||
## Limitations
|
||||
|
||||
* Swift Package Manager (SPM) projects are not supported
|
||||
* Swift Package Manager (SPM) dependencies are not supported. _Because SPM does not allow customizing Build Settings, XCRemoteCache cannot specify `clang` and `swiftc` wrappers that control if the local compilation should be skipped (cache hit) or not (cache miss)_
|
||||
* Filenames with `_vers.c` suffix are reserved and cannot be used as a source file
|
||||
* All compilation files should be referenced via the git repo root. Referencing `/AbsolutePath/someOther.swift` or `../../someOther.swift` that resolve to the location outside of the git repo root is prohibited.
|
||||
|
||||
@@ -398,6 +399,12 @@ rake 'build[release, x86_64-apple-macosx]'
|
||||
|
||||
The zip package will be generated at `releases/XCRemoteCache.zip`.
|
||||
|
||||
## Support
|
||||
|
||||
Create a [new issue](https://github.com/spotify/XCRemoteCache/issues/new) with as many details as possible.
|
||||
|
||||
Reach us at the `#xcremotecache` channel in [Slack](https://slackin.spotify.com/).
|
||||
|
||||
## Contributing
|
||||
|
||||
We feel that a welcoming community is important and we ask that you follow Spotify's
|
||||
|
||||
@@ -28,7 +28,7 @@ task :lint => [:prepare] do
|
||||
puts 'Run linting'
|
||||
|
||||
system("swiftformat --lint --config .swiftformat --cache ignore .") or abort "swiftformat failure" if SWIFTFORMAT_ENABLED
|
||||
system("swiftlint lint --config .swiftlint.yml") or abort "swiftlint failure" if SWIFTLINT_ENABLED
|
||||
system("swiftlint lint --config .swiftlint.yml --strict") or abort "swiftlint failure" if SWIFTLINT_ENABLED
|
||||
end
|
||||
|
||||
task :autocorrect => [:prepare] do
|
||||
|
||||
@@ -41,6 +41,7 @@ class BuildArtifactCreator: ArtifactSwiftProductsBuilderImpl, ArtifactCreator {
|
||||
private let moduleName: String?
|
||||
private let modulesFolderPath: String
|
||||
private let dSYMPath: URL
|
||||
private let metaWriter: MetaWriter
|
||||
private let fileManager: FileManager
|
||||
|
||||
init(
|
||||
@@ -50,6 +51,7 @@ class BuildArtifactCreator: ArtifactSwiftProductsBuilderImpl, ArtifactCreator {
|
||||
moduleName: String?,
|
||||
modulesFolderPath: String,
|
||||
dSYMPath: URL,
|
||||
metaWriter: MetaWriter,
|
||||
fileManager: FileManager
|
||||
) {
|
||||
self.buildDir = buildDir
|
||||
@@ -59,6 +61,7 @@ class BuildArtifactCreator: ArtifactSwiftProductsBuilderImpl, ArtifactCreator {
|
||||
self.moduleName = moduleName
|
||||
self.fileManager = fileManager
|
||||
self.dSYMPath = dSYMPath
|
||||
self.metaWriter = metaWriter
|
||||
super.init(workingDir: tempDir, moduleName: moduleName, fileManager: fileManager)
|
||||
}
|
||||
|
||||
@@ -72,7 +75,11 @@ class BuildArtifactCreator: ArtifactSwiftProductsBuilderImpl, ArtifactCreator {
|
||||
let dynamicLibraryArtifacts = try prepareDynamicLibraryArtifacts()
|
||||
zipPaths.append(contentsOf: dynamicLibraryArtifacts)
|
||||
|
||||
let creator = ZipArtifactCreator(workingDir: zipWorkingDir, fileManager: fileManager)
|
||||
let creator = ZipArtifactCreator(
|
||||
workingDir: zipWorkingDir,
|
||||
metaWriter: metaWriter,
|
||||
fileManager: fileManager
|
||||
)
|
||||
return try creator.createArtifact(zipContent: zipPaths, artifactKey: artifactKey, meta: meta)
|
||||
}
|
||||
|
||||
|
||||
@@ -23,11 +23,12 @@ import Zip
|
||||
class ZipArtifactCreator {
|
||||
/// Location where zip file should be generated
|
||||
private let workingDir: URL
|
||||
private let metaWriter: MetaWriter
|
||||
private let fileManager: FileManager
|
||||
private let metaEncoder = JSONEncoder()
|
||||
|
||||
init(workingDir: URL, fileManager: FileManager) {
|
||||
init(workingDir: URL, metaWriter: MetaWriter, fileManager: FileManager) {
|
||||
self.workingDir = workingDir
|
||||
self.metaWriter = metaWriter
|
||||
self.fileManager = fileManager
|
||||
}
|
||||
|
||||
@@ -35,18 +36,10 @@ class ZipArtifactCreator {
|
||||
let zipURL = workingDir.appendingPathComponent("\(artifactKey).zip")
|
||||
try fileManager.createDirectory(at: workingDir, withIntermediateDirectories: true, attributes: nil)
|
||||
// Include meta json to the artifact
|
||||
let metaURL = try dumpMeta(meta)
|
||||
let metaURL = try metaWriter.write(meta, locationDir: workingDir)
|
||||
let zipPaths = zipContent + [metaURL]
|
||||
|
||||
try Zip.zipFiles(paths: zipPaths, zipFilePath: zipURL, password: nil, progress: nil)
|
||||
return Artifact(id: artifactKey, package: zipURL, meta: metaURL)
|
||||
}
|
||||
|
||||
// Save meta to a local file
|
||||
private func dumpMeta<T: Meta>(_ meta: T) throws -> URL {
|
||||
let metaURL = workingDir.appendingPathComponent(meta.fileKey).appendingPathExtension("json")
|
||||
let metaData = try metaEncoder.encode(meta)
|
||||
try fileManager.spt_writeToFile(atPath: metaURL.path, contents: metaData)
|
||||
return metaURL
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,13 +33,16 @@ enum ThinningCreatorPluginError: Error {
|
||||
/// Warning! This plugin assumes that producer's DerivedData are always cleaned before a build
|
||||
class ThinningCreatorPlugin: ArtifactCreatorPlugin {
|
||||
private let targetTempDir: URL
|
||||
private let modeMarkerPath: String
|
||||
private let dirScanner: DirScanner
|
||||
|
||||
/// Default Initializer
|
||||
/// - Parameter targetTempDir: Location of current target-specific temp dir (TARGET_TEMP_DIR)
|
||||
/// - Parameter modeMarkerPath: path of maker file that informs if a given target can reuse remote artifacts.
|
||||
/// - Parameter dirScanner: scanner to access disk and read files and directories hierarchy
|
||||
init(targetTempDir: URL, dirScanner: DirScanner) {
|
||||
init(targetTempDir: URL, modeMarkerPath: String, dirScanner: DirScanner) {
|
||||
self.targetTempDir = targetTempDir
|
||||
self.modeMarkerPath = modeMarkerPath
|
||||
self.dirScanner = dirScanner
|
||||
}
|
||||
|
||||
@@ -57,31 +60,19 @@ class ThinningCreatorPlugin: ArtifactCreatorPlugin {
|
||||
let fileKey: String
|
||||
}
|
||||
let uploadedTargetArtifacts = try allURLs.compactMap { tempDir -> TargetTuple? in
|
||||
// All targets that uploaded their artifacts, have it placed in the
|
||||
// `$(TARGET_TEMP_DIR)/xccache/produced/{{fileKey}}.zip` location. Find all targets that have such a file
|
||||
|
||||
let targetGeneratedArtifactRootDir = tempDir
|
||||
.appendingPathComponent("xccache")
|
||||
.appendingPathComponent("produced")
|
||||
guard try dirScanner.itemType(atPath: targetGeneratedArtifactRootDir.path) == ItemType.dir else {
|
||||
// given target didn't generate any artifacts (e.g. it is never cached with XCRemoteCache)
|
||||
return nil
|
||||
}
|
||||
|
||||
let allFilesProduced = try dirScanner.items(at: targetGeneratedArtifactRootDir)
|
||||
let allArtifacts = allFilesProduced.filter { $0.pathExtension == "zip" }
|
||||
guard !allArtifacts.isEmpty else {
|
||||
let potentialArtifacts = try findTargetPackageZip(tempDir: tempDir)
|
||||
guard !potentialArtifacts.isEmpty else {
|
||||
// there is no generated *.zip file, so given target didn't create an artifact - it could be
|
||||
// just a helper target (like the target we integrate this plugin with)
|
||||
return nil
|
||||
}
|
||||
// Find {{fileKey}} based on the .zip file basename
|
||||
guard allArtifacts.count == 1 else {
|
||||
guard potentialArtifacts.count == 1 else {
|
||||
throw ThinningCreatorPluginError.noSingleTargetArtifactsGenerated(
|
||||
rootDir: targetGeneratedArtifactRootDir
|
||||
rootDir: tempDir
|
||||
)
|
||||
}
|
||||
let fileKey = allArtifacts[0].deletingPathExtension().lastPathComponent
|
||||
let fileKey = potentialArtifacts[0].deletingPathExtension().lastPathComponent
|
||||
// Taking target name from tempDir, which has a structures "*.build"
|
||||
let targetName = tempDir.deletingPathExtension().lastPathComponent
|
||||
return TargetTuple(targetName: targetName, fileKey: fileKey)
|
||||
@@ -96,6 +87,41 @@ class ThinningCreatorPlugin: ArtifactCreatorPlugin {
|
||||
return Dictionary(uniqueKeysWithValues: extraKeysTuples)
|
||||
}
|
||||
|
||||
private func findTargetPackageZip(tempDir: URL) throws -> [URL] {
|
||||
// Producer mode:
|
||||
// All targets that uploaded their artifacts, have it placed in the
|
||||
// `$(TARGET_TEMP_DIR)/xccache/produced/{{fileKey}}.zip` location. Find all targets that have such a file
|
||||
// ProducerFast mode:
|
||||
// If a target reused already existing artifact, it still has `$(TARGET_TEMP_DIR)/rc.enabled` marker file
|
||||
// and the reused zip is placed in:
|
||||
// `$(TARGET_TEMP_DIR)/xccache/{{fileKey}}.zip` location.
|
||||
|
||||
let targetEnabledMarker = tempDir.appendingPathComponent(modeMarkerPath)
|
||||
let targetReusedArtifactRootDir = tempDir.appendingPathComponent("xccache")
|
||||
let targetGeneratedArtifactRootDir = tempDir
|
||||
.appendingPathComponent("xccache")
|
||||
.appendingPathComponent("produced")
|
||||
|
||||
let pathToDirWithZipArtifacts: URL
|
||||
// try the `.producerFast` scenario first (the artifact was not locally
|
||||
// generated but just reused from the remote cache)
|
||||
if try dirScanner.itemType(atPath: targetEnabledMarker.path) == ItemType.file {
|
||||
pathToDirWithZipArtifacts = targetReusedArtifactRootDir
|
||||
} else {
|
||||
// cover a case when a target was build locally and an artifact
|
||||
// has just been created (locally)
|
||||
guard try dirScanner.itemType(atPath: targetGeneratedArtifactRootDir.path) == ItemType.dir else {
|
||||
// given target didn't generate any artifacts (e.g. it is never cached with XCRemoteCache)
|
||||
return []
|
||||
}
|
||||
pathToDirWithZipArtifacts = targetGeneratedArtifactRootDir
|
||||
}
|
||||
|
||||
let allFilesProduced = try dirScanner.items(at: pathToDirWithZipArtifacts)
|
||||
let allArtifacts = allFilesProduced.filter { $0.pathExtension == "zip" }
|
||||
return allArtifacts
|
||||
}
|
||||
|
||||
func artifactToUpload(main: MainArtifactMeta) throws -> [Artifact] {
|
||||
return []
|
||||
}
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ class ThinningDiskSwiftcProductsGenerator: SwiftcProductsGenerator {
|
||||
destinationSwiftmodulePaths = Dictionary(
|
||||
uniqueKeysWithValues: SwiftmoduleFileExtension.SwiftmoduleExtensions
|
||||
.map { ext, _ in
|
||||
switch (ext) {
|
||||
switch ext {
|
||||
case .swiftsourceinfo:
|
||||
let dest = modulePathDir.appendingPathComponent("Project")
|
||||
.appendingPathComponent(moduleName)
|
||||
|
||||
@@ -41,6 +41,7 @@ class Postbuild {
|
||||
private let dSYMOrganizer: DSYMOrganizer
|
||||
private let modeController: CacheModeController
|
||||
private let metaReader: MetaReader
|
||||
private let metaWriter: MetaWriter
|
||||
private let creatorPlugins: [ArtifactCreatorPlugin]
|
||||
private let consumerPlugins: [ArtifactConsumerPostbuildPlugin]
|
||||
|
||||
@@ -58,6 +59,7 @@ class Postbuild {
|
||||
dSYMOrganizer: DSYMOrganizer,
|
||||
modeController: CacheModeController,
|
||||
metaReader: MetaReader,
|
||||
metaWriter: MetaWriter,
|
||||
creatorPlugins: [ArtifactCreatorPlugin],
|
||||
consumerPlugins: [ArtifactConsumerPostbuildPlugin]
|
||||
) {
|
||||
@@ -74,6 +76,7 @@ class Postbuild {
|
||||
self.dSYMOrganizer = dSYMOrganizer
|
||||
self.modeController = modeController
|
||||
self.metaReader = metaReader
|
||||
self.metaWriter = metaWriter
|
||||
self.creatorPlugins = creatorPlugins
|
||||
self.consumerPlugins = consumerPlugins
|
||||
}
|
||||
@@ -125,6 +128,22 @@ class Postbuild {
|
||||
try generateFingerprintOverrides(contextSpecificFingerprint: fingerprint.contextSpecific)
|
||||
}
|
||||
|
||||
/// Uploads only a meta to the remote server - useful when the file artifact (.zip) already exists on a remote
|
||||
/// server and only a meta for a current commit sha has to be uploaded
|
||||
public func performMetaUpload(meta: MainArtifactMeta, for commit: String) throws {
|
||||
// Reset plugins keys as these are unique to each
|
||||
var meta = meta
|
||||
meta.pluginsKeys = [:]
|
||||
meta = try creatorPlugins.reduce(meta) { prevMeta, plugin in
|
||||
var meta = prevMeta
|
||||
// add extra keys from the plugin. A plugin overrides previously defined keys in case of duplication
|
||||
meta.pluginsKeys = try meta.pluginsKeys.merging(plugin.extraMetaKeys(prevMeta), uniquingKeysWith: { $1 })
|
||||
return meta
|
||||
}
|
||||
let metaPath = try metaWriter.write(meta, locationDir: context.targetTempDir)
|
||||
try networkClient.uploadSynchronously(metaPath, as: .meta(commit: commit))
|
||||
}
|
||||
|
||||
/// Builds an artifact package and uploads it to the remote server
|
||||
public func performBuildUpload(for commit: String) throws {
|
||||
let dependencies = try generateDependencies()
|
||||
|
||||
@@ -73,6 +73,7 @@ public struct PostbuildContext {
|
||||
var thinnedTargets: [String]
|
||||
/// Action type: build, indexbuild etc.
|
||||
var action: BuildActionType
|
||||
let modeMarkerPath: String
|
||||
}
|
||||
|
||||
extension PostbuildContext {
|
||||
@@ -115,5 +116,6 @@ extension PostbuildContext {
|
||||
let thinFocusedTargetsString: String = env.readEnv(key: "SPT_XCREMOTE_CACHE_THINNED_TARGETS") ?? ""
|
||||
thinnedTargets = thinFocusedTargetsString.split(separator: ",").map(String.init)
|
||||
action = (try? BuildActionType(rawValue: env.readEnv(key: "ACTION"))) ?? .unknown
|
||||
modeMarkerPath = config.modeMarkerPath
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ public class XCPostbuild {
|
||||
algorithm: MD5Algorithm()
|
||||
)
|
||||
let organizer = ZipArtifactOrganizer(targetTempDir: context.targetTempDir, fileManager: fileManager)
|
||||
let metaWriter = JsonMetaWriter(fileWriter: fileManager)
|
||||
let artifactCreator = BuildArtifactCreator(
|
||||
buildDir: context.productsDir,
|
||||
tempDir: context.targetTempDir,
|
||||
@@ -92,6 +93,7 @@ public class XCPostbuild {
|
||||
moduleName: context.moduleName,
|
||||
modulesFolderPath: context.modulesFolderPath,
|
||||
dSYMPath: context.dSYMPath,
|
||||
metaWriter: metaWriter,
|
||||
fileManager: fileManager
|
||||
)
|
||||
let dirAccessor = DirAccessorComposer(
|
||||
@@ -205,9 +207,10 @@ public class XCPostbuild {
|
||||
worker: DispatchGroupParallelizationWorker(qos: .userInitiated)
|
||||
)
|
||||
consumerPlugins.append(thinningPlugin)
|
||||
case .producer:
|
||||
case .producer, .producerFast:
|
||||
let thinningPlugin = ThinningCreatorPlugin(
|
||||
targetTempDir: context.targetTempDir,
|
||||
modeMarkerPath: context.modeMarkerPath,
|
||||
dirScanner: fileManager
|
||||
)
|
||||
creatorPlugins.append(thinningPlugin)
|
||||
@@ -229,6 +232,7 @@ public class XCPostbuild {
|
||||
dSYMOrganizer: dSYMOrganizer,
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: creatorPlugins,
|
||||
consumerPlugins: consumerPlugins
|
||||
)
|
||||
@@ -243,11 +247,22 @@ public class XCPostbuild {
|
||||
try postbuildAction.deleteFingerprintOverrides()
|
||||
}
|
||||
|
||||
|
||||
// Trigger uploading the artifact
|
||||
if context.mode == .producer {
|
||||
switch (context.mode, try modeController.isEnabled(), context.remoteCommit) {
|
||||
case (.producerFast, true, .available(commit: let commitToReuse)):
|
||||
// Upload only updated meta. Artifact zip is already on a remote server
|
||||
let referenceCommit = try config.publishingSha ?? gitClient.getCurrentSha()
|
||||
let metaData = try remoteNetworkClient.fetch(.meta(commit: commitToReuse))
|
||||
let meta = try metaReader.read(data: metaData)
|
||||
try postbuildAction.performMetaUpload(meta: meta, for: referenceCommit)
|
||||
case (.producer, _, _), (.producerFast, _, _):
|
||||
// Generate artifacts and upload to the remote server for a reference sha
|
||||
let referenceCommit = try config.publishingSha ?? gitClient.getCurrentSha()
|
||||
try postbuildAction.performBuildUpload(for: referenceCommit)
|
||||
default:
|
||||
// Consumer does not upload anything
|
||||
break
|
||||
}
|
||||
|
||||
let executableURL = context.productsDir.appendingPathComponent(context.executablePath)
|
||||
@@ -261,9 +276,8 @@ public class XCPostbuild {
|
||||
} else {
|
||||
try postbuildAction.performBuildCleanup()
|
||||
try cacheHitLogger.logMiss()
|
||||
// Producer mode doesn't use cached artifacts so modeController is not enabled. If producer
|
||||
// reaches this point, there were no issues with publishing
|
||||
let actionName = context.mode == .producer ? "Published" : "Disabled"
|
||||
// If producers reach this point, there were no issues with publishing
|
||||
let actionName = context.mode == .consumer ? "Disabled" : "Published"
|
||||
printToUser("\(actionName) remote cache for \(context.targetName)")
|
||||
}
|
||||
} catch PluginError.unrecoverableError(let error) {
|
||||
|
||||
@@ -55,6 +55,7 @@ class Prebuild {
|
||||
self.artifactConsumerPrebuildPlugins = artifactConsumerPrebuildPlugins
|
||||
}
|
||||
|
||||
// swiftlint:disable:next function_body_length
|
||||
public func perform() throws -> PrebuildResult {
|
||||
guard case .available(let commit) = context.remoteCommit else {
|
||||
return .incompatible
|
||||
|
||||
@@ -129,7 +129,10 @@ public class XCPrebuild {
|
||||
algorithm: MD5Algorithm()
|
||||
)
|
||||
let organizer = ZipArtifactOrganizer(targetTempDir: context.targetTempDir, fileManager: fileManager)
|
||||
let compilationHistoryOrganizer = CompilationHistoryFileOrganizer(context.compilationHistoryFile, fileManager: fileManager)
|
||||
let compilationHistoryOrganizer = CompilationHistoryFileOrganizer(
|
||||
context.compilationHistoryFile,
|
||||
fileManager: fileManager
|
||||
)
|
||||
let metaReader = JsonMetaReader(fileAccessor: fileManager)
|
||||
var consumerPlugins: [ArtifactConsumerPrebuildPlugin] = []
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ class TemplateBasedCCWrapperBuilder: CCWrapperBuilder {
|
||||
|
||||
|
||||
/// Generates source of the cc wrapper
|
||||
// swiftlint:disable line_length
|
||||
// swiftlint:disable:next function_body_length
|
||||
private func buildWrapperSource(clangCommand: String, markerFilename: String, commitSha: String) -> String {
|
||||
return """
|
||||
@@ -516,5 +517,5 @@ class TemplateBasedCCWrapperBuilder: CCWrapperBuilder {
|
||||
#pragma GCC diagnostic pop
|
||||
}
|
||||
"""
|
||||
} // swiftlint:disable:next file_length
|
||||
}
|
||||
} // swiftlint:disable:next file_length line_length
|
||||
} // swiftlint:enable line_length
|
||||
|
||||
@@ -56,7 +56,7 @@ class FileLLDBInitPatcher: LLDBInitPatcher {
|
||||
}
|
||||
|
||||
private func findIndices(in collection: [String], value: String) -> [Int] {
|
||||
collection.enumerated().reduce([]) { (result, line) -> [Int] in
|
||||
collection.enumerated().reduce([]) { result, line -> [Int] in
|
||||
if line.element == Self.preambleString {
|
||||
return result + [line.offset]
|
||||
}
|
||||
@@ -75,7 +75,7 @@ class FileLLDBInitPatcher: LLDBInitPatcher {
|
||||
var contentLines = originalContentLines
|
||||
let preambleIndices = findIndices(in: contentLines, value: Self.preambleString)
|
||||
|
||||
if preambleIndices.count > 0 {
|
||||
if !preambleIndices.isEmpty {
|
||||
let firstLLDBCommandIndex = preambleIndices[0] + 1
|
||||
if firstLLDBCommandIndex >= contentLines.count {
|
||||
// corrupted file, append the script line at the bottom
|
||||
|
||||
@@ -64,6 +64,7 @@ public class XCIntegrate {
|
||||
self.output = output
|
||||
}
|
||||
|
||||
// swiftlint:disable:next function_body_length
|
||||
public func main() {
|
||||
do {
|
||||
let env = ProcessInfo.processInfo.environment
|
||||
@@ -114,7 +115,7 @@ public class XCIntegrate {
|
||||
|
||||
let integrator = XcodeProjIntegrate(
|
||||
project: context.projectPath,
|
||||
mode:context.mode,
|
||||
mode: context.mode,
|
||||
binaries: context.binaries,
|
||||
configurationIncludeOracle: configurationOracle,
|
||||
targetIncludeOracle: targetOracle,
|
||||
|
||||
@@ -114,7 +114,9 @@ struct XcodeProjIntegrate: Integrate {
|
||||
markPhase = PBXShellScriptBuildPhase(
|
||||
name: "\(Self.BuildStepPrefix)RemoteCache_mark",
|
||||
inputPaths: [binaries.prepare.path],
|
||||
shellScript: "\"$SCRIPT_INPUT_FILE_0\" mark --configuration \"$CONFIGURATION\" --platform \"$PLATFORM_NAME\""
|
||||
shellScript:
|
||||
"\"$SCRIPT_INPUT_FILE_0\" mark " +
|
||||
"--configuration \"$CONFIGURATION\" --platform \"$PLATFORM_NAME\""
|
||||
)
|
||||
}
|
||||
|
||||
@@ -129,6 +131,7 @@ struct XcodeProjIntegrate: Integrate {
|
||||
try encodedYAML.write(to: configOverrideLocation, atomically: false, encoding: .utf8)
|
||||
}
|
||||
|
||||
// swiftlint:disable:next function_body_length
|
||||
func run() throws {
|
||||
let outputFile = output ?? projectURL
|
||||
let projectRoot = projectURL.deletingLastPathComponent()
|
||||
|
||||
@@ -100,7 +100,7 @@ struct XcodeSettingsCFlags: XcodeSettingsFlags {
|
||||
case (.some(let existing), _):
|
||||
var flagsComponents: [String] = existing.split(separator: " ").map(String.init)
|
||||
// remove (if exists)
|
||||
let existingFlagIndex = flagsComponents.firstIndex { (component) -> Bool in
|
||||
let existingFlagIndex = flagsComponents.firstIndex { component -> Bool in
|
||||
component.hasPrefix("\(Self.prefix)\(key)=")
|
||||
}
|
||||
if let index = existingFlagIndex {
|
||||
|
||||
@@ -38,6 +38,7 @@ public class XCPrepareMark {
|
||||
self.commit = commit
|
||||
}
|
||||
|
||||
// swiftlint:disable:next function_body_length
|
||||
public func main() {
|
||||
let env = ProcessInfo.processInfo.environment
|
||||
let fileManager = FileManager.default
|
||||
|
||||
@@ -120,9 +120,9 @@ class Swiftc: SwiftcProtocol {
|
||||
let prebuildDiscoveryURL = context.tempDir.appendingPathComponent(context.prebuildDependenciesPath)
|
||||
let prebuildDiscoverWriter = dependenciesWriterFactory(prebuildDiscoveryURL, fileManager)
|
||||
try prebuildDiscoverWriter.write(skipForSha: remoteCommit)
|
||||
case .consumer, .producer:
|
||||
// Never skips prebuild phase and fallbacks to the swiftc compilation for:
|
||||
// 1) Not enabled remote cache or 2) producer
|
||||
case .consumer, .producer, .producerFast:
|
||||
// Never skip prebuild phase and fallback to the swiftc compilation for:
|
||||
// 1) Not enabled remote cache, 2) producer(s)
|
||||
break
|
||||
}
|
||||
return .forceFallback
|
||||
|
||||
@@ -24,6 +24,8 @@ public struct SwiftcContext {
|
||||
case producer
|
||||
/// Commit sha of the commit to use during remote cache
|
||||
case consumer(commit: RemoteCommitInfo)
|
||||
/// Remote artifact exists and can be optimistically used in place of a local compilation
|
||||
case producerFast
|
||||
}
|
||||
|
||||
let objcHeaderOutput: URL
|
||||
@@ -74,6 +76,14 @@ public struct SwiftcContext {
|
||||
mode = .consumer(commit: remoteCommit)
|
||||
case .producer:
|
||||
mode = .producer
|
||||
case .producerFast:
|
||||
let remoteCommit = RemoteCommitInfo(try? String(contentsOf: remoteCommitLocation).trim())
|
||||
switch remoteCommit {
|
||||
case .unavailable:
|
||||
mode = .producer
|
||||
case .available:
|
||||
mode = .producerFast
|
||||
}
|
||||
}
|
||||
invocationHistoryFile = URL(fileURLWithPath: config.compilationHistoryFile, relativeTo: tempDir)
|
||||
}
|
||||
|
||||
@@ -116,6 +116,12 @@ class SwiftcOrchestrator {
|
||||
}
|
||||
case .consumer:
|
||||
fallbackToDefault(command: swiftcCommand)
|
||||
case .producerFast:
|
||||
let compileStepResult = try swiftc.mockCompilation()
|
||||
if case .forceFallback = compileStepResult {
|
||||
// cannot reuse cached artifact. Build it locally and upload to the server just as for the producer
|
||||
fallthrough
|
||||
}
|
||||
case .producer:
|
||||
var swiftcArgs = ProcessInfo().arguments
|
||||
swiftcArgs = try producerFallbackCommandProcessors.reduce(swiftcArgs) { args, processor in
|
||||
|
||||
@@ -101,7 +101,10 @@ public class XCSwiftc {
|
||||
objcHeaderOutput: context.objcHeaderOutput,
|
||||
diskCopier: HardLinkDiskCopier(fileManager: fileManager)
|
||||
)
|
||||
let allInvocationsStorage = ExistingFileStorage(storageFile: context.invocationHistoryFile, command: swiftcCommand)
|
||||
let allInvocationsStorage = ExistingFileStorage(
|
||||
storageFile: context.invocationHistoryFile,
|
||||
command: swiftcCommand
|
||||
)
|
||||
// When fallbacking to local compilation do not call historical `swiftc` invocations
|
||||
// The current fallback invocation already compiles all files in a target
|
||||
let invocationStorage = FilteredInvocationStorage(
|
||||
|
||||
@@ -20,4 +20,5 @@
|
||||
public enum Mode: String, Codable, CaseIterable {
|
||||
case consumer
|
||||
case producer
|
||||
case producerFast = "producer-fast"
|
||||
}
|
||||
|
||||
@@ -94,10 +94,11 @@ public struct XCRemoteCacheConfig: Encodable {
|
||||
var focusedTargets: [String] = []
|
||||
/// Disable cache for http requests to fecth metadata and download artifacts
|
||||
var disableHttpCache: Bool = false
|
||||
/// Path, relative to $TARGET_TEMP_DIR which gathers all compilation commands that should be executed if a target
|
||||
/// switches to local compilation. Example: A new `.swift` file invalidates remote artifact and triggers local compilation
|
||||
/// When that happens, all previously skipped clang build steps need to be eventually called locally - this file lists
|
||||
/// all these commands.
|
||||
/// Path, relative to $TARGET_TEMP_DIR which gathers all compilation commands that should be e
|
||||
/// xecuted if a target switches to local compilation.
|
||||
/// Example: A new `.swift` file invalidates remote arXcodeProjIntegrate.swifttifact and triggers local compilation
|
||||
/// When that happens, all previously skipped clang build steps
|
||||
/// need to be eventually called locally - this file lists all these commands.
|
||||
var compilationHistoryFile: String = "history.compile"
|
||||
/// Timeout for remote response data interval (in seconds). If an interval between data chunks is
|
||||
/// longer than a timeout, a request fails
|
||||
@@ -124,6 +125,7 @@ public struct XCRemoteCacheConfig: Encodable {
|
||||
extension XCRemoteCacheConfig {
|
||||
/// Merges existing config with the other config and returns a final result
|
||||
/// `other` scheme overrides existing configuration
|
||||
// swiftlint:disable:next function_body_length
|
||||
func merged(with scheme: ConfigFileScheme) -> XCRemoteCacheConfig {
|
||||
var merge = self
|
||||
merge.mode = scheme.mode ?? mode
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2021 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol MetaWriter {
|
||||
func write<T>(_ meta: T, locationDir : URL) throws -> URL where T : Meta
|
||||
}
|
||||
|
||||
class JsonMetaWriter: MetaWriter {
|
||||
private let metaEncoder = JSONEncoder()
|
||||
private let fileWriter: FileWriter
|
||||
|
||||
init(fileWriter: FileWriter) {
|
||||
self.fileWriter = fileWriter
|
||||
}
|
||||
|
||||
func write<T>(_ meta: T, locationDir : URL) throws -> URL where T : Meta {
|
||||
let metaURL = locationDir.appendingPathComponent(meta.fileKey).appendingPathExtension("json")
|
||||
let metaData = try metaEncoder.encode(meta)
|
||||
try fileWriter.write(toPath: metaURL.path, contents: metaData)
|
||||
return metaURL
|
||||
}
|
||||
}
|
||||
@@ -35,9 +35,22 @@ struct AWSV4Signature {
|
||||
request.setValue((request.httpBody ?? Data()).sha256(), forHTTPHeaderField: "x-amz-content-sha256")
|
||||
|
||||
let canonicalRequest = CanonicalRequest(request: request)
|
||||
let stringToSign = StringToSign(region: region, service: service, canonicalRequestHash: canonicalRequest.hash, date: date)
|
||||
let awsV4SigningKey = AWSV4SigningKey(secretAccessKey: secretKey, region: region, service: service, date: date)
|
||||
let signature = HMAC.calcHMAC(keyArray: awsV4SigningKey.value, value: stringToSign.value).map { String(format: "%02hhx", $0) }.joined()
|
||||
let stringToSign = StringToSign(
|
||||
region: region,
|
||||
service: service,
|
||||
canonicalRequestHash: canonicalRequest.hash,
|
||||
date: date
|
||||
)
|
||||
let awsV4SigningKey = AWSV4SigningKey(
|
||||
secretAccessKey: secretKey,
|
||||
region: region,
|
||||
service: service,
|
||||
date: date
|
||||
)
|
||||
let signature = HMAC.calcHMAC(
|
||||
keyArray: awsV4SigningKey.value,
|
||||
value: stringToSign.value
|
||||
).map { String(format: "%02hhx", $0) }.joined()
|
||||
|
||||
let authValue =
|
||||
"AWS4-HMAC-SHA256 " +
|
||||
|
||||
@@ -52,7 +52,14 @@ struct HMAC {
|
||||
|
||||
private static func calcHMAC(keyUnsafeBytes: UnsafeRawBufferPointer, value: String, out: UnsafeMutableRawPointer!) {
|
||||
value.data(using: .utf8)!.withUnsafeBytes { value in
|
||||
CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA256), keyUnsafeBytes.baseAddress, Int(keyUnsafeBytes.count), value.baseAddress, Int(value.count), out)
|
||||
CCHmac(
|
||||
CCHmacAlgorithm(kCCHmacAlgSHA256),
|
||||
keyUnsafeBytes.baseAddress,
|
||||
Int(keyUnsafeBytes.count),
|
||||
value.baseAddress,
|
||||
Int(value.count),
|
||||
out
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ class RemoteNetworkClientAbstractFactory {
|
||||
return RemoteNetworkClientImpl(networkClient, downloadURLBuilder)
|
||||
}
|
||||
switch mode {
|
||||
case .producer:
|
||||
case .producer, .producerFast:
|
||||
let upstreamBuilders = try upstreamStreamURL.map(urlBuilderFactory)
|
||||
return ReplicatedRemotesNetworkClient(
|
||||
networkClient,
|
||||
|
||||
@@ -28,8 +28,8 @@ protocol CacheHitLogger {
|
||||
/// Logs target hit or miss, based on an action of a build
|
||||
class ActionSpecificCacheHitLogger: CacheHitLogger {
|
||||
private let statsLogger: StatsLogger
|
||||
private let hitCounter: XCRemoteCacheStatistics.Counter
|
||||
private let missCounter: XCRemoteCacheStatistics.Counter
|
||||
private let hitCounter: XCRemoteCacheStatistics.Counter?
|
||||
private let missCounter: XCRemoteCacheStatistics.Counter?
|
||||
|
||||
init(action: BuildActionType, statsLogger: StatsLogger) {
|
||||
self.statsLogger = statsLogger
|
||||
@@ -37,19 +37,24 @@ class ActionSpecificCacheHitLogger: CacheHitLogger {
|
||||
case .index:
|
||||
hitCounter = .indexingTargetHitCount
|
||||
missCounter = .indexingTargetMissCount
|
||||
case .unknown:
|
||||
fallthrough
|
||||
case .build:
|
||||
hitCounter = .targetCacheHit
|
||||
missCounter = .targetCacheMiss
|
||||
case .unknown:
|
||||
hitCounter = nil
|
||||
missCounter = nil
|
||||
}
|
||||
}
|
||||
|
||||
func logHit() throws {
|
||||
try statsLogger.log(hitCounter)
|
||||
if let hitCounter = hitCounter {
|
||||
try statsLogger.log(hitCounter)
|
||||
}
|
||||
}
|
||||
|
||||
func logMiss() throws {
|
||||
try statsLogger.log(missCounter)
|
||||
if let missCounter = missCounter {
|
||||
try statsLogger.log(missCounter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ class ArtifactSwiftProductsBuilderImplTests: FileXCTestCase {
|
||||
private var builder: ArtifactSwiftProductsBuilderImpl!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
let rootDir = try prepareTempDir()
|
||||
moduleDir = rootDir.appendingPathComponent("Products")
|
||||
swiftmoduleFile = moduleDir.appendingPathComponent("MyModule.swiftmodule")
|
||||
@@ -47,24 +48,41 @@ class ArtifactSwiftProductsBuilderImplTests: FileXCTestCase {
|
||||
func testIncludesRequiredSwiftmoduleFiles() throws {
|
||||
try fileManager.spt_createFile(swiftmoduleFile, content: "swiftmodule")
|
||||
try fileManager.spt_createFile(swiftmoduleDocFile, content: "swiftdoc")
|
||||
let builderSwiftmoduleDir = builder.buildingArtifactSwiftModulesLocation().appendingPathComponent("arm64")
|
||||
let expectedBuildedSwiftmoduleFile = builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftmodule")
|
||||
let expectedBuildedSwiftmoduledocFile = builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftdoc")
|
||||
let builderSwiftmoduleDir =
|
||||
builder
|
||||
.buildingArtifactSwiftModulesLocation()
|
||||
.appendingPathComponent("arm64")
|
||||
let expectedBuildedSwiftmoduleFile =
|
||||
builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftmodule")
|
||||
let expectedBuildedSwiftmoduledocFile =
|
||||
builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftdoc")
|
||||
|
||||
try builder.includeModuleDefinitionsToTheArtifact(arch: "arm64", moduleURL: swiftmoduleFile)
|
||||
|
||||
XCTAssertEqual(fileManager.contents(atPath: expectedBuildedSwiftmoduleFile.path), "swiftmodule".data(using: .utf8))
|
||||
XCTAssertEqual(fileManager.contents(atPath: expectedBuildedSwiftmoduledocFile.path), "swiftdoc".data(using: .utf8))
|
||||
XCTAssertEqual(
|
||||
fileManager.contents(atPath: expectedBuildedSwiftmoduleFile.path),
|
||||
"swiftmodule".data(using: .utf8)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
fileManager.contents(atPath: expectedBuildedSwiftmoduledocFile.path),
|
||||
"swiftdoc".data(using: .utf8)
|
||||
)
|
||||
}
|
||||
|
||||
func testIncludesAllSwiftmoduleFiles() throws {
|
||||
try fileManager.spt_createEmptyFile(swiftmoduleFile)
|
||||
try fileManager.spt_createEmptyFile(swiftmoduleDocFile)
|
||||
try fileManager.spt_createEmptyFile(swiftmoduleSourceInfoFile)
|
||||
let builderSwiftmoduleDir = builder.buildingArtifactSwiftModulesLocation().appendingPathComponent("arm64")
|
||||
let expectedBuildedSwiftmoduleFile = builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftmodule")
|
||||
let expectedBuildedSwiftmoduledocFile = builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftdoc")
|
||||
let expectedBuildedSwiftSourceInfoFile = builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftsourceinfo")
|
||||
let builderSwiftmoduleDir =
|
||||
builder
|
||||
.buildingArtifactSwiftModulesLocation()
|
||||
.appendingPathComponent("arm64")
|
||||
let expectedBuildedSwiftmoduleFile =
|
||||
builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftmodule")
|
||||
let expectedBuildedSwiftmoduledocFile =
|
||||
builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftdoc")
|
||||
let expectedBuildedSwiftSourceInfoFile =
|
||||
builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftsourceinfo")
|
||||
|
||||
try builder.includeModuleDefinitionsToTheArtifact(arch: "arm64", moduleURL: swiftmoduleFile)
|
||||
|
||||
@@ -74,6 +92,11 @@ class ArtifactSwiftProductsBuilderImplTests: FileXCTestCase {
|
||||
}
|
||||
|
||||
func testFailsIncludingWhenMissingRequiredSwiftmoduleFiles() throws {
|
||||
XCTAssertThrowsError(try builder.includeModuleDefinitionsToTheArtifact(arch: "arm64", moduleURL: swiftmoduleFile))
|
||||
XCTAssertThrowsError(
|
||||
try builder.includeModuleDefinitionsToTheArtifact(
|
||||
arch: "arm64",
|
||||
moduleURL: swiftmoduleFile
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ class BuildArtifactCreatorTests: FileXCTestCase {
|
||||
moduleName: "Target",
|
||||
modulesFolderPath: "",
|
||||
dSYMPath: dSYM,
|
||||
metaWriter: JsonMetaWriter(fileWriter: fileManager),
|
||||
fileManager: fileManager
|
||||
)
|
||||
}
|
||||
|
||||
@@ -35,7 +35,11 @@ class ZipArtifactCreatorTests: FileXCTestCase {
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
workingDir = try prepareTempDir().appendingPathComponent("creator")
|
||||
creator = ZipArtifactCreator(workingDir: workingDir, fileManager: fileManager)
|
||||
creator = ZipArtifactCreator(
|
||||
workingDir: workingDir,
|
||||
metaWriter: JsonMetaWriter(fileWriter: fileManager),
|
||||
fileManager: fileManager
|
||||
)
|
||||
}
|
||||
|
||||
func testCreatingArtifactGeneratesValidArtifactId() throws {
|
||||
|
||||
+62
-1
@@ -33,7 +33,10 @@ class ThinningCreatorPluginTests: FileXCTestCase {
|
||||
targetTempDirRoot = workingDir.appendingPathComponent("Root")
|
||||
currentTargetTempDir = targetTempDirRoot.appendingPathComponent("Current.build")
|
||||
try fileManager.spt_createEmptyDir(currentTargetTempDir)
|
||||
plugin = ThinningCreatorPlugin(targetTempDir: currentTargetTempDir, dirScanner: FileManager.default)
|
||||
plugin = ThinningCreatorPlugin(
|
||||
targetTempDir: currentTargetTempDir,
|
||||
modeMarkerPath: "rc_marker.enabled",
|
||||
dirScanner: FileManager.default)
|
||||
}
|
||||
|
||||
func testReturnsEmptyExtraKeysForNoArtifacts() throws {
|
||||
@@ -73,4 +76,62 @@ class ThinningCreatorPluginTests: FileXCTestCase {
|
||||
|
||||
XCTAssertThrowsError(try plugin.extraMetaKeys(Self.sampleMeta))
|
||||
}
|
||||
|
||||
func testDefinesExtraMetaKeysForTargetsThatReusedArtifact() throws {
|
||||
let otherTargetTempDir = targetTempDirRoot.appendingPathComponent("Other.build")
|
||||
let marker = otherTargetTempDir.appendingPathComponent("rc_marker.enabled")
|
||||
let reusedArtifact = otherTargetTempDir
|
||||
.appendingPathComponent("xccache")
|
||||
.appendingPathComponent("123")
|
||||
.appendingPathExtension("zip")
|
||||
try fileManager.spt_createEmptyFile(marker)
|
||||
try fileManager.spt_createEmptyFile(reusedArtifact)
|
||||
|
||||
let extraKeys = try plugin.extraMetaKeys(Self.sampleMeta)
|
||||
|
||||
XCTAssertEqual(extraKeys, ["thinning_Other": "123"])
|
||||
}
|
||||
|
||||
func testFailsGeneratingExtraMetaKeysForTwoArtifactsInTargetTempDir() throws {
|
||||
let otherTargetTempDir = targetTempDirRoot.appendingPathComponent("Other.build")
|
||||
let marker = otherTargetTempDir.appendingPathComponent("rc_marker.enabled")
|
||||
let reusedArtifact1 = otherTargetTempDir
|
||||
.appendingPathComponent("xccache")
|
||||
.appendingPathComponent("001")
|
||||
.appendingPathExtension("zip")
|
||||
let reusedArtifact2 = otherTargetTempDir
|
||||
.appendingPathComponent("xccache")
|
||||
.appendingPathComponent("002")
|
||||
.appendingPathExtension("zip")
|
||||
try fileManager.spt_createEmptyFile(marker)
|
||||
try fileManager.spt_createEmptyFile(reusedArtifact1)
|
||||
try fileManager.spt_createEmptyFile(reusedArtifact2)
|
||||
|
||||
XCTAssertThrowsError(try plugin.extraMetaKeys(Self.sampleMeta))
|
||||
}
|
||||
|
||||
func testDefinesExtraMetaKeysForGeneratedAndReusedArtifact() throws {
|
||||
let otherTargetTempDir = targetTempDirRoot.appendingPathComponent("Generated.build")
|
||||
let generatedArtifact = otherTargetTempDir
|
||||
.appendingPathComponent("xccache")
|
||||
.appendingPathComponent("produced")
|
||||
.appendingPathComponent("000")
|
||||
.appendingPathExtension("zip")
|
||||
try fileManager.spt_createEmptyFile(generatedArtifact)
|
||||
let reusedTargetTempDir = targetTempDirRoot.appendingPathComponent("Reused.build")
|
||||
let marker = reusedTargetTempDir.appendingPathComponent("rc_marker.enabled")
|
||||
let reusedArtifact = reusedTargetTempDir
|
||||
.appendingPathComponent("xccache")
|
||||
.appendingPathComponent("999")
|
||||
.appendingPathExtension("zip")
|
||||
try fileManager.spt_createEmptyFile(marker)
|
||||
try fileManager.spt_createEmptyFile(reusedArtifact)
|
||||
|
||||
let extraKeys = try plugin.extraMetaKeys(Self.sampleMeta)
|
||||
|
||||
XCTAssertEqual(extraKeys, [
|
||||
"thinning_Generated": "000",
|
||||
"thinning_Reused": "999"
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,8 @@ class PostbuildTests: FileXCTestCase {
|
||||
bundleDir: nil,
|
||||
derivedSourcesDir: "",
|
||||
thinnedTargets: [],
|
||||
action: .build
|
||||
action: .build,
|
||||
modeMarkerPath: ""
|
||||
)
|
||||
private var network = RemoteNetworkClientImpl(
|
||||
NetworkClientFake(fileManager: .default),
|
||||
@@ -84,6 +85,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
)
|
||||
private var modeController = CacheModeControllerFake()
|
||||
private var metaReader = JsonMetaReader(fileAccessor: FileManager.default)
|
||||
private var metaWriter = JsonMetaWriter(fileWriter: FileManager.default)
|
||||
private static let SampleMeta = MainArtifactSampleMeta.defaults
|
||||
private var sampleMetaFile: URL!
|
||||
|
||||
@@ -122,6 +124,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: []
|
||||
)
|
||||
@@ -151,6 +154,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: []
|
||||
)
|
||||
@@ -178,6 +182,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: dsymOrganizer,
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: []
|
||||
)
|
||||
@@ -205,6 +210,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: dsymOrganizer,
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: []
|
||||
)
|
||||
@@ -242,6 +248,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: dsymOrganizer,
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: []
|
||||
)
|
||||
@@ -282,6 +289,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: dsymOrganizer,
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: []
|
||||
)
|
||||
@@ -307,6 +315,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: fakeModeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: []
|
||||
)
|
||||
@@ -354,6 +363,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [plugin],
|
||||
consumerPlugins: []
|
||||
)
|
||||
@@ -385,6 +395,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [plugin],
|
||||
consumerPlugins: []
|
||||
)
|
||||
@@ -417,6 +428,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: [consumerPlugin]
|
||||
)
|
||||
@@ -454,6 +466,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: [consumerPlugin]
|
||||
)
|
||||
@@ -485,6 +498,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: [consumerPlugin]
|
||||
)
|
||||
@@ -518,6 +532,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: []
|
||||
)
|
||||
@@ -550,6 +565,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: []
|
||||
)
|
||||
@@ -558,5 +574,68 @@ class PostbuildTests: FileXCTestCase {
|
||||
try postbuild.deleteFingerprintOverrides()
|
||||
|
||||
XCTAssertFalse(fileManager.fileExists(atPath: previousFingerprintOverride.path))
|
||||
} // swiftlint:disable:next file_length
|
||||
}
|
||||
|
||||
func testUploadingMeta() throws {
|
||||
let postbuild = Postbuild(
|
||||
context: postbuildContext,
|
||||
networkClient: network,
|
||||
remapper: remapper,
|
||||
fingerprintAccumulator: fingerprintGenerator,
|
||||
artifactsOrganizer: organizer,
|
||||
artifactCreator: artifactCreator,
|
||||
fingerprintSyncer: syncer,
|
||||
dependenciesReader: dependenciesReader,
|
||||
dependencyProcessor: processor,
|
||||
fingerprintOverrideManager: overrideManager,
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: []
|
||||
)
|
||||
|
||||
try postbuild.performMetaUpload(meta: Self.SampleMeta, for: "33")
|
||||
|
||||
|
||||
let data = try network.fetch(.meta(commit: "33"))
|
||||
let downloadedMeta = try metaReader.read(data: data)
|
||||
|
||||
XCTAssertEqual(downloadedMeta, Self.SampleMeta)
|
||||
}
|
||||
|
||||
func testUploadingMetaWithNewPluginKeys() throws {
|
||||
let plugin = MetaAppenderArtifactCreatorPlugin(["New": "Value"])
|
||||
let postbuild = Postbuild(
|
||||
context: postbuildContext,
|
||||
networkClient: network,
|
||||
remapper: remapper,
|
||||
fingerprintAccumulator: fingerprintGenerator,
|
||||
artifactsOrganizer: organizer,
|
||||
artifactCreator: artifactCreator,
|
||||
fingerprintSyncer: syncer,
|
||||
dependenciesReader: dependenciesReader,
|
||||
dependencyProcessor: processor,
|
||||
fingerprintOverrideManager: overrideManager,
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [plugin],
|
||||
consumerPlugins: []
|
||||
)
|
||||
var meta = Self.SampleMeta
|
||||
meta.pluginsKeys = ["Previous": "Value"]
|
||||
var expectedMeta = meta
|
||||
expectedMeta.pluginsKeys = ["New": "Value"]
|
||||
|
||||
try postbuild.performMetaUpload(meta: meta, for: "33")
|
||||
|
||||
let data = try network.fetch(.meta(commit: "33"))
|
||||
let downloadedMeta = try metaReader.read(data: data)
|
||||
|
||||
XCTAssertEqual(downloadedMeta, expectedMeta)
|
||||
}
|
||||
}
|
||||
// swiftlint:disable:next file_length
|
||||
|
||||
@@ -28,6 +28,7 @@ class FileLLDBInitPatcherTests: XCTestCase {
|
||||
private var patcher: FileLLDBInitPatcher!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
accessor = FileAccessorFake(mode: .normal)
|
||||
patcher = FileLLDBInitPatcher(
|
||||
file: lldbInitPath,
|
||||
|
||||
@@ -62,4 +62,19 @@ class SwiftcContextTests: FileXCTestCase {
|
||||
|
||||
XCTAssertEqual(context.mode, .consumer(commit: .unavailable))
|
||||
}
|
||||
|
||||
func testProducerModeWhenFileWithCommitShaExistsIsResolvedToProducerFast() throws {
|
||||
config.mode = .producerFast
|
||||
let context = try SwiftcContext(config: config, input: input)
|
||||
|
||||
XCTAssertEqual(context.mode, .producerFast)
|
||||
}
|
||||
|
||||
func testProducerModeWhenFileWithCommitShaDoesntExxistIsResolvedToProducer() throws {
|
||||
config.mode = .producerFast
|
||||
try fileManager.spt_deleteItem(at: remoteCommitFile)
|
||||
let context = try SwiftcContext(config: config, input: input)
|
||||
|
||||
XCTAssertEqual(context.mode, .producer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
import XCTest
|
||||
|
||||
class SwiftcFilemapInputEditorTests: FileXCTestCase {
|
||||
|
||||
private let sampleInfo = SwiftCompilationInfo(info: SwiftModuleCompilationInfo(
|
||||
private let sampleInfo = SwiftCompilationInfo(
|
||||
info: SwiftModuleCompilationInfo(
|
||||
dependencies: nil,
|
||||
swiftDependencies: "/"
|
||||
), files: [])
|
||||
@@ -65,17 +65,19 @@ class SwiftcFilemapInputEditorTests: FileXCTestCase {
|
||||
}
|
||||
}
|
||||
"""#.data(using: .utf8)!
|
||||
let expectedInfo = SwiftCompilationInfo(info: SwiftModuleCompilationInfo(
|
||||
dependencies: "/master.d",
|
||||
swiftDependencies: "/master.swiftdeps"
|
||||
), files: [
|
||||
SwiftFileCompilationInfo(
|
||||
file: "/file1.swift",
|
||||
dependencies: "/file1.d",
|
||||
object: "/file1.o",
|
||||
swiftDependencies: "/file1.swiftdeps"
|
||||
let expectedInfo = SwiftCompilationInfo(
|
||||
info: SwiftModuleCompilationInfo(
|
||||
dependencies: "/master.d",
|
||||
swiftDependencies: "/master.swiftdeps"
|
||||
),
|
||||
])
|
||||
files: [
|
||||
SwiftFileCompilationInfo(
|
||||
file: "/file1.swift",
|
||||
dependencies: "/file1.d",
|
||||
object: "/file1.o",
|
||||
swiftDependencies: "/file1.swiftdeps"
|
||||
),
|
||||
])
|
||||
try fileManager.spt_writeToFile(atPath: inputFile.path, contents: infoContentData)
|
||||
|
||||
let readInfo = try editor.read()
|
||||
@@ -93,17 +95,18 @@ class SwiftcFilemapInputEditorTests: FileXCTestCase {
|
||||
}
|
||||
|
||||
func testWritingSavesContentWithOptionalParameters() throws {
|
||||
let extendedInfo = SwiftCompilationInfo(info: SwiftModuleCompilationInfo(
|
||||
dependencies: "/master.d",
|
||||
swiftDependencies: "/master.swiftdeps"
|
||||
), files: [
|
||||
SwiftFileCompilationInfo(
|
||||
file: "/file1.swift",
|
||||
dependencies: "/file1.d",
|
||||
object: "/file1.o",
|
||||
swiftDependencies: "/file1.swiftdeps"
|
||||
),
|
||||
])
|
||||
let extendedInfo = SwiftCompilationInfo(
|
||||
info: SwiftModuleCompilationInfo(
|
||||
dependencies: "/master.d",
|
||||
swiftDependencies: "/master.swiftdeps"
|
||||
), files: [
|
||||
SwiftFileCompilationInfo(
|
||||
file: "/file1.swift",
|
||||
dependencies: "/file1.d",
|
||||
object: "/file1.o",
|
||||
swiftDependencies: "/file1.swiftdeps"
|
||||
),
|
||||
])
|
||||
|
||||
try editor.write(extendedInfo)
|
||||
|
||||
|
||||
@@ -190,4 +190,45 @@ class SwiftcOrchestratorTests: XCTestCase {
|
||||
|
||||
XCTAssertNotNil(shellOutSpy.switchedProcess)
|
||||
}
|
||||
|
||||
func testForFailedCompilationMockInProducerFastModeBuildsArtifactObjCHeader() throws {
|
||||
let swiftc = SwiftcMock(mockingResult: .forceFallback)
|
||||
let orchestrator = SwiftcOrchestrator(
|
||||
mode: .producerFast,
|
||||
swiftc: swiftc,
|
||||
swiftcCommand: "",
|
||||
objcHeaderOutput: objcHeaderURL,
|
||||
moduleOutput: moduleOutputURL,
|
||||
arch: "archTest",
|
||||
artifactBuilder: artifactBuilder,
|
||||
producerFallbackCommandProcessors: [],
|
||||
invocationStorage: invocationStorage,
|
||||
shellOut: shellOutSpy
|
||||
)
|
||||
|
||||
try orchestrator.run()
|
||||
|
||||
XCTAssertEqual(artifactBuilder.addedObjCHeaders, ["archTest": [objcHeaderURL]])
|
||||
}
|
||||
|
||||
func testSuccessedMockInProducerFastModeDoesntFillObjCHeader() throws {
|
||||
let swiftc = SwiftcMock(mockingResult: .success)
|
||||
let orchestrator = SwiftcOrchestrator(
|
||||
mode: .producerFast,
|
||||
swiftc: swiftc,
|
||||
swiftcCommand: "",
|
||||
objcHeaderOutput: objcHeaderURL,
|
||||
moduleOutput: moduleOutputURL,
|
||||
arch: "arch",
|
||||
artifactBuilder: artifactBuilder,
|
||||
producerFallbackCommandProcessors: [],
|
||||
invocationStorage: invocationStorage,
|
||||
shellOut: shellOutSpy
|
||||
)
|
||||
|
||||
try orchestrator.run()
|
||||
|
||||
XCTAssertEqual(artifactBuilder.addedObjCHeaders, [:])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -277,7 +277,9 @@ class SwiftcTests: FileXCTestCase {
|
||||
let artifactObjCHeader = URL(fileURLWithPath: "/cachedArtifact/include/archTest/Target-Swift.h")
|
||||
let artifactSwiftmodule = URL(fileURLWithPath: "/cachedArtifact/swiftmodule/archTest/Target.swiftmodule")
|
||||
let artifactSwiftdoc = URL(fileURLWithPath: "/cachedArtifact/swiftmodule/archTest/Target.swiftdoc")
|
||||
let artifactSwiftSourceInfo = URL(fileURLWithPath: "/cachedArtifact/swiftmodule/archTest/Target.swiftsourceinfo")
|
||||
let artifactSwiftSourceInfo = URL(
|
||||
fileURLWithPath: "/cachedArtifact/swiftmodule/archTest/Target.swiftsourceinfo"
|
||||
)
|
||||
|
||||
artifactOrganizer = ArtifactOrganizerFake(artifactRoot: artifactRoot)
|
||||
let swiftc = Swiftc(
|
||||
@@ -457,5 +459,5 @@ class SwiftcTests: FileXCTestCase {
|
||||
)
|
||||
|
||||
XCTAssertNoThrow(try swiftc.mockCompilation())
|
||||
}
|
||||
} // swiftlint:disable:next file_length
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) 2021 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
@testable import XCRemoteCache
|
||||
import XCTest
|
||||
import Yams
|
||||
|
||||
class ModeTests: XCTestCase {
|
||||
|
||||
func testProducerFast() throws {
|
||||
let yaml = "producer-fast"
|
||||
let decoder = YAMLDecoder(encoding: .utf8)
|
||||
let mode: Mode = try decoder.decode(from: yaml)
|
||||
|
||||
XCTAssertEqual(mode, .producerFast)
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ class FileFingerprintSyncerTests: FileXCTestCase {
|
||||
private var swiftmoduleDir: URL!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
syncer = FileFingerprintSyncer(
|
||||
fingerprintOverrideExtension: "md5",
|
||||
dirAccessor: fileManager,
|
||||
|
||||
@@ -27,6 +27,7 @@ class TargetDependenciesReaderTests: XCTestCase {
|
||||
private var reader: TargetDependenciesReader!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
dirAccessor = DirAccessorFake()
|
||||
/// A Factory that builds a faked dependency reader that returns a single dependency,
|
||||
/// a basename of the input .d file and the ".swift" extension
|
||||
|
||||
@@ -27,6 +27,7 @@ class CopyDiskCopierTests: FileXCTestCase {
|
||||
private var emptySourceFile: URL!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
workingDir = try prepareTempDir()
|
||||
emptySourceFile = workingDir.appendingPathComponent("source")
|
||||
try fileManager.spt_writeToFile(atPath: emptySourceFile.path, contents: Data())
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) 2021 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
@testable import XCRemoteCache
|
||||
import XCTest
|
||||
|
||||
class JsonMetaWriterTests: XCTestCase {
|
||||
|
||||
func testWritesToFileWithFilekeyFilename() throws {
|
||||
let fileAccessor = FileAccessorFake(mode: .normal)
|
||||
let writer = JsonMetaWriter(fileWriter: fileAccessor)
|
||||
let workingDir: URL = "/"
|
||||
let meta = MainArtifactSampleMeta.defaults
|
||||
|
||||
let url = try writer.write(MainArtifactSampleMeta.defaults, locationDir: workingDir)
|
||||
|
||||
XCTAssertEqual(url, workingDir.appendingPathComponent(meta.fileKey).appendingPathExtension("json"))
|
||||
}
|
||||
|
||||
func testWritesMetaInValidFormat() throws {
|
||||
let fileAccessor = FileAccessorFake(mode: .normal)
|
||||
let writer = JsonMetaWriter(fileWriter: fileAccessor)
|
||||
let reader = JsonMetaReader(fileAccessor: fileAccessor)
|
||||
let workingDir: URL = "/"
|
||||
let meta = MainArtifactSampleMeta.defaults
|
||||
|
||||
let url = try writer.write(MainArtifactSampleMeta.defaults, locationDir: workingDir)
|
||||
|
||||
let readMeta = try reader.read(localFile: url)
|
||||
XCTAssertEqual(readMeta, meta)
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ class FilteredInvocationStorageTests: XCTestCase {
|
||||
var storage: FilteredInvocationStorage!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
storage = FilteredInvocationStorage(storage: underlyingStorage, retrieveIgnoredCommands: ["to_ignore"])
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ class InvocationFileStorageTests: FileXCTestCase {
|
||||
private var storage: ExistingFileStorage!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
file = try prepareTempDir().appendingPathComponent("file.history")
|
||||
try fileManager.spt_createEmptyFile(file)
|
||||
storage = ExistingFileStorage(storageFile: file, command: command)
|
||||
|
||||
@@ -25,6 +25,7 @@ class ActionSpecificCacheHitLoggerTests: FileXCTestCase {
|
||||
private var coordinator: StatsCoordinator!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
coordinator = InMemoryStatsCoordinator()
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,14 @@ The CocoaPods plugin that integrates XCRemoteCache with the project.
|
||||
|
||||
## Installation
|
||||
|
||||
### Using RubyGems
|
||||
|
||||
```bash
|
||||
gem install cocoapods-xcremotecache
|
||||
```
|
||||
|
||||
### From sources
|
||||
|
||||
Build & install the plugin
|
||||
|
||||
```bash
|
||||
@@ -52,3 +60,7 @@ To fully uninstall the plugin, call:
|
||||
```bash
|
||||
gem uninstall cocoapods-xcremotecache
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
* When `generate_multiple_pod_projects` mode is enabled, only first-party targets are cached by XCRemoteCache (all dependencies are compiled locally).
|
||||
|
||||
@@ -46,17 +46,17 @@ module CocoapodsXCRemoteCacheModifier
|
||||
@@configuration = c
|
||||
end
|
||||
|
||||
def self.set_configuration_default_values(user_proj_directory)
|
||||
def self.set_configuration_default_values
|
||||
default_values = {
|
||||
'mode' => 'consumer',
|
||||
'enabled' => true,
|
||||
'xcrc_location' => "#{user_proj_directory}/XCRC",
|
||||
'xcrc_location' => "XCRC",
|
||||
'exclude_build_configurations' => [],
|
||||
'check_build_configuration' => 'Debug',
|
||||
'check_platform' => 'iphonesimulator',
|
||||
'modify_lldb_init' => true,
|
||||
'xccc_file' => "#{user_proj_directory}/#{BIN_DIR}/xccc",
|
||||
'remote_commit_file' => "#{user_proj_directory}/#{BIN_DIR}/arc.rc",
|
||||
'xccc_file' => "#{BIN_DIR}/xccc",
|
||||
'remote_commit_file' => "#{BIN_DIR}/arc.rc",
|
||||
'exclude_targets' => [],
|
||||
}
|
||||
@@configuration.merge! default_values.select { |k, v| !@@configuration.key?(k) }
|
||||
@@ -89,61 +89,95 @@ module CocoapodsXCRemoteCacheModifier
|
||||
@@configuration.select { |key, value| !CUSTOM_CONFIGURATION_KEYS.include?(key) }
|
||||
end
|
||||
|
||||
def self.enable_xcremotecache(target, user_proj_directory, xc_location, xc_cc_path, mode, exclude_build_configurations, check_build_configuration, check_platform, final_target)
|
||||
target.build_configurations.each do |config|
|
||||
# apply only for relevant Configurations
|
||||
next if exclude_build_configurations.include?(config.name)
|
||||
if mode == 'consumer'
|
||||
config.build_settings['CC'] = [xc_cc_path]
|
||||
end
|
||||
config.build_settings['SWIFT_EXEC'] = ["#{xc_location}/xcswiftc"]
|
||||
config.build_settings['LIBTOOL'] = ["#{xc_location}/xclibtool"]
|
||||
config.build_settings['LD'] = ["#{xc_location}/xcld"]
|
||||
def self.parent_dir(path, parent_count)
|
||||
"../" * parent_count + path
|
||||
end
|
||||
|
||||
config.build_settings['XCREMOTE_CACHE_FAKE_SRCROOT'] = FAKE_SRCROOT
|
||||
add_cflags!(config.build_settings, '-fdebug-prefix-map', "#{user_proj_directory}=$(XCREMOTE_CACHE_FAKE_SRCROOT)")
|
||||
add_swiftflags!(config.build_settings, '-debug-prefix-map', "#{user_proj_directory}=$(XCREMOTE_CACHE_FAKE_SRCROOT)")
|
||||
end
|
||||
|
||||
# @param target [Target] target to apply XCRemoteCache
|
||||
# @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' 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)
|
||||
srcroot_relative_xc_location = parent_dir(xc_location, repo_distance)
|
||||
|
||||
# User project is not generated from scratch (contrary to `Pods`), delete all previous XCRemoteCache phases
|
||||
target.build_phases.delete_if {|phase|
|
||||
# Some phases (e.g. PBXSourcesBuildPhase) don't have strict name check respond_to?
|
||||
if phase.respond_to?(:name)
|
||||
phase.name != nil && phase.name.start_with?("[XCRC]")
|
||||
target.build_configurations.each do |config|
|
||||
# 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)}"]
|
||||
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['XCREMOTE_CACHE_FAKE_SRCROOT'] = FAKE_SRCROOT
|
||||
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)")
|
||||
end
|
||||
|
||||
# Prebuild
|
||||
if mode == 'consumer'
|
||||
prebuild_script = target.new_shell_script_build_phase("[XCRC] Prebuild")
|
||||
existing_prebuild_script = target.build_phases.detect do |phase|
|
||||
if phase.respond_to?(:name)
|
||||
phase.name != nil && phase.name.start_with?("[XCRC] Prebuild")
|
||||
end
|
||||
end
|
||||
prebuild_script = existing_prebuild_script || target.new_shell_script_build_phase("[XCRC] Prebuild")
|
||||
prebuild_script.shell_script = "\"$SCRIPT_INPUT_FILE_0\""
|
||||
prebuild_script.input_paths = ["#{xc_location}/xcprebuild"]
|
||||
prebuild_script.input_paths = ["$SRCROOT/#{srcroot_relative_xc_location}/xcprebuild"]
|
||||
prebuild_script.output_paths = [
|
||||
"$(TARGET_TEMP_DIR)/rc.enabled",
|
||||
"$(TARGET_TEMP_DIR)/rc.enabled",
|
||||
"$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)"
|
||||
]
|
||||
prebuild_script.dependency_file = "$(TARGET_TEMP_DIR)/prebuild.d"
|
||||
|
||||
# Move prebuild (last element) to the first position (to make it real 'prebuild')
|
||||
target.build_phases.rotate!(-1)
|
||||
target.build_phases.rotate!(-1) if existing_prebuild_script.nil?
|
||||
elsif mode == 'producer'
|
||||
# Delete existing prebuild build phase (to support switching between modes)
|
||||
target.build_phases.delete_if do |phase|
|
||||
if phase.respond_to?(:name)
|
||||
phase.name != nil && phase.name.start_with?("[XCRC] Prebuild")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Postbuild
|
||||
postbuild_script = target.new_shell_script_build_phase("[XCRC] Postbuild")
|
||||
existing_postbuild_script = target.build_phases.detect do |phase|
|
||||
if phase.respond_to?(:name)
|
||||
phase.name != nil && phase.name.start_with?("[XCRC] Postbuild")
|
||||
end
|
||||
end
|
||||
postbuild_script = existing_postbuild_script || target.new_shell_script_build_phase("[XCRC] Postbuild")
|
||||
postbuild_script.shell_script = "\"$SCRIPT_INPUT_FILE_0\""
|
||||
postbuild_script.input_paths = ["#{xc_location}/xcpostbuild"]
|
||||
postbuild_script.input_paths = ["$SRCROOT/#{srcroot_relative_xc_location}/xcpostbuild"]
|
||||
postbuild_script.output_paths = [
|
||||
"$(TARGET_BUILD_DIR)/$(MODULES_FOLDER_PATH)/$(PRODUCT_MODULE_NAME).swiftmodule/$(PLATFORM_PREFERRED_ARCH).swiftmodule.md5",
|
||||
"$(TARGET_BUILD_DIR)/$(MODULES_FOLDER_PATH)/$(PRODUCT_MODULE_NAME).swiftmodule/$(PLATFORM_PREFERRED_ARCH).swiftmodule.md5",
|
||||
"$(TARGET_BUILD_DIR)/$(MODULES_FOLDER_PATH)/$(PRODUCT_MODULE_NAME).swiftmodule/$(PLATFORM_PREFERRED_ARCH)-$(LLVM_TARGET_TRIPLE_VENDOR)-$(SWIFT_PLATFORM_TARGET_PREFIX)$(LLVM_TARGET_TRIPLE_SUFFIX).swiftmodule.md5"
|
||||
]
|
||||
postbuild_script.dependency_file = "$(TARGET_TEMP_DIR)/postbuild.d"
|
||||
|
||||
# Mark a sha as ready for a given platform and configuration when building the final_target
|
||||
if mode == 'producer' && target.name == final_target
|
||||
mark_script = target.new_shell_script_build_phase("[XCRC] Mark")
|
||||
existing_mark_script = target.build_phases.detect do |phase|
|
||||
if phase.respond_to?(:name)
|
||||
phase.name != nil && phase.name.start_with?("[XCRC] Mark")
|
||||
end
|
||||
end
|
||||
mark_script = existing_mark_script || target.new_shell_script_build_phase("[XCRC] Mark")
|
||||
mark_script.shell_script = "\"$SCRIPT_INPUT_FILE_0\" mark --configuration $CONFIGURATION --platform $PLATFORM_NAME"
|
||||
mark_script.input_paths = ["#{xc_location}/xcprepare"]
|
||||
mark_script.input_paths = ["$SRCROOT/#{srcroot_relative_xc_location}/xcprepare"]
|
||||
else
|
||||
# Delete existing mark build phase (to support switching between modes or changing the final target)
|
||||
target.build_phases.delete_if do |phase|
|
||||
if phase.respond_to?(:name)
|
||||
phase.name != nil && phase.name.start_with?("[XCRC] Mark")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -251,6 +285,7 @@ module CocoapodsXCRemoteCacheModifier
|
||||
# Returns the content (array of lines) of the lldbinit with stripped XCRemoteCache rewrite
|
||||
def self.clean_lldbinit_content(lldbinit_path)
|
||||
all_lines = []
|
||||
return all_lines unless File.exist?(lldbinit_path)
|
||||
File.open(lldbinit_path) { |file|
|
||||
while(line = file.gets) != nil
|
||||
line = line.strip
|
||||
@@ -290,7 +325,7 @@ module CocoapodsXCRemoteCacheModifier
|
||||
|
||||
begin
|
||||
user_proj_directory = File.dirname(user_project.path)
|
||||
set_configuration_default_values(user_proj_directory)
|
||||
set_configuration_default_values
|
||||
|
||||
unless @@configuration['enabled']
|
||||
Pod::UI.puts "[XCRC] XCRemoteCache disabled"
|
||||
@@ -310,8 +345,12 @@ module CocoapodsXCRemoteCacheModifier
|
||||
check_build_configuration = @@configuration['check_build_configuration']
|
||||
check_platform = @@configuration['check_platform']
|
||||
|
||||
xccc_location_absolute = "#{user_proj_directory}/#{xccc_location}"
|
||||
xcrc_location_absolute = "#{user_proj_directory}/#{xcrc_location}"
|
||||
remote_commit_file_absolute = "#{user_proj_directory}/#{remote_commit_file}"
|
||||
|
||||
# Download XCRC
|
||||
download_xcrc_if_needed(xcrc_location)
|
||||
download_xcrc_if_needed(xcrc_location_absolute)
|
||||
|
||||
# Save .rcinfo
|
||||
save_rcinfo(generate_rcinfo(), user_proj_directory)
|
||||
@@ -320,12 +359,34 @@ module CocoapodsXCRemoteCacheModifier
|
||||
Dir.mkdir(BIN_DIR) unless File.exist?(BIN_DIR)
|
||||
|
||||
# Remove previous xccc & arc.rc
|
||||
File.delete(remote_commit_file) if File.exist?(remote_commit_file)
|
||||
File.delete(xccc_location) if File.exist?(xccc_location)
|
||||
File.delete(remote_commit_file_absolute) if File.exist?(remote_commit_file_absolute)
|
||||
File.delete(xccc_location_absolute) if File.exist?(xccc_location_absolute)
|
||||
|
||||
# Prepare XCRC
|
||||
|
||||
# Pods projects can be generated only once (if incremental_installation is enabled)
|
||||
# Always integrate XCRemoteCache to all Pods, in case it will be needed later
|
||||
unless installer_context.pods_project.nil?
|
||||
# Attach XCRemoteCache to Pods targets
|
||||
installer_context.pods_project.targets.each do |target|
|
||||
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)
|
||||
end
|
||||
|
||||
# Create .rcinfo into `Pods` directory as that .xcodeproj reads configuration from .xcodeproj location
|
||||
pods_proj_directory = installer_context.sandbox_root
|
||||
|
||||
# Manual .rcinfo generation (in YAML format)
|
||||
save_rcinfo({'extra_configuration_file' => "#{user_proj_directory}/.rcinfo"}, pods_proj_directory)
|
||||
|
||||
installer_context.pods_project.save()
|
||||
end
|
||||
|
||||
# Enabled/disable XCRemoteCache for the main (user) project
|
||||
begin
|
||||
prepare_result = YAML.load`#{xcrc_location}/xcprepare --configuration #{check_build_configuration} --platform #{check_platform}`
|
||||
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
|
||||
disable_xcremotecache(user_project)
|
||||
@@ -338,26 +399,11 @@ module CocoapodsXCRemoteCacheModifier
|
||||
next
|
||||
end
|
||||
|
||||
# Attach XCRemoteCache to Pods targets
|
||||
installer_context.pods_project.targets.each do |target|
|
||||
next if target.name.start_with?("Pods-")
|
||||
next if target.name.end_with?("Tests")
|
||||
next if exclude_targets.include?(target.name)
|
||||
enable_xcremotecache(target, user_proj_directory, xcrc_location, xccc_location, mode, exclude_build_configurations, check_build_configuration, check_platform, final_target)
|
||||
end
|
||||
|
||||
# Create .rcinfo into `Pods` directory as that .xcodeproj reads configuration from .xcodeproj location
|
||||
pods_proj_directory = installer_context.sandbox_root
|
||||
|
||||
# Manual .rcinfo generation (in YAML format)
|
||||
save_rcinfo({'extra_configuration_file' => "#{user_proj_directory}/.rcinfo"}, pods_proj_directory)
|
||||
|
||||
installer_context.pods_project.save()
|
||||
|
||||
# Attach XCRC to the app targets
|
||||
user_project.targets.each do |target|
|
||||
next if exclude_targets.include?(target.name)
|
||||
enable_xcremotecache(target, user_proj_directory, xcrc_location, xccc_location, mode, exclude_build_configurations, check_build_configuration, check_platform, final_target)
|
||||
enable_xcremotecache(target, 0, xcrc_location, xccc_location, mode, exclude_build_configurations, final_target)
|
||||
end
|
||||
|
||||
# Set Target sourcemap
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
# limitations under the License.
|
||||
|
||||
module CocoapodsXcremotecache
|
||||
VERSION = "0.0.1"
|
||||
VERSION = "0.0.3"
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user