Compare commits
107 Commits
v0.3.0-rc0
...
v0.3.2
| Author | SHA1 | Date | |
|---|---|---|---|
| a7316d35cc | |||
| 62a7fea0be | |||
| 88e4dceb99 | |||
| e8db767d4a | |||
| 12c635e5ca | |||
| 7f95da7b7c | |||
| 5892a92546 | |||
| 7aa44f20c1 | |||
| 9df2bd5a8e | |||
| 508b11d6ac | |||
| 5f2a8409f2 | |||
| df627ca374 | |||
| a905cdbddc | |||
| 6995c7c1b7 | |||
| 7f43cb87bd | |||
| 5ff9888c11 | |||
| ad545c7802 | |||
| 057c5c3e28 | |||
| 4116dba33d | |||
| 46cc3b75aa | |||
| 2285822ae6 | |||
| e518d28723 | |||
| 3b453b15bc | |||
| 73edc2c7aa | |||
| a0c21471b9 | |||
| 610946f0c4 | |||
| 53d4f07286 | |||
| bc9a77a58f | |||
| e0205f749a | |||
| 0d259b56d3 | |||
| 366c485453 | |||
| 8f86917597 | |||
| f9524a6854 | |||
| 9a27fa81a4 | |||
| c55f1a5803 | |||
| e4d277c8db | |||
| 6cd662bf7d | |||
| 56081f75b3 | |||
| c1cd1ac565 | |||
| 768a296175 | |||
| 6a1a8c6919 | |||
| 3d02af8ade | |||
| 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 | |||
| 41829e769f | |||
| e3261d9bb1 | |||
| b6318e9785 | |||
| 5a77ab1766 | |||
| faec67a2ff | |||
| 3e309f2b64 | |||
| 88f4d30735 | |||
| e3d45b6ad5 | |||
| 1c09a7a242 | |||
| 1468d315ab | |||
| 2077897871 | |||
| b2b1a93911 | |||
| 1205fec047 | |||
| a9ee8dceb3 | |||
| 80515205cc | |||
| 999dabed84 | |||
| 4fbb063094 | |||
| 600648d2d5 | |||
| 764219fa7d | |||
| 86c74e61e2 | |||
| 0139392ec9 | |||
| d25b4e0e06 | |||
| 0f44da7bea | |||
| 9052558d60 |
@@ -13,7 +13,7 @@ jobs:
|
||||
macOS:
|
||||
runs-on: macOS-latest
|
||||
env:
|
||||
XCODE_VERSION: ${{ '12.4' }}
|
||||
XCODE_VERSION: ${{ '13.1' }}
|
||||
steps:
|
||||
- name: Select Xcode
|
||||
run: "sudo xcode-select -s /Applications/Xcode_$XCODE_VERSION.app"
|
||||
|
||||
@@ -8,7 +8,7 @@ jobs:
|
||||
name: Add macOS binaries to release
|
||||
runs-on: macOS-latest
|
||||
env:
|
||||
XCODE_VERSION: ${{ '12.4' }}
|
||||
XCODE_VERSION: ${{ '13.1' }}
|
||||
steps:
|
||||
- name: Select Xcode
|
||||
run: "sudo xcode-select -s /Applications/Xcode_$XCODE_VERSION.app"
|
||||
|
||||
+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
|
||||
|
||||
@@ -1,15 +1,61 @@
|
||||
# XCRemoteCache
|
||||
<p align="center">
|
||||
<img src="docs/img/logo.png#gh-light-mode-only" width="75%">
|
||||
<img src="docs/img/logo-dark.png#gh-dark-mode-only" width="75%">
|
||||
</p>
|
||||
|
||||
XCRemoteCache is a remote cache tool for Xcode projects. It reuses target artifacts generated on a remote machine, served from a simple REST server.
|
||||
_XCRemoteCache is a remote cache tool for Xcode projects. It reuses target artifacts generated on a remote machine, served from a simple REST server._
|
||||
|
||||
[](https://github.com/spotify/XCRemoteCache/workflows/CI/badge.svg)
|
||||
[](LICENSE)
|
||||
[](https://slackin.spotify.com)
|
||||
|
||||
- [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)
|
||||
* [Performance optimizations](#performance-optimizations)
|
||||
* [Focused targets](#focused-targets)
|
||||
- [How to integrate XCRemoteCache with your Xcode project?](#how-to-integrate-xcremotecache-with-your-xcode-project)
|
||||
* [1. Download XCRemoteCache](#1-download-xcremotecache)
|
||||
* [A. Automatic integration](#a-automatic-integration)
|
||||
+ [2. Create a minimal XCRemoteCache configuration](#2-create-a-minimal-xcremotecache-configuration)
|
||||
+ [3. Run automatic integration script](#3-run-automatic-integration-script)
|
||||
- [3a. Producer side](#3a-producer-side)
|
||||
- [3b. Consumer side](#3b-consumer-side)
|
||||
* [A full list of `xcprepare integrate` supported options](#a-full-list-of-xcprepare-integrate-supported-options)
|
||||
* [B. Manual integration](#b-manual-integration)
|
||||
+ [2. Configure XCRemoteCache](#2-configure-xcremotecache)
|
||||
+ [3. Call xcprepare](#3-call-xcprepare)
|
||||
+ [4. Integrate with the Xcode project](#4-integrate-with-the-xcode-project)
|
||||
+ [5. Configure LLDB source-map (Optional)](#5-configure-lldb-source-map-optional)
|
||||
+ [6. Producer mode - Artifacts generation](#6-producer-mode---artifacts-generation)
|
||||
- [6a. Configure producer mode](#6a-configure-producer-mode)
|
||||
- [6b. Fill the cache](#6b-fill-the-cache)
|
||||
- [6c. Mark commit sha](#6c-mark-commit-sha)
|
||||
- [A full list of configuration parameters:](#a-full-list-of-configuration-parameters)
|
||||
- [Backend cache server](#backend-cache-server)
|
||||
* [Sample REST cache server from a docker image](#sample-rest-cache-server-from-a-docker-image)
|
||||
* [Amazon S3 and Google Cloud Storage](#amazon-s3-and-google-cloud-storage)
|
||||
- [CocoaPods plugin](#cocoapods-plugin)
|
||||
- [Requirements](#requirements)
|
||||
- [Apple silicon support](#apple-silicon-support)
|
||||
* [Artifacts per architecture (Recommended)](#artifacts-per-architecture-recommended)
|
||||
* [Fat artifacts](#fat-artifacts)
|
||||
- [Limitations](#limitations)
|
||||
- [FAQ](#faq)
|
||||
- [Development](#development)
|
||||
- [Release](#release)
|
||||
* [Building release package](#building-release-package)
|
||||
- [Contributing](#contributing)
|
||||
- [Code of conduct](#code-of-conduct)
|
||||
- [License](#license)
|
||||
- [Security Issues?](#security-issues)
|
||||
|
||||
## How and Why?
|
||||
|
||||
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.
|
||||
|
||||
@@ -93,7 +139,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. | `""` | ⬜️ |
|
||||
@@ -148,6 +194,7 @@ Configure Xcode targets that **should use** XCRemoteCache:
|
||||
* `SWIFT_EXEC` - location of `xcprepare` (e.g. `xcremotecache/xcswiftc`)
|
||||
* `LIBTOOL` - location of `xclibtool` (e.g. `xcremotecache/xclibtool`)
|
||||
* `LD` - location of `xcld` (e.g. `xcremotecache/xcld`)
|
||||
* `XCRC_PLATFORM_PREFERRED_ARCH` - `$(LINK_FILE_LIST_$(CURRENT_VARIANT)_$(PLATFORM_PREFERRED_ARCH):dir:standardizepath:file:default=arm64)`
|
||||
|
||||
<details>
|
||||
<summary>Screenshot</summary>
|
||||
@@ -167,8 +214,8 @@ Configure Xcode targets that **should use** XCRemoteCache:
|
||||
* command: `"$SCRIPT_INPUT_FILE_0"`
|
||||
* input files: location of `xcpostbuild` command (e.g. `xcremotecache/xcpostbuild`)
|
||||
* output files:
|
||||
* `$(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`
|
||||
* `$(TARGET_BUILD_DIR)/$(MODULES_FOLDER_PATH)/$(PRODUCT_MODULE_NAME).swiftmodule/$(XCRC_PLATFORM_PREFERRED_ARCH).swiftmodule.md5`
|
||||
* `$(TARGET_BUILD_DIR)/$(MODULES_FOLDER_PATH)/$(PRODUCT_MODULE_NAME).swiftmodule/$(XCRC_PLATFORM_PREFERRED_ARCH)-$(LLVM_TARGET_TRIPLE_VENDOR)-$(SWIFT_PLATFORM_TARGET_PREFIX)$(LLVM_TARGET_TRIPLE_SUFFIX).swiftmodule.md5`
|
||||
* discovery dependency file: `$(TARGET_TEMP_DIR)/postbuild.d`
|
||||
|
||||
<details>
|
||||
@@ -257,10 +304,12 @@ _Note that for the `producer` mode, the prebuild build phase and `xccc`, `xcld`,
|
||||
| `product_files_extensions_with_content_override ` | List of all extensions that should carry over source fingerprints. Extensions of all product files that contain non-deterministic content (absolute paths, timestamp, etc) should be included. | `["swiftmodule"]` | ⬜️ |
|
||||
| `thinning_enabled ` | If true, support for thin projects is enabled | `false` | ⬜️ |
|
||||
| `thinning_target_module_name ` | Module name of a target that works as a helper for thinned targets | `"ThinningRemoteCacheModule"` | ⬜️ |
|
||||
| `prettify_meta_files` | A Boolean value that opts-in pretty JSON formatting for meta files | `false` | ⬜️ |
|
||||
| `aws_secret_key` | Secret key for AWS V4 Signature Authorization. If this is set to a non-empty String - an Authentication Header will be added based on this and the other `aws_*` parameters.| `""` | ⬜️ |
|
||||
| `aws_access_key` | Access key for AWS V4 Signature Authorization. | `""` | ⬜️ |
|
||||
| `aws_region` | Region for AWS V4 Signature Authorization. E.g. `eu`. | `""` | ⬜️ |
|
||||
| `aws_service` | Service for AWS V4 Signature Authorization. E.g. `storage`. | `""` | ⬜️ |
|
||||
| `out_of_band_mappings` | A dictionary of files path remapping that should be applied to make it absolute path agnostic on a list of dependencies. Useful if a project refers files out of repo root, either compilation files or precompiled dependencies. Keys represent generic replacement and values are substrings that should be replaced. Example: for mapping `["COOL_LIBRARY": "/CoolLibrary"]` `/CoolLibrary/main.swift`will be represented as `$(COOL_LIBRARY)/main.swift`). Warning: remapping order is not-deterministic so avoid remappings with multiple matchings. | `[:]` | ⬜️ |
|
||||
|
||||
|
||||
## Backend cache server
|
||||
@@ -317,6 +366,31 @@ Retention Policy: Buckets usually have a retention policy option which ensures o
|
||||
|
||||
Head over to our [cocoapods-plugin](cocoapods-plugin/README.md) docs to see how to integrate XCRemoteCache in your CocoaPods project.
|
||||
|
||||
## Apple silicon support
|
||||
|
||||
### Artifacts per architecture (Recommended)
|
||||
|
||||
_If all of your machines (both producer and all consumers have the same architecture, either Intel or Apple Silicon), you don't have to do anything._
|
||||
|
||||
XCRemoteCache supports building artifacts for Apple silicon consumers. Is it recommended to build separately for `x86_64` and `arm64` architectures to have single-architecture artifacts that do not require downloading irrelevant binaries. Here are required steps if you want to support both Intel and Apple silicon consumers.
|
||||
|
||||
* Building for a simulator on a producer: run a first build for `x86_64`, clean a build and build again for `arm64`, e.g.:
|
||||
```
|
||||
xcodebuild ARCHS=x86_64 ONLY_ACTIVE_ARCH=NO build ...
|
||||
xcodebuild clean
|
||||
xcodebuild ARCHS=arm64 ONLY_ACTIVE_ARCH=NO build ...
|
||||
```
|
||||
|
||||
### Fat artifacts
|
||||
|
||||
If you prefer to generate far artifacts (with both Intel and Apple silicon binaries), you can disable "Build Archive Architecture Only" on a producer side, e.g.
|
||||
|
||||
```
|
||||
xcodebuild ONLY_ACTIVE_ARCH=NO build ...
|
||||
```
|
||||
|
||||
Note: This setup is not recommended and may not be supported in future XCRemoteCache releases.
|
||||
|
||||
## Requirements
|
||||
|
||||
* The repo under `git` version control
|
||||
@@ -330,6 +404,7 @@ Head over to our [cocoapods-plugin](cocoapods-plugin/README.md) docs to see how
|
||||
|
||||
## Limitations
|
||||
|
||||
* 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.
|
||||
|
||||
@@ -343,14 +418,25 @@ Follow the [Development](docs/Development.md) guide. It has all the information
|
||||
|
||||
## Release
|
||||
|
||||
To build a release zip package, call:
|
||||
To release a version, in [Releases](https://github.com/spotify/XCRemoteCache/releases) draft a new release with `v0.3.0{-rc0}` tag format.
|
||||
Packages with binaries will be automatically uploaded to the GitHub [Releases](https://github.com/spotify/XCRemoteCache/releases) page.
|
||||
|
||||
### Building release package
|
||||
|
||||
To build a release zip package for a single platform (e.g. `x86_64-apple-macosx`, `arm64-apple-macosx`), call:
|
||||
|
||||
```shell
|
||||
CONFIG=Release rake build
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -80,7 +80,7 @@ class ThinningConsumerUnzippedArtifactSwiftProductsOrganizerFactory: ThinningCon
|
||||
moduleName: moduleName
|
||||
)
|
||||
|
||||
return DiskSwiftcProductsGenerator(
|
||||
return ThinningDiskSwiftcProductsGenerator(
|
||||
modulePathOutput: modulePathOutput,
|
||||
objcHeaderOutput: objcHeaderOutput,
|
||||
diskCopier: diskCopier
|
||||
|
||||
+5
-2
@@ -44,7 +44,10 @@ class DefaultSwiftProductsArchitecturesRecognizer: SwiftProductsArchitecturesRec
|
||||
let moduleDirectory = builtProductsDir
|
||||
.appendingPathComponent(moduleName)
|
||||
.appendingPathExtension(Self.SwiftmoduleDirExtension)
|
||||
let productFiles = try dirAccessor.items(at: moduleDirectory)
|
||||
// Skip folders (e.g. 'Project' dir that stores .sourceinfo, introduced in Xcode13)
|
||||
let productFiles = try dirAccessor.items(at: moduleDirectory).filter { url in
|
||||
try dirAccessor.itemType(atPath: url.path) == .file
|
||||
}
|
||||
/// files in a moduleDirectory have basename corresponding to the
|
||||
/// architecture (e.g. 'x86_64-apple-ios-simulator.swiftmodule', 'x86_64.swiftmodule' ...)
|
||||
let architectures = productFiles.map { file -> String in
|
||||
@@ -55,7 +58,7 @@ class DefaultSwiftProductsArchitecturesRecognizer: SwiftProductsArchitecturesRec
|
||||
}
|
||||
return basenameFile.lastPathComponent
|
||||
}
|
||||
// remove duplicates comming from files with different extensions (swiftmodule, swiftdoc etc.)
|
||||
// remove duplicates coming from files with different extensions (swiftmodule, swiftdoc etc.)
|
||||
return Set(architectures).sorted()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 []
|
||||
}
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
// 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
|
||||
|
||||
/// Generator that produces all products in the DerivedData's Products locations, using provided disk copier
|
||||
class ThinningDiskSwiftcProductsGenerator: SwiftcProductsGenerator {
|
||||
private let destinationSwiftmodulePaths: [SwiftmoduleFileExtension: URL]
|
||||
private let modulePathOutput: URL
|
||||
private let objcHeaderOutput: URL
|
||||
private let diskCopier: DiskCopier
|
||||
|
||||
init(
|
||||
modulePathOutput: URL,
|
||||
objcHeaderOutput: URL,
|
||||
diskCopier: DiskCopier
|
||||
) {
|
||||
self.modulePathOutput = modulePathOutput
|
||||
let modulePathBasename = modulePathOutput.deletingPathExtension()
|
||||
let modulePathDir = modulePathOutput.deletingLastPathComponent()
|
||||
let moduleName = modulePathBasename.lastPathComponent
|
||||
// all swiftmodule-related should be located next to the ".swiftmodule"
|
||||
// except of '.swiftsourceinfo', which should be placed in 'Project' dir
|
||||
destinationSwiftmodulePaths = Dictionary(
|
||||
uniqueKeysWithValues: SwiftmoduleFileExtension.SwiftmoduleExtensions
|
||||
.map { ext, _ in
|
||||
switch ext {
|
||||
case .swiftsourceinfo:
|
||||
let dest = modulePathDir.appendingPathComponent("Project")
|
||||
.appendingPathComponent(moduleName)
|
||||
.appendingPathExtension(ext.rawValue)
|
||||
return (ext, dest)
|
||||
default:
|
||||
return (ext, modulePathBasename.appendingPathExtension(ext.rawValue))
|
||||
}
|
||||
}
|
||||
)
|
||||
self.objcHeaderOutput = objcHeaderOutput
|
||||
self.diskCopier = diskCopier
|
||||
}
|
||||
|
||||
func generateFrom(
|
||||
artifactSwiftModuleFiles sourceAtifactSwiftModuleFiles: [SwiftmoduleFileExtension: URL],
|
||||
artifactSwiftModuleObjCFile: URL
|
||||
) throws -> URL {
|
||||
// Move cached -Swift.h file to the expected location
|
||||
try diskCopier.copy(file: artifactSwiftModuleObjCFile, destination: objcHeaderOutput)
|
||||
for (ext, url) in sourceAtifactSwiftModuleFiles {
|
||||
let dest = destinationSwiftmodulePaths[ext]
|
||||
guard let destination = dest else {
|
||||
throw DiskSwiftcProductsGeneratorError.unknownSwiftmoduleFile
|
||||
}
|
||||
do {
|
||||
// Move cached .swiftmodule to the expected location
|
||||
try diskCopier.copy(file: url, destination: destination)
|
||||
} catch {
|
||||
if case .required = SwiftmoduleFileExtension.SwiftmoduleExtensions[ext] {
|
||||
throw error
|
||||
} else {
|
||||
infoLog("Optional .\(ext) file not found in the artifact at: \(destination.path)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build parent dir of the .swiftmodule file that contains a module
|
||||
return modulePathOutput.deletingLastPathComponent()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -32,6 +32,8 @@ enum MachOType: String, Codable {
|
||||
enum PostbuildContextError: Error {
|
||||
/// URL address is not a valid URL
|
||||
case invalidAddress(String)
|
||||
/// ARCHS env does not contain any architecture to build
|
||||
case missingArchitecture
|
||||
}
|
||||
|
||||
public struct PostbuildContext {
|
||||
@@ -64,6 +66,9 @@ public struct PostbuildContext {
|
||||
var machOType: MachOType
|
||||
var wasDsymGenerated: Bool
|
||||
var dSYMPath: URL
|
||||
// building architecture. Used to find all dependencies from *.d files
|
||||
// Warning: if two architectures are built (e.g. for disabled "Build Archive
|
||||
// Architecture Only"), a first architecture one is picked
|
||||
let arch: String
|
||||
let builtProductsDir: URL
|
||||
/// Location to the product bundle. Can be nil for libraries
|
||||
@@ -71,6 +76,9 @@ public struct PostbuildContext {
|
||||
let derivedSourcesDir: URL
|
||||
/// List of all targets to downloaded from the thinning aggregation target
|
||||
var thinnedTargets: [String]
|
||||
/// Action type: build, indexbuild etc.
|
||||
var action: BuildActionType
|
||||
let modeMarkerPath: String
|
||||
}
|
||||
|
||||
extension PostbuildContext {
|
||||
@@ -79,8 +87,13 @@ extension PostbuildContext {
|
||||
let targetNameValue: String = try env.readEnv(key: "TARGET_NAME")
|
||||
targetName = targetNameValue
|
||||
targetTempDir = try env.readEnv(key: "TARGET_TEMP_DIR")
|
||||
arch = try env.readEnv(key: "PLATFORM_PREFERRED_ARCH")
|
||||
compilationTempDir = try env.readEnv(key: "OBJECT_FILE_DIR_normal").appendingPathComponent(arch)
|
||||
let archs: [String] = try env.readEnv(key: "ARCHS").split(separator: " ").map(String.init)
|
||||
guard let firstArch = archs.first, !firstArch.isEmpty else {
|
||||
throw PostbuildContextError.missingArchitecture
|
||||
}
|
||||
arch = firstArch
|
||||
let variant: String = try env.readEnv(key: "CURRENT_VARIANT")
|
||||
compilationTempDir = try env.readEnv(key: "OBJECT_FILE_DIR_\(variant)").appendingPathComponent(arch)
|
||||
configuration = try env.readEnv(key: "CONFIGURATION")
|
||||
platform = try env.readEnv(key: "PLATFORM_NAME")
|
||||
xcodeBuildNumber = try env.readEnv(key: "XCODE_PRODUCT_BUILD_VERSION")
|
||||
@@ -112,5 +125,7 @@ extension PostbuildContext {
|
||||
derivedSourcesDir = try env.readEnv(key: "DERIVED_SOURCES_DIR")
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,18 +31,19 @@ public class XCPostbuild {
|
||||
let fileManager = FileManager.default
|
||||
let config: XCRemoteCacheConfig
|
||||
let context: PostbuildContext
|
||||
let statsLogger: StatsLogger
|
||||
let cacheHitLogger: CacheHitLogger
|
||||
do {
|
||||
config = try XCRemoteCacheConfigReader(env: env, fileManager: fileManager).readConfiguration()
|
||||
context = try PostbuildContext(config, env: env)
|
||||
let counterFactory: FileStatsCoordinator.CountersFactory = { file, count in
|
||||
ExclusiveFileCounter(ExclusiveFile(file, mode: .override), countersCount: count)
|
||||
}
|
||||
statsLogger = try FileStatsLogger(
|
||||
let statsLogger = try FileStatsLogger(
|
||||
statsLocation: context.statsLocation,
|
||||
counterFactory: counterFactory,
|
||||
fileManager: fileManager
|
||||
)
|
||||
cacheHitLogger = ActionSpecificCacheHitLogger(action: context.action, statsLogger: statsLogger)
|
||||
} catch {
|
||||
exit(1, "FATAL: Postbuild initialization failed with error: \(error)")
|
||||
}
|
||||
@@ -65,9 +66,10 @@ public class XCPostbuild {
|
||||
// Initialize dependencies
|
||||
let primaryGitBranch = GitBranch(repoLocation: config.primaryRepo, branch: config.primaryBranch)
|
||||
let gitClient = GitClientImpl(repoRoot: config.repoRoot, primary: primaryGitBranch, shell: shellGetStdout)
|
||||
let pathRemapper = try StringDependenciesRemapper.buildFromEnvs(
|
||||
keys: DependenciesMapping.rewrittenEnvs,
|
||||
envs: env
|
||||
let pathRemapper = try StringDependenciesRemapperFactory().build(
|
||||
orderKeys: DependenciesMapping.rewrittenEnvs,
|
||||
envs: env,
|
||||
customMappings: config.outOfBandMappings
|
||||
)
|
||||
let envFingerprint = try EnvironmentFingerprintGenerator(
|
||||
configuration: config,
|
||||
@@ -84,6 +86,7 @@ public class XCPostbuild {
|
||||
algorithm: MD5Algorithm()
|
||||
)
|
||||
let organizer = ZipArtifactOrganizer(targetTempDir: context.targetTempDir, fileManager: fileManager)
|
||||
let metaWriter = JsonMetaWriter(fileWriter: fileManager, pretty: config.prettifyMetaFiles)
|
||||
let artifactCreator = BuildArtifactCreator(
|
||||
buildDir: context.productsDir,
|
||||
tempDir: context.targetTempDir,
|
||||
@@ -91,6 +94,7 @@ public class XCPostbuild {
|
||||
moduleName: context.moduleName,
|
||||
modulesFolderPath: context.modulesFolderPath,
|
||||
dSYMPath: context.dSYMPath,
|
||||
metaWriter: metaWriter,
|
||||
fileManager: fileManager
|
||||
)
|
||||
let dirAccessor = DirAccessorComposer(
|
||||
@@ -204,9 +208,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)
|
||||
@@ -228,6 +233,7 @@ public class XCPostbuild {
|
||||
dSYMOrganizer: dSYMOrganizer,
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: creatorPlugins,
|
||||
consumerPlugins: consumerPlugins
|
||||
)
|
||||
@@ -242,11 +248,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)
|
||||
@@ -255,14 +272,13 @@ public class XCPostbuild {
|
||||
// Populate stats event for a final RC state
|
||||
// Doing it in a postmerge, as xcswiftc (and xccc) has a right to disable RC
|
||||
if try modeController.isEnabled() {
|
||||
try statsLogger.log(.targetCacheHit)
|
||||
try cacheHitLogger.logHit()
|
||||
printToUser("Cached build for \(context.targetName) target")
|
||||
} else {
|
||||
try postbuildAction.performBuildCleanup()
|
||||
try statsLogger.log(.targetCacheMiss)
|
||||
// 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"
|
||||
try cacheHitLogger.logMiss()
|
||||
// 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) {
|
||||
@@ -277,7 +293,7 @@ public class XCPostbuild {
|
||||
do {
|
||||
try modeController.disable()
|
||||
// TODO: consider tracking errors in stats
|
||||
try statsLogger.log(.targetCacheMiss)
|
||||
try cacheHitLogger.logMiss()
|
||||
printToUser("Disabled remote cache for \(context.targetName)")
|
||||
} catch {
|
||||
exit(1, "FATAL: Postbuild finishing failed with error: \(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
|
||||
|
||||
@@ -115,9 +115,10 @@ public class XCPrebuild {
|
||||
)
|
||||
let client: NetworkClient = config.disableHttpCache ? networkClient : cacheNetworkClient
|
||||
let remoteNetworkClient = RemoteNetworkClientImpl(client, urlBuilder)
|
||||
let pathRemapper = try StringDependenciesRemapper.buildFromEnvs(
|
||||
keys: DependenciesMapping.rewrittenEnvs,
|
||||
envs: env
|
||||
let pathRemapper = try StringDependenciesRemapperFactory().build(
|
||||
orderKeys: DependenciesMapping.rewrittenEnvs,
|
||||
envs: env,
|
||||
customMappings: config.outOfBandMappings
|
||||
)
|
||||
let filesFingerprintGenerator = FingerprintAccumulatorImpl(
|
||||
algorithm: MD5Algorithm(),
|
||||
@@ -129,7 +130,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] = []
|
||||
|
||||
|
||||
@@ -72,13 +72,14 @@ class TemplateBasedCCWrapperBuilder: CCWrapperBuilder {
|
||||
)
|
||||
infoLog("ClangWrapperBuilder compiles file at \(compilationFile).")
|
||||
// -O3: optimize for faster execution
|
||||
let args = [clangCommand, "-O3", compilationFile.path, "-o", destination.path]
|
||||
let args = [clangCommand, "-arch", "arm64", "-arch", "x86_64", "-O3", compilationFile.path, "-o", destination.path]
|
||||
let compilationOutput = try shell("xcrun", args, URL(fileURLWithPath: "").path, nil)
|
||||
infoLog("Clang compilation output: \(compilationOutput)")
|
||||
}
|
||||
|
||||
|
||||
/// 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
|
||||
|
||||
@@ -62,6 +62,7 @@ class XcodeProjBuildSettingsIntegrateAppender: BuildSettingsIntegrateAppender {
|
||||
result["OTHER_CFLAGS"] = clangFlags.settingValue
|
||||
|
||||
result["XCRC_FAKE_SRCROOT"] = "/\(String(repeating: "x", count: 10))"
|
||||
result["XCRC_PLATFORM_PREFERRED_ARCH"] = "$(LINK_FILE_LIST_$(CURRENT_VARIANT)_$(PLATFORM_PREFERRED_ARCH):dir:standardizepath:file:default=arm64)"
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -100,11 +100,11 @@ struct XcodeProjIntegrate: Integrate {
|
||||
outputPaths: [
|
||||
"""
|
||||
$(TARGET_BUILD_DIR)/$(MODULES_FOLDER_PATH)/$(PRODUCT_MODULE_NAME).swiftmodule/\
|
||||
$(PLATFORM_PREFERRED_ARCH).swiftmodule.md5
|
||||
$(XCRC_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)\
|
||||
$(XCRC_PLATFORM_PREFERRED_ARCH)-$(LLVM_TARGET_TRIPLE_VENDOR)-$(SWIFT_PLATFORM_TARGET_PREFIX)\
|
||||
$(LLVM_TARGET_TRIPLE_SUFFIX).swiftmodule.md5
|
||||
""",
|
||||
],
|
||||
@@ -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
|
||||
@@ -166,9 +166,11 @@ class Swiftc: SwiftcProtocol {
|
||||
|
||||
// Save individual .d and touch .o for each .swift file
|
||||
for compilation in allCompilations.files {
|
||||
// Touching .o is required to invalidate already existing .a or linked library
|
||||
let touch = touchFactory(compilation.object, fileManager)
|
||||
try touch.touch()
|
||||
if let object = compilation.object {
|
||||
// Touching .o is required to invalidate already existing .a or linked library
|
||||
let touch = touchFactory(object, fileManager)
|
||||
try touch.touch()
|
||||
}
|
||||
if let individualDeps = compilation.dependencies {
|
||||
// swiftc product should be invalidated if any of dependencies file has changed
|
||||
try cachedDependenciesWriterFactory.generate(output: individualDeps)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -52,7 +52,8 @@ struct SwiftFileCompilationInfo: Encodable, Equatable {
|
||||
let file: URL
|
||||
// not present for WMO builds
|
||||
let dependencies: URL?
|
||||
let object: URL
|
||||
// not present for 'indexbuild' builds
|
||||
let object: URL?
|
||||
// not present for WMO builds
|
||||
let swiftDependencies: URL?
|
||||
}
|
||||
@@ -129,14 +130,14 @@ extension SwiftFileCompilationInfo {
|
||||
}
|
||||
file = URL(fileURLWithPath: name)
|
||||
dependencies = dict.readURL(key: "dependencies")
|
||||
object = try dict.readURL(key: "object")
|
||||
object = dict.readURL(key: "object")
|
||||
swiftDependencies = dict.readURL(key: "swift-dependencies")
|
||||
}
|
||||
|
||||
func dump() -> [String: String] {
|
||||
return [
|
||||
"dependencies": dependencies?.path,
|
||||
"object": object.path,
|
||||
"object": object?.path,
|
||||
"swift-dependencies": swiftDependencies?.path,
|
||||
].compactMapValues { $0 }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -111,6 +112,8 @@ public struct XCRemoteCacheConfig: Encodable {
|
||||
var thinningEnabled: Bool = false
|
||||
/// Module name of a target that works as a helper for thinned targets
|
||||
var thinningTargetModuleName: String = "ThinningRemoteCacheModule"
|
||||
/// Opt-in pretty json formatting for meta files
|
||||
var prettifyMetaFiles: Bool = false
|
||||
/// Secret key for AWS V4 Signature, if this is set the Authentication Header will be added
|
||||
var AWSSecretKey: String = ""
|
||||
/// Access key for AWS V4 Signature
|
||||
@@ -119,11 +122,19 @@ public struct XCRemoteCacheConfig: Encodable {
|
||||
var AWSRegion: String = ""
|
||||
/// Service for AWS V4 Signature (e.g. `storage`)
|
||||
var AWSService: String = ""
|
||||
/// A dictionary of files path remapping that should be applied to make it absolute path agnostic on a list of dependencies.
|
||||
/// Useful if a project refers files out of repo root, either compilation files or precompiled dependencies.
|
||||
/// Keys represent generic replacement and values are substrings that should be replaced.
|
||||
/// Example: for mapping `["COOL_LIBRARY": "/CoolLibrary"]`
|
||||
/// `/CoolLibrary/main.swift`will be represented as `$(COOL_LIBRARY)/main.swift`).
|
||||
/// Warning: remapping order is not-deterministic so avoid remappings with multiple matchings.
|
||||
var outOfBandMappings: [String: String] = [:]
|
||||
}
|
||||
|
||||
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
|
||||
@@ -163,10 +174,12 @@ extension XCRemoteCacheConfig {
|
||||
scheme.productFilesExtensionsWithContentOverride ?? productFilesExtensionsWithContentOverride
|
||||
merge.thinningEnabled = scheme.thinningEnabled ?? thinningEnabled
|
||||
merge.thinningTargetModuleName = scheme.thinningTargetModuleName ?? thinningTargetModuleName
|
||||
merge.prettifyMetaFiles = scheme.prettifyMetaFiles ?? prettifyMetaFiles
|
||||
merge.AWSAccessKey = scheme.AWSAccessKey ?? AWSAccessKey
|
||||
merge.AWSSecretKey = scheme.AWSSecretKey ?? AWSSecretKey
|
||||
merge.AWSRegion = scheme.AWSRegion ?? AWSRegion
|
||||
merge.AWSService = scheme.AWSService ?? AWSService
|
||||
merge.outOfBandMappings = scheme.outOfBandMappings ?? outOfBandMappings
|
||||
return merge
|
||||
}
|
||||
|
||||
@@ -221,10 +234,12 @@ struct ConfigFileScheme: Decodable {
|
||||
let productFilesExtensionsWithContentOverride: [String]?
|
||||
let thinningEnabled: Bool?
|
||||
let thinningTargetModuleName: String?
|
||||
let prettifyMetaFiles: Bool?
|
||||
let AWSSecretKey: String?
|
||||
let AWSAccessKey: String?
|
||||
let AWSRegion: String?
|
||||
let AWSService: String?
|
||||
let outOfBandMappings: [String: String]?
|
||||
|
||||
// Yams library doesn't support encoding strategy, see https://github.com/jpsim/Yams/issues/84
|
||||
enum CodingKeys: String, CodingKey {
|
||||
@@ -262,10 +277,12 @@ struct ConfigFileScheme: Decodable {
|
||||
case productFilesExtensionsWithContentOverride = "product_files_extensions_with_content_override"
|
||||
case thinningEnabled = "thinning_enabled"
|
||||
case thinningTargetModuleName = "thinning_target_module_name"
|
||||
case prettifyMetaFiles = "prettify_meta_files"
|
||||
case AWSSecretKey = "aws_secret_key"
|
||||
case AWSAccessKey = "aws_access_key"
|
||||
case AWSRegion = "aws_region"
|
||||
case AWSService = "aws_service"
|
||||
case outOfBandMappings = "out_of_band_mappings"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ class DependenciesRemapperComposite: DependenciesRemapper {
|
||||
}
|
||||
|
||||
func replace(genericPaths: [String]) -> [String] {
|
||||
remappers.reduce(genericPaths) { prev, mapper in
|
||||
remappers.reversed().reduce(genericPaths) { prev, mapper in
|
||||
mapper.replace(genericPaths: prev)
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ final class StringDependenciesRemapper: DependenciesRemapper {
|
||||
|
||||
func replace(genericPaths: [String]) -> [String] {
|
||||
return genericPaths.map { path in
|
||||
let localPath = mappings.reduce(path) { prevPath, mapping in
|
||||
let localPath = mappings.reversed().reduce(path) { prevPath, mapping in
|
||||
prevPath.replacingOccurrences(of: mapping.generic, with: mapping.local)
|
||||
}
|
||||
return localPath
|
||||
@@ -77,14 +77,3 @@ final class StringDependenciesRemapper: DependenciesRemapper {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension StringDependenciesRemapper {
|
||||
static func buildFromEnvs(keys: [String], envs: [String: String]) throws -> Self {
|
||||
let mappings: [Mapping] = try keys.map { key in
|
||||
let localValue: String = try envs.readEnv(key: key)
|
||||
return Mapping(generic: "$(\(key))", local: localValue)
|
||||
}
|
||||
return Self(mappings: mappings)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// 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
|
||||
|
||||
enum StringDependenciesRemapperFactoryError: Error {
|
||||
/// Remapping keys are duplicated and can lead to undetermined results
|
||||
case mappingKeyDuplication
|
||||
}
|
||||
|
||||
class StringDependenciesRemapperFactory {
|
||||
func build(
|
||||
orderKeys: [String],
|
||||
envs: [String: String],
|
||||
customMappings: [String: String]
|
||||
) throws -> StringDependenciesRemapper {
|
||||
let mappingMap = try envs.merging(customMappings) { envValue, outOfBandMapping in
|
||||
throw StringDependenciesRemapperFactoryError.mappingKeyDuplication
|
||||
}
|
||||
let mappingOrderKeys = orderKeys + customMappings.keys
|
||||
let mappings: [StringDependenciesRemapper.Mapping] = try mappingOrderKeys.map { key in
|
||||
let localValue: String = try mappingMap.readEnv(key: key)
|
||||
return StringDependenciesRemapper.Mapping(generic: "$(\(key))", local: localValue)
|
||||
}
|
||||
return StringDependenciesRemapper(mappings: mappings)
|
||||
}
|
||||
}
|
||||
@@ -19,10 +19,10 @@
|
||||
|
||||
/// Generates a fingerprint string of the environment (compilation context)
|
||||
class EnvironmentFingerprintGenerator {
|
||||
/// Default ENV variables constituing the environment fingerprint
|
||||
/// Default ENV variables constituting the environment fingerprint
|
||||
private static let defaultEnvFingerprintKeys = [
|
||||
"GCC_PREPROCESSOR_DEFINITIONS",
|
||||
"CLANG_PROFILE_DATA_DIRECTORY",
|
||||
"CLANG_COVERAGE_MAPPING",
|
||||
"TARGET_NAME",
|
||||
"CONFIGURATION",
|
||||
"PLATFORM_NAME",
|
||||
@@ -31,6 +31,7 @@ class EnvironmentFingerprintGenerator {
|
||||
"DYLIB_COMPATIBILITY_VERSION",
|
||||
"DYLIB_CURRENT_VERSION",
|
||||
"PRODUCT_MODULE_NAME",
|
||||
"ARCHS"
|
||||
]
|
||||
private let version: String
|
||||
private let customFingerprintEnvs: [String]
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// 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, pretty: Bool) {
|
||||
self.fileWriter = fileWriter
|
||||
let encoder = JSONEncoder()
|
||||
if pretty {
|
||||
encoder.outputFormatting = .prettyPrinted
|
||||
}
|
||||
self.metaEncoder = encoder
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
// 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.
|
||||
|
||||
/// Logger to log events to the statistics database
|
||||
protocol CacheHitLogger {
|
||||
/// Increments a counter related to the cache hit
|
||||
func logHit() throws
|
||||
/// Increments a counter related to the cache miss
|
||||
func logMiss() throws
|
||||
}
|
||||
|
||||
/// 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?
|
||||
|
||||
init(action: BuildActionType, statsLogger: StatsLogger) {
|
||||
self.statsLogger = statsLogger
|
||||
switch action {
|
||||
case .index:
|
||||
hitCounter = .indexingTargetHitCount
|
||||
missCounter = .indexingTargetMissCount
|
||||
case .build:
|
||||
hitCounter = .targetCacheHit
|
||||
missCounter = .targetCacheMiss
|
||||
case .unknown:
|
||||
hitCounter = nil
|
||||
missCounter = nil
|
||||
}
|
||||
}
|
||||
|
||||
func logHit() throws {
|
||||
if let hitCounter = hitCounter {
|
||||
try statsLogger.log(hitCounter)
|
||||
}
|
||||
}
|
||||
|
||||
func logMiss() throws {
|
||||
if let missCounter = missCounter {
|
||||
try statsLogger.log(missCounter)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,9 @@ class FileStatsCoordinator: FileStatsLogger, StatsCoordinator {
|
||||
return try XCRemoteCacheStatistics(
|
||||
hitCount: counters[XCRemoteCacheStatistics.Counter.targetCacheHit.rawValue],
|
||||
missCount: counters[XCRemoteCacheStatistics.Counter.targetCacheMiss.rawValue],
|
||||
localCacheBytes: countLocalCacheSize()
|
||||
localCacheBytes: countLocalCacheSize(),
|
||||
indexingHitCount: counters[XCRemoteCacheStatistics.Counter.indexingTargetHitCount.rawValue],
|
||||
indexingMissCount: counters[XCRemoteCacheStatistics.Counter.indexingTargetMissCount.rawValue]
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,45 +18,63 @@
|
||||
// under the License.
|
||||
|
||||
/// Representation of all statistics related to XCRemoteCache
|
||||
struct XCRemoteCacheStatistics: Encodable {
|
||||
struct XCRemoteCacheStatistics: Encodable, Equatable {
|
||||
// Yams library doesn't support encoding strategy, see https://github.com/jpsim/Yams/issues/84
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case hitCount = "hit_count"
|
||||
case missCount = "miss_count"
|
||||
case localCacheBytes = "local_cache_bytes"
|
||||
case indexingHitCount = "indexing_hit_count"
|
||||
case indexingMissCount = "indexing_miss_count"
|
||||
}
|
||||
|
||||
/// Counters fields position: rawValue defines the index of the counter
|
||||
/// that backs up the statistic metric
|
||||
enum Counter: Int, CaseIterable {
|
||||
// Warning! Do not add between existing fieds, only add new one at the bottom
|
||||
// Warning! Do not add between existing fields, only add new one at the bottom
|
||||
// rawValue represents the counter position
|
||||
// e.g. '0' means that 'hitCount' metric will
|
||||
// be stored in a first counter (in a file)
|
||||
case targetCacheHit = 0
|
||||
case targetCacheMiss
|
||||
case indexingTargetHitCount
|
||||
case indexingTargetMissCount
|
||||
}
|
||||
|
||||
/// Number of cache hits
|
||||
let hitCount: Int
|
||||
/// Number of cache mises
|
||||
/// Number of cache misses
|
||||
let missCount: Int
|
||||
/// Size of a local cache in bytes
|
||||
let localCacheBytes: Int
|
||||
/// Number of cache hits for 'indexbuild' actions
|
||||
let indexingHitCount: Int
|
||||
/// Number of cache misses for 'indexbuild' actions
|
||||
let indexingMissCount: Int
|
||||
|
||||
static let initial = XCRemoteCacheStatistics(hitCount: 0, missCount: 0, localCacheBytes: 0)
|
||||
static let initial = XCRemoteCacheStatistics(
|
||||
hitCount: 0,
|
||||
missCount: 0,
|
||||
localCacheBytes: 0,
|
||||
indexingHitCount: 0,
|
||||
indexingMissCount: 0
|
||||
)
|
||||
}
|
||||
|
||||
extension XCRemoteCacheStatistics {
|
||||
func with(
|
||||
hitCount: Int? = nil,
|
||||
missCount: Int? = nil,
|
||||
localCacheBytes: Int? = nil
|
||||
localCacheBytes: Int? = nil,
|
||||
indexingHitCount: Int? = nil,
|
||||
indexingMissCount: Int? = nil
|
||||
) -> XCRemoteCacheStatistics {
|
||||
return XCRemoteCacheStatistics(
|
||||
hitCount: hitCount ?? self.hitCount,
|
||||
missCount: missCount ?? self.missCount,
|
||||
localCacheBytes: localCacheBytes ?? self.localCacheBytes
|
||||
localCacheBytes: localCacheBytes ?? self.localCacheBytes,
|
||||
indexingHitCount: indexingHitCount ?? self.indexingHitCount,
|
||||
indexingMissCount: indexingMissCount ?? self.indexingMissCount
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// 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
|
||||
|
||||
/// Represents a type of a build invoked by the Xcode
|
||||
enum BuildActionType: String, Codable {
|
||||
/// Standard build
|
||||
case build
|
||||
/// An extra build, exclusive for indexing (Introduced in Xcode 13)
|
||||
case index = "indexbuild"
|
||||
/// Unknown type, probably incompatible Xcode version used
|
||||
case unknown
|
||||
}
|
||||
@@ -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, pretty: false),
|
||||
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, pretty: false),
|
||||
fileManager: fileManager
|
||||
)
|
||||
}
|
||||
|
||||
func testCreatingArtifactGeneratesValidArtifactId() throws {
|
||||
|
||||
+14
@@ -84,4 +84,18 @@ class DefaultSwiftProductsArchitecturesRecognizerTests: FileXCTestCase {
|
||||
|
||||
XCTAssertEqual(architectures, ["x86"])
|
||||
}
|
||||
|
||||
func testRecognizesArchitectureFromFilesOnly() throws {
|
||||
let swiftmodule = swiftmoduleDir.appendingPathComponent("x86.swiftmodule")
|
||||
let swiftmoduleExtraDir = swiftmoduleDir.appendingPathComponent("Dir")
|
||||
try fileManager.spt_createEmptyFile(swiftmodule)
|
||||
try fileManager.spt_createEmptyDir(swiftmoduleExtraDir)
|
||||
|
||||
let architectures = try recognizer.recognizeArchitectures(
|
||||
builtProductsDir: builtProductsDir,
|
||||
moduleName: "MyModule"
|
||||
)
|
||||
|
||||
XCTAssertEqual(architectures, ["x86"])
|
||||
}
|
||||
}
|
||||
|
||||
+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"
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
// 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 ThinningDiskSwiftcProductsGeneratorTests: FileXCTestCase {
|
||||
|
||||
func testLinksSwiftProductsToValidLocations() throws {
|
||||
let workingDir = try prepareTempDir()
|
||||
let moduleFile = try fileManager.spt_createFile(
|
||||
workingDir.appendingPathComponent("MyModule.swiftmodule"),
|
||||
content: "module"
|
||||
)
|
||||
let headerFile = try fileManager.spt_createFile(
|
||||
workingDir.appendingPathComponent("MyModule-Swift.h"),
|
||||
content: "header"
|
||||
)
|
||||
let docsFile = try fileManager.spt_createFile(
|
||||
workingDir.appendingPathComponent("MyModule.swiftdoc"),
|
||||
content: "docs"
|
||||
)
|
||||
let sourceInfoFile = try fileManager.spt_createFile(
|
||||
workingDir.appendingPathComponent("MyModule.swiftsourceinfo"),
|
||||
content: "sourceInfo"
|
||||
)
|
||||
let artifactSwiftModuleFiles: [SwiftmoduleFileExtension: URL] = [
|
||||
.swiftmodule: moduleFile,
|
||||
.swiftdoc: docsFile,
|
||||
.swiftsourceinfo: sourceInfoFile,
|
||||
]
|
||||
let buildDir = workingDir.appendingPathComponent("build")
|
||||
let headersDir = workingDir.appendingPathComponent("headers")
|
||||
let destinationSwiftModuleDir = buildDir
|
||||
.appendingPathComponent("MyModule.swiftmodule", isDirectory: true)
|
||||
let objCHeader = headersDir
|
||||
.appendingPathComponent("MyModule-Swift.h")
|
||||
let destinationSwiftModule = destinationSwiftModuleDir
|
||||
.appendingPathComponent("arm64.swiftmodule")
|
||||
let expectedSwiftSourceInfoFile = destinationSwiftModuleDir
|
||||
.appendingPathComponent("Project")
|
||||
.appendingPathComponent("arm64.swiftsourceinfo")
|
||||
let generator = ThinningDiskSwiftcProductsGenerator(
|
||||
modulePathOutput: destinationSwiftModule,
|
||||
objcHeaderOutput: objCHeader,
|
||||
diskCopier: HardLinkDiskCopier(fileManager: .default)
|
||||
)
|
||||
|
||||
let generatedModulePath = try generator.generateFrom(
|
||||
artifactSwiftModuleFiles: artifactSwiftModuleFiles,
|
||||
artifactSwiftModuleObjCFile: headerFile
|
||||
)
|
||||
|
||||
XCTAssertEqual(generatedModulePath, destinationSwiftModuleDir)
|
||||
XCTAssertEqual(fileManager.contents(atPath: expectedSwiftSourceInfoFile.path), "sourceInfo".data(using: .utf8))
|
||||
XCTAssertEqual(fileManager.contents(atPath: objCHeader.path), "header".data(using: .utf8))
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,8 @@ class PostbuildContextTests: FileXCTestCase {
|
||||
private static let SampleEnvs = [
|
||||
"TARGET_NAME": "TARGET_NAME",
|
||||
"TARGET_TEMP_DIR": "TARGET_TEMP_DIR",
|
||||
"PLATFORM_PREFERRED_ARCH": "PLATFORM_PREFERRED_ARCH",
|
||||
"OBJECT_FILE_DIR_normal": "OBJECT_FILE_DIR_normal" ,
|
||||
"ARCHS": "x86_64",
|
||||
"OBJECT_FILE_DIR_normal": "/OBJECT_FILE_DIR_normal" ,
|
||||
"CONFIGURATION": "CONFIGURATION",
|
||||
"PLATFORM_NAME": "PLATFORM_NAME",
|
||||
"XCODE_PRODUCT_BUILD_VERSION": "XCODE_PRODUCT_BUILD_VERSION",
|
||||
@@ -42,6 +42,7 @@ class PostbuildContextTests: FileXCTestCase {
|
||||
"DWARF_DSYM_FILE_NAME": "DWARF_DSYM_FILE_NAME",
|
||||
"BUILT_PRODUCTS_DIR": "BUILT_PRODUCTS_DIR",
|
||||
"DERIVED_SOURCES_DIR": "DERIVED_SOURCES_DIR",
|
||||
"CURRENT_VARIANT": "normal"
|
||||
]
|
||||
|
||||
override func setUpWithError() throws {
|
||||
@@ -73,4 +74,59 @@ class PostbuildContextTests: FileXCTestCase {
|
||||
|
||||
XCTAssertEqual(context.remoteCommit, .unavailable)
|
||||
}
|
||||
|
||||
func testFallbacksActionToUnknown() throws {
|
||||
let context = try PostbuildContext(config, env: Self.SampleEnvs)
|
||||
|
||||
XCTAssertEqual(context.action, .unknown)
|
||||
}
|
||||
|
||||
func testReadsActionFromEnv() throws {
|
||||
var envs = Self.SampleEnvs
|
||||
envs["ACTION"] = "indexbuild"
|
||||
let context = try PostbuildContext(config, env: envs)
|
||||
|
||||
XCTAssertEqual(context.action, .index)
|
||||
}
|
||||
|
||||
func testReadsSingleArch() throws {
|
||||
var envs = Self.SampleEnvs
|
||||
envs["ARCHS"] = "x86_64"
|
||||
let context = try PostbuildContext(config, env: envs)
|
||||
|
||||
XCTAssertEqual(context.arch, "x86_64")
|
||||
}
|
||||
|
||||
func testReadsFirstArch() throws {
|
||||
var envs = Self.SampleEnvs
|
||||
envs["ARCHS"] = "x86_64 arm64"
|
||||
let context = try PostbuildContext(config, env: envs)
|
||||
|
||||
XCTAssertEqual(context.arch, "x86_64")
|
||||
}
|
||||
|
||||
func testFailsForEmptyArch() throws {
|
||||
var envs = Self.SampleEnvs
|
||||
envs["ARCHS"] = ""
|
||||
|
||||
XCTAssertThrowsError(try PostbuildContext(config, env: envs))
|
||||
}
|
||||
|
||||
func testFailsForMissingArch() throws {
|
||||
var envs = Self.SampleEnvs
|
||||
envs["ARCHS"] = nil
|
||||
|
||||
XCTAssertThrowsError(try PostbuildContext(config, env: envs))
|
||||
}
|
||||
|
||||
func testFindTempDirForCustomVariant() throws {
|
||||
var envs = Self.SampleEnvs
|
||||
envs["ARCHS"] = "x86_64"
|
||||
envs["CURRENT_VARIANT"] = "custom"
|
||||
envs["OBJECT_FILE_DIR_custom"] = "/OBJECT_FILE_DIR_custom"
|
||||
|
||||
let context = try PostbuildContext(config, env: envs)
|
||||
|
||||
XCTAssertEqual(context.compilationTempDir, "/OBJECT_FILE_DIR_custom/x86_64")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,9 @@ class PostbuildTests: FileXCTestCase {
|
||||
builtProductsDir: "",
|
||||
bundleDir: nil,
|
||||
derivedSourcesDir: "",
|
||||
thinnedTargets: []
|
||||
thinnedTargets: [],
|
||||
action: .build,
|
||||
modeMarkerPath: ""
|
||||
)
|
||||
private var network = RemoteNetworkClientImpl(
|
||||
NetworkClientFake(fileManager: .default),
|
||||
@@ -83,6 +85,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
)
|
||||
private var modeController = CacheModeControllerFake()
|
||||
private var metaReader = JsonMetaReader(fileAccessor: FileManager.default)
|
||||
private var metaWriter = JsonMetaWriter(fileWriter: FileManager.default, pretty: false)
|
||||
private static let SampleMeta = MainArtifactSampleMeta.defaults
|
||||
private var sampleMetaFile: URL!
|
||||
|
||||
@@ -121,6 +124,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: []
|
||||
)
|
||||
@@ -150,6 +154,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: []
|
||||
)
|
||||
@@ -177,6 +182,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: dsymOrganizer,
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: []
|
||||
)
|
||||
@@ -204,6 +210,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: dsymOrganizer,
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: []
|
||||
)
|
||||
@@ -241,6 +248,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: dsymOrganizer,
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: []
|
||||
)
|
||||
@@ -281,6 +289,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: dsymOrganizer,
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: []
|
||||
)
|
||||
@@ -306,6 +315,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: fakeModeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: []
|
||||
)
|
||||
@@ -353,6 +363,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [plugin],
|
||||
consumerPlugins: []
|
||||
)
|
||||
@@ -384,6 +395,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [plugin],
|
||||
consumerPlugins: []
|
||||
)
|
||||
@@ -416,6 +428,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: [consumerPlugin]
|
||||
)
|
||||
@@ -453,6 +466,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: [consumerPlugin]
|
||||
)
|
||||
@@ -484,6 +498,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: [consumerPlugin]
|
||||
)
|
||||
@@ -517,6 +532,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: []
|
||||
)
|
||||
@@ -549,6 +565,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: []
|
||||
)
|
||||
@@ -557,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(
|
||||
@@ -422,4 +424,40 @@ class SwiftcTests: FileXCTestCase {
|
||||
|
||||
XCTAssertNoThrow(try swiftc.mockCompilation())
|
||||
}
|
||||
|
||||
func testSkipsGeneratingObjectFileWhenNotProvidedInCompilationInfo() throws {
|
||||
let outputFilesDir = workingDir.appendingPathComponent("outputFiles")
|
||||
try fileManager.spt_createEmptyDir(outputFilesDir)
|
||||
let input = SwiftCompilationInfo(
|
||||
info: SwiftModuleCompilationInfo(
|
||||
dependencies: nil,
|
||||
swiftDependencies: outputFilesDir.appendingPathComponent("master.swiftdeps")
|
||||
),
|
||||
files: [
|
||||
SwiftFileCompilationInfo(
|
||||
file: "/file1.swift",
|
||||
dependencies: nil,
|
||||
object: nil,
|
||||
swiftDependencies: nil
|
||||
),
|
||||
]
|
||||
)
|
||||
swiftcInputReader = SwiftcInputReaderStub(info: input)
|
||||
let swiftc = Swiftc(
|
||||
inputFileListReader: inputFileListReader,
|
||||
markerReader: markerReader,
|
||||
allowedFilesListScanner: allowedFilesListScanner,
|
||||
artifactOrganizer: artifactOrganizer,
|
||||
inputReader: swiftcInputReader,
|
||||
context: context,
|
||||
markerWriter: markerWriter,
|
||||
productsGenerator: productsGenerator,
|
||||
fileManager: FileManager.default,
|
||||
dependenciesWriterFactory: FileDependenciesWriter.init,
|
||||
touchFactory: touchFactory,
|
||||
plugins: []
|
||||
)
|
||||
|
||||
XCTAssertNoThrow(try swiftc.mockCompilation())
|
||||
} // swiftlint:disable:next file_length
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ class TemplateBasedCCWrapperBuilderTests: FileXCTestCase {
|
||||
private static let history = "history.compile"
|
||||
private static let prebuild = "prebuild.d"
|
||||
private static let commitSha = "321"
|
||||
private static let timeout = 5.0
|
||||
private static let timeout = 10.0
|
||||
|
||||
static let xccc: URL = {
|
||||
let fileManager = FileManager.default
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -78,4 +78,41 @@ class DependenciesRemapperCompositeTests: XCTestCase {
|
||||
|
||||
XCTAssertEqual(localPath, ["/tmp/root/some.swift", "/pwd/other.swift"])
|
||||
}
|
||||
|
||||
func testRemapsMultipleMatchingMappers() throws {
|
||||
let remapper = DependenciesRemapperComposite([
|
||||
StringDependenciesRemapper(mappings: [StringDependenciesRemapper.Mapping(generic: "$(ROOT)", local: "/root")]),
|
||||
StringDependenciesRemapper(mappings: [StringDependenciesRemapper.Mapping(generic: "$(SPECIFIC)", local: "$(ROOT)/specific")])
|
||||
])
|
||||
let localPaths = ["/root/specific/file"]
|
||||
|
||||
let genericPaths = remapper.replace(localPaths: localPaths)
|
||||
|
||||
XCTAssertEqual(genericPaths, ["$(SPECIFIC)/file"])
|
||||
}
|
||||
|
||||
func testRemapsBackToLocalWithRevertedRemappersOrder() throws {
|
||||
let remapper = DependenciesRemapperComposite([
|
||||
StringDependenciesRemapper(mappings: [StringDependenciesRemapper.Mapping(generic: "$(ROOT)", local: "/root")]),
|
||||
StringDependenciesRemapper(mappings: [StringDependenciesRemapper.Mapping(generic: "$(SPECIFIC)", local: "$(ROOT)/specific")])
|
||||
])
|
||||
let genericPaths = ["$(SPECIFIC)/file"]
|
||||
|
||||
let localPaths = remapper.replace(genericPaths: genericPaths)
|
||||
|
||||
XCTAssertEqual(localPaths, ["/root/specific/file"])
|
||||
}
|
||||
|
||||
func testRemappingTwoMappingsBackAndForthIsIdentical() throws {
|
||||
let remapper = DependenciesRemapperComposite([
|
||||
StringDependenciesRemapper(mappings: [StringDependenciesRemapper.Mapping(generic: "$(ROOT)", local: "/root")]),
|
||||
StringDependenciesRemapper(mappings: [StringDependenciesRemapper.Mapping(generic: "$(SPECIFIC)", local: "$(ROOT)/specific")])
|
||||
])
|
||||
let localPaths = ["/root/specific/file"]
|
||||
|
||||
let genericPaths = remapper.replace(localPaths: localPaths)
|
||||
let remappedLocalPaths = remapper.replace(genericPaths: genericPaths)
|
||||
|
||||
XCTAssertEqual(localPaths, remappedLocalPaths)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ class FileFingerprintSyncerTests: FileXCTestCase {
|
||||
private var swiftmoduleDir: URL!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
syncer = FileFingerprintSyncer(
|
||||
fingerprintOverrideExtension: "md5",
|
||||
dirAccessor: fileManager,
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// 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 StringDependenciesRemapperFactoryTests: XCTestCase {
|
||||
private var factory: StringDependenciesRemapperFactory!
|
||||
|
||||
override func setUp() {
|
||||
factory = StringDependenciesRemapperFactory()
|
||||
}
|
||||
|
||||
func testMappingsFromEnvMaps() throws {
|
||||
let remapper = try factory.build(
|
||||
orderKeys: ["SRC_ROOT"],
|
||||
envs: ["SRC_ROOT": "/tmp/root"],
|
||||
customMappings: [:]
|
||||
)
|
||||
|
||||
let localPaths = remapper.replace(genericPaths: ["$(SRC_ROOT)/some.swift"])
|
||||
XCTAssertEqual(localPaths, ["/tmp/root/some.swift"])
|
||||
}
|
||||
|
||||
func testInvalidMappingsFromEnvFails() throws {
|
||||
XCTAssertThrowsError(
|
||||
try factory.build(
|
||||
orderKeys: ["SRC_ROOT"],
|
||||
envs: ["NO_SRC_ROOT": ""],
|
||||
customMappings: [:]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testBuildingRemapperWithMergedCustomMappings() throws {
|
||||
let remapper = try factory.build(
|
||||
orderKeys: ["PWD"],
|
||||
envs: ["PWD": "/some"],
|
||||
customMappings: ["TMP": "/tmp"]
|
||||
)
|
||||
|
||||
let genericPaths = remapper.replace(localPaths: ["/some/repoFile.swift", "/tmp/externalFile.swift"])
|
||||
XCTAssertEqual(genericPaths, ["$(PWD)/repoFile.swift", "$(TMP)/externalFile.swift"])
|
||||
}
|
||||
|
||||
func testFailsBuildingRemapperWithConflictedMappings() throws {
|
||||
XCTAssertThrowsError(
|
||||
try factory.build(
|
||||
orderKeys: ["PWD"],
|
||||
envs: ["PWD": "/some"],
|
||||
customMappings: ["PWD": "/other"]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -70,17 +70,29 @@ class StringDependenciesRemapperTests: XCTestCase {
|
||||
XCTAssertEqual(genericPaths, ["$(SRC_ROOT)/some.swift", "$(PWD)/extra.swift"])
|
||||
}
|
||||
|
||||
func testMappingsFromEnvMaps() throws {
|
||||
remapper = try StringDependenciesRemapper.buildFromEnvs(keys: ["SRC_ROOT"], envs: ["SRC_ROOT": "/tmp/root"])
|
||||
func testMappingsLocalPathsIsDoneInOrder() {
|
||||
let mappings: [StringDependenciesRemapper.Mapping] = [
|
||||
.init(generic: "$(TMP)", local: "/tmp"),
|
||||
.init(generic: "$(ROOT)", local: "$(TMP)/root"),
|
||||
]
|
||||
remapper = StringDependenciesRemapper(mappings: mappings)
|
||||
|
||||
let localPaths = remapper.replace(genericPaths: ["$(SRC_ROOT)/some.swift"])
|
||||
|
||||
let genericPaths = remapper.replace(localPaths: ["/tmp/root/some.swift"])
|
||||
|
||||
XCTAssertEqual(genericPaths, ["$(ROOT)/some.swift"])
|
||||
}
|
||||
|
||||
func testMappingsGenericPathsIsDoneInReversedOrder() {
|
||||
let mappings: [StringDependenciesRemapper.Mapping] = [
|
||||
.init(generic: "$(TMP)", local: "/tmp"),
|
||||
.init(generic: "$(ROOT)", local: "$(TMP)/root"),
|
||||
]
|
||||
remapper = StringDependenciesRemapper(mappings: mappings)
|
||||
|
||||
|
||||
let localPaths = remapper.replace(genericPaths: ["$(ROOT)/some.swift"])
|
||||
|
||||
XCTAssertEqual(localPaths, ["/tmp/root/some.swift"])
|
||||
}
|
||||
|
||||
func testInvalidMappingsFromEnvFAils() throws {
|
||||
XCTAssertThrowsError(
|
||||
try StringDependenciesRemapper.buildFromEnvs(keys: ["SRC_ROOT"], envs: ["NO_SRC_ROOT": ""])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -24,7 +24,7 @@ class EnvironmentFingerprintGeneratorTests: XCTestCase {
|
||||
|
||||
private static let defaultENV = [
|
||||
"GCC_PREPROCESSOR_DEFINITIONS": "GCC",
|
||||
"CLANG_PROFILE_DATA_DIRECTORY": "CLANG",
|
||||
"CLANG_COVERAGE_MAPPING": "YES",
|
||||
"TARGET_NAME": "TARGET",
|
||||
"CONFIGURATION": "CONG",
|
||||
"PLATFORM_NAME": "PLAT",
|
||||
@@ -33,6 +33,7 @@ class EnvironmentFingerprintGeneratorTests: XCTestCase {
|
||||
"DYLIB_COMPATIBILITY_VERSION": "2",
|
||||
"DYLIB_CURRENT_VERSION": "3",
|
||||
"PRODUCT_MODULE_NAME": "4",
|
||||
"ARCHS": "AR"
|
||||
]
|
||||
/// Corresponds to EnvironmentFingerprintGenerator.version
|
||||
private static let currentVersion = "5"
|
||||
@@ -55,7 +56,7 @@ class EnvironmentFingerprintGeneratorTests: XCTestCase {
|
||||
func testConsidersDefaultEnvs() throws {
|
||||
let fingerprint = try fingerprintGenerator.generateFingerprint()
|
||||
|
||||
XCTAssertEqual(fingerprint, "GCC,CLANG,TARGET,CONG,PLAT,XC,1,2,3,4,\(Self.currentVersion)")
|
||||
XCTAssertEqual(fingerprint, "GCC,YES,TARGET,CONG,PLAT,XC,1,2,3,4,AR,\(Self.currentVersion)")
|
||||
}
|
||||
|
||||
func testFingerprintIncludesVersionAsLastComponent() throws {
|
||||
@@ -73,7 +74,7 @@ class EnvironmentFingerprintGeneratorTests: XCTestCase {
|
||||
|
||||
let fingerprint = try fingerprintGenerator.generateFingerprint()
|
||||
|
||||
XCTAssertEqual(fingerprint, ",,,,,,,,,,\(Self.currentVersion)")
|
||||
XCTAssertEqual(fingerprint, ",,,,,,,,,,,\(Self.currentVersion)")
|
||||
}
|
||||
|
||||
func testConsidersCustomEnvs() throws {
|
||||
@@ -89,6 +90,6 @@ class EnvironmentFingerprintGeneratorTests: XCTestCase {
|
||||
|
||||
let fingerprint = try fingerprintGenerator.generateFingerprint()
|
||||
|
||||
XCTAssertEqual(fingerprint, "GCC,CLANG,TARGET,CONG,PLAT,XC,1,2,3,4,CUSTOM_VALUE,\(Self.currentVersion)")
|
||||
XCTAssertEqual(fingerprint, "GCC,YES,TARGET,CONG,PLAT,XC,1,2,3,4,AR,CUSTOM_VALUE,\(Self.currentVersion)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// 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, pretty: false)
|
||||
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, pretty: false)
|
||||
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)
|
||||
}
|
||||
|
||||
func testWritesPrettyMetaInValidFormat() throws {
|
||||
let fileAccessor = FileAccessorFake(mode: .normal)
|
||||
let writer = JsonMetaWriter(fileWriter: fileAccessor, pretty: true)
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
// 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 ActionSpecificCacheHitLoggerTests: FileXCTestCase {
|
||||
private var coordinator: StatsCoordinator!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
coordinator = InMemoryStatsCoordinator()
|
||||
}
|
||||
|
||||
func testReportsBuildHitLogsToAStandardBuild() throws {
|
||||
let logger = ActionSpecificCacheHitLogger(action: .build, statsLogger: coordinator)
|
||||
|
||||
try logger.logHit()
|
||||
|
||||
let allStats = try coordinator.readStats()
|
||||
XCTAssertEqual(
|
||||
allStats,
|
||||
.init(
|
||||
hitCount: 1,
|
||||
missCount: 0,
|
||||
localCacheBytes: 0,
|
||||
indexingHitCount: 0,
|
||||
indexingMissCount: 0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testReportsBuildMissToAStandardBuild() throws {
|
||||
let logger = ActionSpecificCacheHitLogger(action: .build, statsLogger: coordinator)
|
||||
|
||||
try logger.logMiss()
|
||||
|
||||
let allStats = try coordinator.readStats()
|
||||
XCTAssertEqual(
|
||||
allStats,
|
||||
.init(
|
||||
hitCount: 0,
|
||||
missCount: 1,
|
||||
localCacheBytes: 0,
|
||||
indexingHitCount: 0,
|
||||
indexingMissCount: 0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testReportsIndexbuildHitToIndexingBuild() throws {
|
||||
let logger = ActionSpecificCacheHitLogger(action: .index, statsLogger: coordinator)
|
||||
|
||||
try logger.logHit()
|
||||
|
||||
let allStats = try coordinator.readStats()
|
||||
XCTAssertEqual(
|
||||
allStats,
|
||||
.init(
|
||||
hitCount: 0,
|
||||
missCount: 0,
|
||||
localCacheBytes: 0,
|
||||
indexingHitCount: 1,
|
||||
indexingMissCount: 0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testReportsIndexbuildMissToIndexingBuild() throws {
|
||||
let logger = ActionSpecificCacheHitLogger(action: .index, statsLogger: coordinator)
|
||||
|
||||
try logger.logMiss()
|
||||
|
||||
let allStats = try coordinator.readStats()
|
||||
XCTAssertEqual(
|
||||
allStats,
|
||||
.init(
|
||||
hitCount: 0,
|
||||
missCount: 0,
|
||||
localCacheBytes: 0,
|
||||
indexingHitCount: 0,
|
||||
indexingMissCount: 1
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// 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
|
||||
@testable import XCRemoteCache
|
||||
|
||||
/// Fake that manages all logs in memory and reports 0 cached bytes
|
||||
/// Note: This fake is thread unsafe
|
||||
class InMemoryStatsCoordinator: StatsCoordinator {
|
||||
private var counters = [XCRemoteCacheStatistics.Counter: Int]()
|
||||
|
||||
func log(_ event: XCRemoteCacheStatistics.Counter) throws {
|
||||
counters[event, default: 0] += 1
|
||||
}
|
||||
|
||||
func readStats() throws -> XCRemoteCacheStatistics {
|
||||
XCRemoteCacheStatistics(
|
||||
hitCount: counters[.targetCacheHit] ?? 0,
|
||||
missCount: counters[.targetCacheMiss] ?? 0,
|
||||
localCacheBytes: 0,
|
||||
indexingHitCount: counters[.indexingTargetHitCount] ?? 0,
|
||||
indexingMissCount: counters[.indexingTargetMissCount] ?? 0
|
||||
)
|
||||
}
|
||||
|
||||
func reset() throws {
|
||||
counters = [:]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -44,6 +52,7 @@ An object that is passed to the `xcremotecache` can contain all properties suppo
|
||||
| `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` | ⬜️ |
|
||||
|
||||
## Uninstalling
|
||||
|
||||
@@ -52,3 +61,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).
|
||||
|
||||
@@ -17,6 +17,7 @@ require 'cocoapods/resolver'
|
||||
require 'open-uri'
|
||||
require 'yaml'
|
||||
require 'json'
|
||||
require 'pathname'
|
||||
|
||||
|
||||
module CocoapodsXCRemoteCacheModifier
|
||||
@@ -26,6 +27,7 @@ module CocoapodsXCRemoteCacheModifier
|
||||
FAKE_SRCROOT = "/#{'x' * 10 }"
|
||||
LLDB_INIT_COMMENT="#RemoteCacheCustomSourceMap"
|
||||
LLDB_INIT_PATH = "#{ENV['HOME']}/.lldbinit"
|
||||
FAT_ARCHIVE_NAME_INFIX = 'arm64-x86_64'
|
||||
|
||||
CUSTOM_CONFIGURATION_KEYS = [
|
||||
'enabled',
|
||||
@@ -45,18 +47,19 @@ 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' => [],
|
||||
'prettify_meta_files' => false
|
||||
}
|
||||
@@configuration.merge! default_values.select { |k, v| !@@configuration.key?(k) }
|
||||
end
|
||||
@@ -88,61 +91,96 @@ 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
|
||||
config.build_settings['XCRC_PLATFORM_PREFERRED_ARCH'] = ["$(LINK_FILE_LIST_$(CURRENT_VARIANT)_$(PLATFORM_PREFERRED_ARCH):dir:standardizepath:file:default=arm64)"]
|
||||
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)-$(LLVM_TARGET_TRIPLE_VENDOR)-$(SWIFT_PLATFORM_TARGET_PREFIX)$(LLVM_TARGET_TRIPLE_SUFFIX).swiftmodule.md5"
|
||||
"$(TARGET_BUILD_DIR)/$(MODULES_FOLDER_PATH)/$(PRODUCT_MODULE_NAME).swiftmodule/$(XCRC_PLATFORM_PREFERRED_ARCH).swiftmodule.md5",
|
||||
"$(TARGET_BUILD_DIR)/$(MODULES_FOLDER_PATH)/$(PRODUCT_MODULE_NAME).swiftmodule/$(XCRC_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
|
||||
|
||||
@@ -196,15 +234,18 @@ module CocoapodsXCRemoteCacheModifier
|
||||
release_url = 'https://api.github.com/repos/spotify/XCRemoteCache/releases/latest'
|
||||
asset_url = nil
|
||||
|
||||
open(release_url, :http_basic_authentication => credentials) do |f|
|
||||
asset_url = JSON.parse(f.read)['assets'][0]['url']
|
||||
URI.open(release_url) do |f|
|
||||
assets_array = JSON.parse(f.read)['assets']
|
||||
# Pick fat archive
|
||||
asset_array = assets_array.detect{|arr| arr['name'].include?(FAT_ARCHIVE_NAME_INFIX)}
|
||||
asset_url = asset_array['url']
|
||||
end
|
||||
|
||||
if asset_url.nil?
|
||||
throw "Downloading XCRemoteCache failed"
|
||||
end
|
||||
|
||||
open(asset_url, :http_basic_authentication => credentials, "accept" => 'application/octet-stream') do |f|
|
||||
URI.open(asset_url, "accept" => 'application/octet-stream') do |f|
|
||||
File.open(local_package_location, "wb") do |file|
|
||||
file.puts f.read
|
||||
end
|
||||
@@ -247,6 +288,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
|
||||
@@ -286,7 +328,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"
|
||||
@@ -306,22 +348,59 @@ 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)
|
||||
root_rcinfo = generate_rcinfo()
|
||||
save_rcinfo(root_rcinfo, user_proj_directory)
|
||||
|
||||
# Create directory for xccc & arc.rc location
|
||||
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 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}"
|
||||
})
|
||||
save_rcinfo(pods_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)
|
||||
@@ -334,26 +413,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.4"
|
||||
end
|
||||
|
||||
@@ -71,3 +71,5 @@ To enable thinning target on the consumer side:
|
||||
* Prefer using fakes instead of spies or mocks. Place testing doubles in [Tests/XCRemoteCacheTests/TestDoubles](../Tests/XCRemoteCacheTests/TestDoubles) so other tests can reuse them
|
||||
|
||||
* If you test a scenario that accesses a file on a disk, consider using the `DiskUsageSizeProviderTests` class that isolates a working directory and eliminates potential file leaks between testcases
|
||||
|
||||
* For dependency injection arguments, avoid passing default values (e.g. `init(dep: SomeDependency = SomeDependency())` and require passing explicit values (e.g. `init(dep: SomeDependency))`. It will be clear from a call site which dependencies is used and suggests adding a testcase when a new dependency is added.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
Reference in New Issue
Block a user