Compare commits

..

94 Commits

Author SHA1 Message Date
Bartosz Polaczyk f3832c31bd Merge pull request #55 from polac24/20220118-release-cocoapods-automatically
Release CocoaPods plugin automatically
2022-01-19 10:25:09 +01:00
Bartosz Polaczyk 6a7e6d1135 Add a section link 2022-01-18 21:55:17 +01:00
Bartosz Polaczyk da59c2a211 Document releasing to RubyGems 2022-01-18 21:51:16 +01:00
Bartosz Polaczyk eaba7f3c67 Publish to RubyGems 2022-01-18 21:46:27 +01:00
Bartosz Polaczyk bab3326175 Merge pull request #52 from polac24/20220116-cocoapods-plugin-release
Prepare Cocoapods plugin for a release
2022-01-17 12:21:07 +01:00
Bartosz Polaczyk dc0a82058b Bump plugin version 2022-01-16 12:56:46 +01:00
Bartosz Polaczyk c5c2732cc9 Expose prettify_meta_files and disable_certificate_verification to .rcinfo 2022-01-16 12:56:35 +01:00
Bartosz Polaczyk 46debcfa8a Merge pull request #51 from alekzernov/feature/ssl-cert-not-valid
Added a flag to disable checking the ssl certificate
2022-01-16 12:52:53 +01:00
Зернов Александр 3f6d3af5a5 code review. 2022-01-16 14:23:28 +03:00
Зернов Александр c42cb7ac55 fix hooks 2022-01-13 17:59:41 +03:00
Зернов Александр c65eccc5ee Add certificateVerification setting. 2022-01-13 15:22:06 +03:00
Aleksander Grzyb a7316d35cc Merge pull request #46 from spotify/adjust-xccc-for-apple-silicon
adjust `xccc` to support `arm64`
2022-01-06 13:49:37 +01:00
Aleksander Grzyb 62a7fea0be adjust xccc to support arm64 2022-01-06 09:52:25 +01:00
Bartosz Polaczyk 88e4dceb99 Merge pull request #45 from polac24/20220102-pods-rcinfo
[Pods] Generate unique .rcinfo for Pods directory
2022-01-05 22:00:35 +01:00
Bartosz Polaczyk e8db767d4a Merge pull request #44 from polac24/20211229-fingerprint-include-archs
Always include ARCHS in a fingerprint
2022-01-04 19:26:51 +01:00
Bartosz Polaczyk 12c635e5ca Merge pull request #42 from polac24/20211228-m1-consumer
Patch PLATFORM_PREFERRED_ARCH to support native Apple Silicon builds
2022-01-04 19:26:26 +01:00
Bartosz Polaczyk 7f95da7b7c Generate unique .rcinfo for Pods directory 2022-01-02 11:16:18 +01:00
Bartosz Polaczyk 5892a92546 Update Readme 2021-12-29 19:43:12 +01:00
Bartosz Polaczyk 7aa44f20c1 Include ARCHS in a figerprint 2021-12-29 19:03:26 +01:00
Bartosz Polaczyk 9df2bd5a8e Bump cocoapods plugin version 2021-12-28 19:09:34 +01:00
Bartosz Polaczyk 508b11d6ac Patch PLATFORM_PREFERRED_ARCH to support native Apple Silicon builds 2021-12-28 18:39:18 +01:00
Bartosz Polaczyk 5f2a8409f2 Merge pull request #41 from polac24/20211215-out-of-band
Add out_of_band mappings
2021-12-20 23:13:02 +01:00
Bartosz Polaczyk df627ca374 Prereview cleanup 2021-12-16 20:13:41 +01:00
Bartosz Polaczyk a905cdbddc Extract remapping factory 2021-12-16 20:06:34 +01:00
Bartosz Polaczyk 6995c7c1b7 Replace generic paths in the reverse order 2021-12-16 19:42:31 +01:00
Bartosz Polaczyk 7f43cb87bd Update readme 2021-12-16 19:42:06 +01:00
Bartosz Polaczyk 5ff9888c11 Align out of band with ENV replacements 2021-12-16 10:52:47 +01:00
Bartosz Polaczyk ad545c7802 Simplify path remapper initialization 2021-12-15 17:11:31 +01:00
Bartosz Polaczyk 057c5c3e28 Renames mapping property and adds docs 2021-12-15 16:59:47 +01:00
Bartosz Polaczyk 4116dba33d Add Composiete remapper tests 2021-12-15 16:50:26 +01:00
Bartosz Polaczyk 46cc3b75aa OutOfBandRemapping 2021-12-15 16:27:40 +01:00
Bartosz Polaczyk 2285822ae6 Merge pull request #39 from polac24/20211211-native-arch
Support producer mode on Apple Silicon
2021-12-13 08:35:10 +01:00
Bartosz Polaczyk e518d28723 Merge pull request #40 from polac24/20211212-dark
Add dark mode logo version
2021-12-13 08:34:55 +01:00
Bartosz Polaczyk 3b453b15bc Update README.md
Co-authored-by: PatrikBillgren <PatrikBillgren@users.noreply.github.com>
2021-12-13 08:22:34 +01:00
Bartosz Polaczyk 73edc2c7aa Add dark mode logo version 2021-12-12 16:29:41 +01:00
Bartosz Polaczyk a0c21471b9 Fox development readme 2021-12-12 14:57:12 +01:00
Bartosz Polaczyk 610946f0c4 Fix Readme 2021-12-12 14:51:35 +01:00
Bartosz Polaczyk 53d4f07286 Add readme documentation 2021-12-12 14:46:34 +01:00
Bartosz Polaczyk bc9a77a58f Revert "Bump CocoaPods plugin version"
This reverts commit 8f86917597.
2021-12-12 14:23:24 +01:00
Bartosz Polaczyk e0205f749a Document explicit dependencies rule 2021-12-12 14:23:05 +01:00
Bartosz Polaczyk 0d259b56d3 Recognize building architecturefrom ARCHS 2021-12-12 14:22:46 +01:00
Bartosz Polaczyk 366c485453 Revert "Replace PLATFORM_PREFERRED_ARCH with NATIVE_ARCH"
This reverts commit f9524a6854.
2021-12-12 13:56:20 +01:00
Bartosz Polaczyk 8f86917597 Bump CocoaPods plugin version 2021-12-11 18:36:04 +01:00
Bartosz Polaczyk f9524a6854 Replace PLATFORM_PREFERRED_ARCH with NATIVE_ARCH 2021-12-11 18:34:40 +01:00
Bartosz Polaczyk 9a27fa81a4 Merge pull request #35 from polac24/xcode-1310
Switch CI to Xcode 13.1
2021-12-10 12:11:32 +01:00
Bartosz Polaczyk c55f1a5803 Merge pull request #30 from mihai8804858/pretty-meta-files
Encode json files using pretty format
2021-12-09 22:05:03 +01:00
Mihai Seremet e4d277c8db Remove default value 2021-12-09 11:52:44 +02:00
Bartosz Polaczyk 6cd662bf7d Switch to Xcode 13.1 for releases 2021-12-08 18:39:18 +01:00
Bartosz Polaczyk 56081f75b3 Switch to 13.1 on PR 2021-12-08 18:38:42 +01:00
Mihai Seremet c1cd1ac565 Add pretty meta files parameter 2021-12-08 12:02:42 +02:00
Bartosz Polaczyk 768a296175 Merge pull request #32 from polac24/20211207-fix-coverage-builds
Inspect code coverage with CLANG_COVERAGE_MAPPING ENV
2021-12-08 09:08:41 +01:00
Bartosz Polaczyk 6a1a8c6919 Do not bump schema version 2021-12-08 08:05:51 +01:00
Bartosz Polaczyk 3d02af8ade Inspect code coverage with CLANG_COVERAGE_MAPPING 2021-12-07 20:41:37 +01:00
Bartosz Polaczyk bf90d518f4 Merge pull request #27 from polac24/20211201-debug-map-align
[Pods] Align Debub-prefix-map for Pods project
2021-12-05 20:35:03 +01:00
Bartosz Polaczyk 634afb3f3f Merge pull request #25 from polac24/20211122-producer-fast
Add producer-fast mode
2021-12-05 20:34:48 +01:00
Bartosz Polaczyk c2b80c0112 Do not return optional array in findTargetPackageZip 2021-12-02 22:28:07 +01:00
Bartosz Polaczyk d0604e9042 Fix producer full typo 2021-12-02 22:24:49 +01:00
Bartosz Polaczyk 297c1a90cb Merge remote-tracking branch 'upstream/master' into 20211201-debug-map-align 2021-12-02 22:22:14 +01:00
Bartosz Polaczyk 34cb54b675 Fix typo 2021-12-02 22:19:44 +01:00
Bartosz Polaczyk 14b2b3aceb [CocoapodsPlugin] Support first-party caching for incremental installation 2021-12-02 22:19:43 +01:00
Bartosz Polaczyk cbed913c63 Merge pull request #19 from polac24/20211121-incremental-installation
[CocoaPodsPlugin] Support first-party caching for incremental install
2021-12-02 10:55:47 +01:00
Bartosz Polaczyk 63448ff0a0 Fix typo 2021-12-02 08:58:19 +01:00
Bartosz Polaczyk 64bccaed16 Merge branch 'master' into 20211121-incremental-installation 2021-12-02 08:56:26 +01:00
Bartosz Polaczyk 80a7abb4d5 Update RDoc array definition 2021-12-02 07:26:36 +01:00
Bartosz Polaczyk c50ee6f798 Align Debub-prefix-map for Pods project 2021-12-01 19:47:44 +01:00
Bartosz Polaczyk 6e4bf25d1c Merge pull request #20 from polac24/20211121-update-spm-limitation
Provide a reason why SPM dependencies cannot be supported
2021-11-30 17:43:27 +01:00
Bartosz Polaczyk 71af03f227 Fix user message for producer-fast 2021-11-25 22:14:10 +01:00
Bartosz Polaczyk 0ebe6f5ceb Reuse existing meta sha 2021-11-25 22:11:04 +01:00
Bartosz Polaczyk 29cba26c5d Add tests 2021-11-25 21:38:55 +01:00
Bartosz Polaczyk 08b6115187 Merge remote-tracking branch 'upstream/master' into 20211122-producer-fast 2021-11-25 21:11:27 +01:00
Bartosz Polaczyk bbbb0a5b0f Add unit tests for Postbuild 2021-11-25 21:10:18 +01:00
Bartosz Polaczyk 758764ad95 Refactor to MetaWriter 2021-11-25 20:58:14 +01:00
Bartosz Polaczyk f332593076 Upload meta on the reused artifact scenario 2021-11-24 22:58:09 +01:00
Bartosz Polaczyk d0b2bc0f71 Prereview cleanup 2021-11-24 21:49:30 +01:00
Bartosz Polaczyk e2f68c8f4e Add unit tests for thinning creator plugin 2021-11-24 21:41:34 +01:00
Bartosz Polaczyk dbff760716 Merge pull request #24 from woodencoder/woodencooder/fix_swiftlint_warnings
Fix SwiftLint warnings
2021-11-24 19:47:32 +01:00
Bartosz Polaczyk bb05b02bd8 Merge pull request #22 from mihai8804858/reuse-existing-build-phases
Reuse existing build phases in CocoaPods plugin
2021-11-24 19:40:02 +01:00
Vladislav Klimenko 5c568a1338 Fix SwiftLint warnings 2021-11-24 21:07:40 +03:00
Mihai Seremet 86273017b4 Fix debugging for Pods project 2021-11-24 12:23:46 +02:00
Mihai Seremet 4f1f73132e Support switching between models in plugin 2021-11-23 00:28:08 +02:00
Bartosz Polaczyk ec1ef567cb Add producer-fast mode 2021-11-22 22:18:59 +01:00
Bartosz Polaczyk 1c94e51059 Add producerFast mode 2021-11-22 21:59:19 +01:00
Mihai Seremet ba41e40bb0 Reuse existing build phases in CocoaPods plugin 2021-11-22 19:04:51 +02:00
Bartosz Polaczyk a0c88d9059 Provide a reason why SPM dependencies cannot be supported 2021-11-21 19:41:21 +01:00
Bartosz Polaczyk 15173b9575 [CocoapodsPlugin] Support first-party caching for incremental installation 2021-11-21 14:17:34 +01:00
Bartosz Polaczyk fa82f920ad Merge pull request #17 from tejassharma96/patch-1
Update how and why link in readme
2021-11-17 22:19:28 +01:00
Tejas Sharma 78031a3135 Update how and why link in readme 2021-11-17 12:57:51 -08:00
Bartosz Polaczyk 2156de1706 Merge pull request #14 from polac24/support-readme
Add support session to Readme
2021-11-17 10:45:21 +01:00
Bartosz Polaczyk ce2ef3ea69 Merge pull request #13 from polac24/cocoapods-002
Release CocoaPods plugin 0.0.2
2021-11-17 10:45:02 +01:00
Bartosz Polaczyk 3219ac24aa Merge pull request #15 from eliperkins/patch-1
Fix typo in README
2021-11-17 08:21:23 +01:00
Eli Perkins d9ef32f24f Fix typo in README 2021-11-16 12:23:19 -05:00
Bartosz Polaczyk 3aaf483263 Add a section about slack support 2021-11-16 17:32:47 +01:00
Bartosz Polaczyk cc691b8c67 Add an option to install via RubyGems 2021-11-16 17:10:45 +01:00
Bartosz Polaczyk 4c95ff915f Release CocoaPods plugin 0.0.2 2021-11-16 17:04:18 +01:00
67 changed files with 1133 additions and 217 deletions
+1 -1
View File
@@ -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"
+27 -1
View File
@@ -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"
@@ -44,3 +44,29 @@ jobs:
file_glob: true
tag: ${{ github.ref }}
overwrite: true
cocoapods:
name: Publish CocoaPods plugin
runs-on: ubuntu-latest
permissions:
contents: read
defaults:
run:
working-directory: cocoapods-plugin
steps:
- uses: actions/checkout@v2
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.0'
- run: bundle install
- name: Publish to RubyGems
run: |
mkdir -p $HOME/.gem
touch $HOME/.gem/credentials
chmod 0600 $HOME/.gem/credentials
printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
gem build *.gemspec
CURRENT_VERSION=$(gem list cocoapods-xcremotecache --remote -q | sed 's/[^0-9\.]//g')
[ -f cocoapods-xcremotecache-$CURRENT_VERSION.gem ] && echo "Version $CURRENT_VERSION already exists" || gem push *.gem
env:
GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}"
+2 -1
View File
@@ -5,4 +5,5 @@
DerivedData
/.swiftpm/
releases
tmp/
tmp/
.idea/
+1
View File
@@ -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
+54 -8
View File
@@ -1,13 +1,15 @@
<p align="center">
<img src="docs/img/logo.png" width="75%">
<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._
[![Build Status](https://github.com/spotify/XCRemoteCache/workflows/CI/badge.svg)](https://github.com/spotify/XCRemoteCache/workflows/CI/badge.svg)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
[![Slack](https://slackin.spotify.com/badge.svg)](https://slackin.spotify.com)
- [How and Why?](#how-and-why-)
- [How and Why?](#how-and-why)
* [Accurate target input files](#accurate-target-input-files)
+ [New file added to the target](#new-file-added-to-the-target)
* [Debug symbols](#debug-symbols)
@@ -36,10 +38,14 @@ _XCRemoteCache is a remote cache tool for Xcode projects. It reuses target artif
* [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)
* [Releasing CocoaPods plugin](#releasing-cocoapods-plugin)
* [Building release package](#building-release-package)
- [Contributing](#contributing)
- [Code of conduct](#code-of-conduct)
@@ -50,7 +56,7 @@ _XCRemoteCache is a remote cache tool for Xcode projects. It reuses target artif
The caching mechanism is based on remote artifacts that should be generated and uploaded to the cache server for each commit on a `master` branch, preferably as a part of CI/CD step. Xcode products are not portable between different Xcode versions, each XCRemoteCache artifact is linked with a specific Xcode build number that generated it. To support multiple Xcode versions, artifacts generation should happen for each Xcode version.
The artifact resue flow is as follows: XCRemoteCache performs a target precheck (aka prebuild) and if a fingerprint for local sources matches the one computed on a generation side, several compilation steps wrappers (e.g. `xcswiftc`, `xccc`, `xclibtool`) mock corresponding compilation step(s) and linking (or archiving) moves the cached build artifact to the expected location.
The artifact reuse flow is as follows: XCRemoteCache performs a target precheck (aka prebuild) and if a fingerprint for local sources matches the one computed on a generation side, several compilation steps wrappers (e.g. `xcswiftc`, `xccc`, `xclibtool`) mock corresponding compilation step(s) and linking (or archiving) moves the cached build artifact to the expected location.
> Multiple commits that have the same target sources reuse artifact package on a remote server.
@@ -134,7 +140,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. | `""` | ⬜️ |
@@ -189,6 +195,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>
@@ -208,8 +215,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>
@@ -298,11 +305,13 @@ _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. | `[:]` | ⬜️ |
| `disable_certificate_verification` | A Boolean value that opts-in SSL certificate validation is disabled | `false` | ⬜️ |
## Backend cache server
@@ -358,6 +367,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
@@ -371,7 +405,7 @@ Head over to our [cocoapods-plugin](cocoapods-plugin/README.md) docs to see how
## Limitations
* Swift Package Manager (SPM) projects are not supported
* Swift Package Manager (SPM) dependencies are not supported. _Because SPM does not allow customizing Build Settings, XCRemoteCache cannot specify `clang` and `swiftc` wrappers that control if the local compilation should be skipped (cache hit) or not (cache miss)_
* Filenames with `_vers.c` suffix are reserved and cannot be used as a source file
* All compilation files should be referenced via the git repo root. Referencing `/AbsolutePath/someOther.swift` or `../../someOther.swift` that resolve to the location outside of the git repo root is prohibited.
@@ -388,6 +422,12 @@ Follow the [Development](docs/Development.md) guide. It has all the information
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.
### Releasing CocoaPods plugin
Bump a gem version defined in [gem_version.rb](cocoapods-plugin/lib/cocoapods-xcremotecache/gem_version.rb) and create a new release described above.
A plugin is automatically uploaded to [RubyGems](https://rubygems.org/gems/cocoapods-xcremotecache) if a given version doesn't exist yet.
### Building release package
To build a release zip package for a single platform (e.g. `x86_64-apple-macosx`, `arm64-apple-macosx`), call:
@@ -398,6 +438,12 @@ rake 'build[release, x86_64-apple-macosx]'
The zip package will be generated at `releases/XCRemoteCache.zip`.
## Support
Create a [new issue](https://github.com/spotify/XCRemoteCache/issues/new) with as many details as possible.
Reach us at the `#xcremotecache` channel in [Slack](https://slackin.spotify.com/).
## Contributing
We feel that a welcoming community is important and we ask that you follow Spotify's
+1 -1
View File
@@ -28,7 +28,7 @@ task :lint => [:prepare] do
puts 'Run linting'
system("swiftformat --lint --config .swiftformat --cache ignore .") or abort "swiftformat failure" if SWIFTFORMAT_ENABLED
system("swiftlint lint --config .swiftlint.yml") or abort "swiftlint failure" if SWIFTLINT_ENABLED
system("swiftlint lint --config .swiftlint.yml --strict") or abort "swiftlint failure" if SWIFTLINT_ENABLED
end
task :autocorrect => [:prepare] do
@@ -41,6 +41,7 @@ class BuildArtifactCreator: ArtifactSwiftProductsBuilderImpl, ArtifactCreator {
private let moduleName: String?
private let modulesFolderPath: String
private let dSYMPath: URL
private let metaWriter: MetaWriter
private let fileManager: FileManager
init(
@@ -50,6 +51,7 @@ class BuildArtifactCreator: ArtifactSwiftProductsBuilderImpl, ArtifactCreator {
moduleName: String?,
modulesFolderPath: String,
dSYMPath: URL,
metaWriter: MetaWriter,
fileManager: FileManager
) {
self.buildDir = buildDir
@@ -59,6 +61,7 @@ class BuildArtifactCreator: ArtifactSwiftProductsBuilderImpl, ArtifactCreator {
self.moduleName = moduleName
self.fileManager = fileManager
self.dSYMPath = dSYMPath
self.metaWriter = metaWriter
super.init(workingDir: tempDir, moduleName: moduleName, fileManager: fileManager)
}
@@ -72,7 +75,11 @@ class BuildArtifactCreator: ArtifactSwiftProductsBuilderImpl, ArtifactCreator {
let dynamicLibraryArtifacts = try prepareDynamicLibraryArtifacts()
zipPaths.append(contentsOf: dynamicLibraryArtifacts)
let creator = ZipArtifactCreator(workingDir: zipWorkingDir, fileManager: fileManager)
let creator = ZipArtifactCreator(
workingDir: zipWorkingDir,
metaWriter: metaWriter,
fileManager: fileManager
)
return try creator.createArtifact(zipContent: zipPaths, artifactKey: artifactKey, meta: meta)
}
@@ -23,11 +23,12 @@ import Zip
class ZipArtifactCreator {
/// Location where zip file should be generated
private let workingDir: URL
private let metaWriter: MetaWriter
private let fileManager: FileManager
private let metaEncoder = JSONEncoder()
init(workingDir: URL, fileManager: FileManager) {
init(workingDir: URL, metaWriter: MetaWriter, fileManager: FileManager) {
self.workingDir = workingDir
self.metaWriter = metaWriter
self.fileManager = fileManager
}
@@ -35,18 +36,10 @@ class ZipArtifactCreator {
let zipURL = workingDir.appendingPathComponent("\(artifactKey).zip")
try fileManager.createDirectory(at: workingDir, withIntermediateDirectories: true, attributes: nil)
// Include meta json to the artifact
let metaURL = try dumpMeta(meta)
let metaURL = try metaWriter.write(meta, locationDir: workingDir)
let zipPaths = zipContent + [metaURL]
try Zip.zipFiles(paths: zipPaths, zipFilePath: zipURL, password: nil, progress: nil)
return Artifact(id: artifactKey, package: zipURL, meta: metaURL)
}
// Save meta to a local file
private func dumpMeta<T: Meta>(_ meta: T) throws -> URL {
let metaURL = workingDir.appendingPathComponent(meta.fileKey).appendingPathExtension("json")
let metaData = try metaEncoder.encode(meta)
try fileManager.spt_writeToFile(atPath: metaURL.path, contents: metaData)
return metaURL
}
}
@@ -33,13 +33,16 @@ enum ThinningCreatorPluginError: Error {
/// Warning! This plugin assumes that producer's DerivedData are always cleaned before a build
class ThinningCreatorPlugin: ArtifactCreatorPlugin {
private let targetTempDir: URL
private let modeMarkerPath: String
private let dirScanner: DirScanner
/// Default Initializer
/// - Parameter targetTempDir: Location of current target-specific temp dir (TARGET_TEMP_DIR)
/// - Parameter modeMarkerPath: path of maker file that informs if a given target can reuse remote artifacts.
/// - Parameter dirScanner: scanner to access disk and read files and directories hierarchy
init(targetTempDir: URL, dirScanner: DirScanner) {
init(targetTempDir: URL, modeMarkerPath: String, dirScanner: DirScanner) {
self.targetTempDir = targetTempDir
self.modeMarkerPath = modeMarkerPath
self.dirScanner = dirScanner
}
@@ -57,31 +60,19 @@ class ThinningCreatorPlugin: ArtifactCreatorPlugin {
let fileKey: String
}
let uploadedTargetArtifacts = try allURLs.compactMap { tempDir -> TargetTuple? in
// All targets that uploaded their artifacts, have it placed in the
// `$(TARGET_TEMP_DIR)/xccache/produced/{{fileKey}}.zip` location. Find all targets that have such a file
let targetGeneratedArtifactRootDir = tempDir
.appendingPathComponent("xccache")
.appendingPathComponent("produced")
guard try dirScanner.itemType(atPath: targetGeneratedArtifactRootDir.path) == ItemType.dir else {
// given target didn't generate any artifacts (e.g. it is never cached with XCRemoteCache)
return nil
}
let allFilesProduced = try dirScanner.items(at: targetGeneratedArtifactRootDir)
let allArtifacts = allFilesProduced.filter { $0.pathExtension == "zip" }
guard !allArtifacts.isEmpty else {
let potentialArtifacts = try findTargetPackageZip(tempDir: tempDir)
guard !potentialArtifacts.isEmpty else {
// there is no generated *.zip file, so given target didn't create an artifact - it could be
// just a helper target (like the target we integrate this plugin with)
return nil
}
// Find {{fileKey}} based on the .zip file basename
guard allArtifacts.count == 1 else {
guard potentialArtifacts.count == 1 else {
throw ThinningCreatorPluginError.noSingleTargetArtifactsGenerated(
rootDir: targetGeneratedArtifactRootDir
rootDir: tempDir
)
}
let fileKey = allArtifacts[0].deletingPathExtension().lastPathComponent
let fileKey = potentialArtifacts[0].deletingPathExtension().lastPathComponent
// Taking target name from tempDir, which has a structures "*.build"
let targetName = tempDir.deletingPathExtension().lastPathComponent
return TargetTuple(targetName: targetName, fileKey: fileKey)
@@ -96,6 +87,41 @@ class ThinningCreatorPlugin: ArtifactCreatorPlugin {
return Dictionary(uniqueKeysWithValues: extraKeysTuples)
}
private func findTargetPackageZip(tempDir: URL) throws -> [URL] {
// Producer mode:
// All targets that uploaded their artifacts, have it placed in the
// `$(TARGET_TEMP_DIR)/xccache/produced/{{fileKey}}.zip` location. Find all targets that have such a file
// ProducerFast mode:
// If a target reused already existing artifact, it still has `$(TARGET_TEMP_DIR)/rc.enabled` marker file
// and the reused zip is placed in:
// `$(TARGET_TEMP_DIR)/xccache/{{fileKey}}.zip` location.
let targetEnabledMarker = tempDir.appendingPathComponent(modeMarkerPath)
let targetReusedArtifactRootDir = tempDir.appendingPathComponent("xccache")
let targetGeneratedArtifactRootDir = tempDir
.appendingPathComponent("xccache")
.appendingPathComponent("produced")
let pathToDirWithZipArtifacts: URL
// try the `.producerFast` scenario first (the artifact was not locally
// generated but just reused from the remote cache)
if try dirScanner.itemType(atPath: targetEnabledMarker.path) == ItemType.file {
pathToDirWithZipArtifacts = targetReusedArtifactRootDir
} else {
// cover a case when a target was build locally and an artifact
// has just been created (locally)
guard try dirScanner.itemType(atPath: targetGeneratedArtifactRootDir.path) == ItemType.dir else {
// given target didn't generate any artifacts (e.g. it is never cached with XCRemoteCache)
return []
}
pathToDirWithZipArtifacts = targetGeneratedArtifactRootDir
}
let allFilesProduced = try dirScanner.items(at: pathToDirWithZipArtifacts)
let allArtifacts = allFilesProduced.filter { $0.pathExtension == "zip" }
return allArtifacts
}
func artifactToUpload(main: MainArtifactMeta) throws -> [Artifact] {
return []
}
@@ -40,7 +40,7 @@ class ThinningDiskSwiftcProductsGenerator: SwiftcProductsGenerator {
destinationSwiftmodulePaths = Dictionary(
uniqueKeysWithValues: SwiftmoduleFileExtension.SwiftmoduleExtensions
.map { ext, _ in
switch (ext) {
switch ext {
case .swiftsourceinfo:
let dest = modulePathDir.appendingPathComponent("Project")
.appendingPathComponent(moduleName)
@@ -41,6 +41,7 @@ class Postbuild {
private let dSYMOrganizer: DSYMOrganizer
private let modeController: CacheModeController
private let metaReader: MetaReader
private let metaWriter: MetaWriter
private let creatorPlugins: [ArtifactCreatorPlugin]
private let consumerPlugins: [ArtifactConsumerPostbuildPlugin]
@@ -58,6 +59,7 @@ class Postbuild {
dSYMOrganizer: DSYMOrganizer,
modeController: CacheModeController,
metaReader: MetaReader,
metaWriter: MetaWriter,
creatorPlugins: [ArtifactCreatorPlugin],
consumerPlugins: [ArtifactConsumerPostbuildPlugin]
) {
@@ -74,6 +76,7 @@ class Postbuild {
self.dSYMOrganizer = dSYMOrganizer
self.modeController = modeController
self.metaReader = metaReader
self.metaWriter = metaWriter
self.creatorPlugins = creatorPlugins
self.consumerPlugins = consumerPlugins
}
@@ -125,6 +128,22 @@ class Postbuild {
try generateFingerprintOverrides(contextSpecificFingerprint: fingerprint.contextSpecific)
}
/// Uploads only a meta to the remote server - useful when the file artifact (.zip) already exists on a remote
/// server and only a meta for a current commit sha has to be uploaded
public func performMetaUpload(meta: MainArtifactMeta, for commit: String) throws {
// Reset plugins keys as these are unique to each
var meta = meta
meta.pluginsKeys = [:]
meta = try creatorPlugins.reduce(meta) { prevMeta, plugin in
var meta = prevMeta
// add extra keys from the plugin. A plugin overrides previously defined keys in case of duplication
meta.pluginsKeys = try meta.pluginsKeys.merging(plugin.extraMetaKeys(prevMeta), uniquingKeysWith: { $1 })
return meta
}
let metaPath = try metaWriter.write(meta, locationDir: context.targetTempDir)
try networkClient.uploadSynchronously(metaPath, as: .meta(commit: commit))
}
/// Builds an artifact package and uploads it to the remote server
public func performBuildUpload(for commit: String) throws {
let dependencies = try generateDependencies()
@@ -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
@@ -73,6 +78,7 @@ public struct PostbuildContext {
var thinnedTargets: [String]
/// Action type: build, indexbuild etc.
var action: BuildActionType
let modeMarkerPath: String
}
extension PostbuildContext {
@@ -81,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")
@@ -115,5 +126,6 @@ extension PostbuildContext {
let thinFocusedTargetsString: String = env.readEnv(key: "SPT_XCREMOTE_CACHE_THINNED_TARGETS") ?? ""
thinnedTargets = thinFocusedTargetsString.split(separator: ",").map(String.init)
action = (try? BuildActionType(rawValue: env.readEnv(key: "ACTION"))) ?? .unknown
modeMarkerPath = config.modeMarkerPath
}
}
@@ -66,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,
@@ -85,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,
@@ -92,6 +94,7 @@ public class XCPostbuild {
moduleName: context.moduleName,
modulesFolderPath: context.modulesFolderPath,
dSYMPath: context.dSYMPath,
metaWriter: metaWriter,
fileManager: fileManager
)
let dirAccessor = DirAccessorComposer(
@@ -205,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)
@@ -229,6 +233,7 @@ public class XCPostbuild {
dSYMOrganizer: dSYMOrganizer,
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: creatorPlugins,
consumerPlugins: consumerPlugins
)
@@ -243,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)
@@ -261,9 +277,8 @@ public class XCPostbuild {
} else {
try postbuildAction.performBuildCleanup()
try cacheHitLogger.logMiss()
// Producer mode doesn't use cached artifacts so modeController is not enabled. If producer
// reaches this point, there were no issues with publishing
let actionName = context.mode == .producer ? "Published" : "Disabled"
// If producers reach this point, there were no issues with publishing
let actionName = context.mode == .consumer ? "Disabled" : "Published"
printToUser("\(actionName) remote cache for \(context.targetName)")
}
} catch PluginError.unrecoverableError(let error) {
@@ -55,6 +55,7 @@ class Prebuild {
self.artifactConsumerPrebuildPlugins = artifactConsumerPrebuildPlugins
}
// swiftlint:disable:next function_body_length
public func perform() throws -> PrebuildResult {
guard case .available(let commit) = context.remoteCommit else {
return .incompatible
@@ -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
@@ -24,6 +24,8 @@ public struct SwiftcContext {
case producer
/// Commit sha of the commit to use during remote cache
case consumer(commit: RemoteCommitInfo)
/// Remote artifact exists and can be optimistically used in place of a local compilation
case producerFast
}
let objcHeaderOutput: URL
@@ -74,6 +76,14 @@ public struct SwiftcContext {
mode = .consumer(commit: remoteCommit)
case .producer:
mode = .producer
case .producerFast:
let remoteCommit = RemoteCommitInfo(try? String(contentsOf: remoteCommitLocation).trim())
switch remoteCommit {
case .unavailable:
mode = .producer
case .available:
mode = .producerFast
}
}
invocationHistoryFile = URL(fileURLWithPath: config.compilationHistoryFile, relativeTo: tempDir)
}
@@ -116,6 +116,12 @@ class SwiftcOrchestrator {
}
case .consumer:
fallbackToDefault(command: swiftcCommand)
case .producerFast:
let compileStepResult = try swiftc.mockCompilation()
if case .forceFallback = compileStepResult {
// cannot reuse cached artifact. Build it locally and upload to the server just as for the producer
fallthrough
}
case .producer:
var swiftcArgs = ProcessInfo().arguments
swiftcArgs = try producerFallbackCommandProcessors.reduce(swiftcArgs) { args, processor in
@@ -101,7 +101,10 @@ public class XCSwiftc {
objcHeaderOutput: context.objcHeaderOutput,
diskCopier: HardLinkDiskCopier(fileManager: fileManager)
)
let allInvocationsStorage = ExistingFileStorage(storageFile: context.invocationHistoryFile, command: swiftcCommand)
let allInvocationsStorage = ExistingFileStorage(
storageFile: context.invocationHistoryFile,
command: swiftcCommand
)
// When fallbacking to local compilation do not call historical `swiftc` invocations
// The current fallback invocation already compiles all files in a target
let invocationStorage = FilteredInvocationStorage(
+1
View File
@@ -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,21 @@ 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] = [:]
/// If true, SSL certificate validation is disabled
var disableCertificateVerification: Bool = false
}
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 +176,13 @@ 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
merge.disableCertificateVerification = scheme.disableCertificateVerification ?? disableCertificateVerification
return merge
}
@@ -221,10 +237,13 @@ 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]?
let disableCertificateVerification: Bool?
// Yams library doesn't support encoding strategy, see https://github.com/jpsim/Yams/issues/84
enum CodingKeys: String, CodingKey {
@@ -262,10 +281,13 @@ 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"
case disableCertificateVerification = "disable_certificate_verification"
}
}
@@ -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
)
}
}
}
@@ -0,0 +1,32 @@
// 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
final class IgnoringCertificatesTrustManager: NSObject, URLSessionDelegate {
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard let serverTrust = challenge.protectionSpace.serverTrust else {
completionHandler(.performDefaultHandling, nil)
return
}
let urlCredential = URLCredential(trust: serverTrust)
completionHandler(.useCredential, urlCredential)
}
}
@@ -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,
@@ -36,6 +36,15 @@ class DefaultURLSessionFactory: URLSessionFactory {
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = config.requestCustomHeaders
configuration.timeoutIntervalForRequest = config.timeoutResponseDataChunksInterval
return URLSession(configuration: configuration)
switch config.disableCertificateVerification {
case true:
return URLSession(
configuration: configuration,
delegate: IgnoringCertificatesTrustManager(),
delegateQueue: nil
)
case false:
return URLSession(configuration: configuration)
}
}
}
@@ -28,8 +28,8 @@ protocol CacheHitLogger {
/// Logs target hit or miss, based on an action of a build
class ActionSpecificCacheHitLogger: CacheHitLogger {
private let statsLogger: StatsLogger
private let hitCounter: XCRemoteCacheStatistics.Counter
private let missCounter: XCRemoteCacheStatistics.Counter
private let hitCounter: XCRemoteCacheStatistics.Counter?
private let missCounter: XCRemoteCacheStatistics.Counter?
init(action: BuildActionType, statsLogger: StatsLogger) {
self.statsLogger = statsLogger
@@ -37,19 +37,24 @@ class ActionSpecificCacheHitLogger: CacheHitLogger {
case .index:
hitCounter = .indexingTargetHitCount
missCounter = .indexingTargetMissCount
case .unknown:
fallthrough
case .build:
hitCounter = .targetCacheHit
missCounter = .targetCacheMiss
case .unknown:
hitCounter = nil
missCounter = nil
}
}
func logHit() throws {
try statsLogger.log(hitCounter)
if let hitCounter = hitCounter {
try statsLogger.log(hitCounter)
}
}
func logMiss() throws {
try statsLogger.log(missCounter)
if let missCounter = missCounter {
try statsLogger.log(missCounter)
}
}
}
@@ -31,6 +31,7 @@ class ArtifactSwiftProductsBuilderImplTests: FileXCTestCase {
private var builder: ArtifactSwiftProductsBuilderImpl!
override func setUpWithError() throws {
try super.setUpWithError()
let rootDir = try prepareTempDir()
moduleDir = rootDir.appendingPathComponent("Products")
swiftmoduleFile = moduleDir.appendingPathComponent("MyModule.swiftmodule")
@@ -47,24 +48,41 @@ class ArtifactSwiftProductsBuilderImplTests: FileXCTestCase {
func testIncludesRequiredSwiftmoduleFiles() throws {
try fileManager.spt_createFile(swiftmoduleFile, content: "swiftmodule")
try fileManager.spt_createFile(swiftmoduleDocFile, content: "swiftdoc")
let builderSwiftmoduleDir = builder.buildingArtifactSwiftModulesLocation().appendingPathComponent("arm64")
let expectedBuildedSwiftmoduleFile = builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftmodule")
let expectedBuildedSwiftmoduledocFile = builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftdoc")
let builderSwiftmoduleDir =
builder
.buildingArtifactSwiftModulesLocation()
.appendingPathComponent("arm64")
let expectedBuildedSwiftmoduleFile =
builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftmodule")
let expectedBuildedSwiftmoduledocFile =
builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftdoc")
try builder.includeModuleDefinitionsToTheArtifact(arch: "arm64", moduleURL: swiftmoduleFile)
XCTAssertEqual(fileManager.contents(atPath: expectedBuildedSwiftmoduleFile.path), "swiftmodule".data(using: .utf8))
XCTAssertEqual(fileManager.contents(atPath: expectedBuildedSwiftmoduledocFile.path), "swiftdoc".data(using: .utf8))
XCTAssertEqual(
fileManager.contents(atPath: expectedBuildedSwiftmoduleFile.path),
"swiftmodule".data(using: .utf8)
)
XCTAssertEqual(
fileManager.contents(atPath: expectedBuildedSwiftmoduledocFile.path),
"swiftdoc".data(using: .utf8)
)
}
func testIncludesAllSwiftmoduleFiles() throws {
try fileManager.spt_createEmptyFile(swiftmoduleFile)
try fileManager.spt_createEmptyFile(swiftmoduleDocFile)
try fileManager.spt_createEmptyFile(swiftmoduleSourceInfoFile)
let builderSwiftmoduleDir = builder.buildingArtifactSwiftModulesLocation().appendingPathComponent("arm64")
let expectedBuildedSwiftmoduleFile = builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftmodule")
let expectedBuildedSwiftmoduledocFile = builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftdoc")
let expectedBuildedSwiftSourceInfoFile = builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftsourceinfo")
let builderSwiftmoduleDir =
builder
.buildingArtifactSwiftModulesLocation()
.appendingPathComponent("arm64")
let expectedBuildedSwiftmoduleFile =
builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftmodule")
let expectedBuildedSwiftmoduledocFile =
builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftdoc")
let expectedBuildedSwiftSourceInfoFile =
builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftsourceinfo")
try builder.includeModuleDefinitionsToTheArtifact(arch: "arm64", moduleURL: swiftmoduleFile)
@@ -74,6 +92,11 @@ class ArtifactSwiftProductsBuilderImplTests: FileXCTestCase {
}
func testFailsIncludingWhenMissingRequiredSwiftmoduleFiles() throws {
XCTAssertThrowsError(try builder.includeModuleDefinitionsToTheArtifact(arch: "arm64", moduleURL: swiftmoduleFile))
XCTAssertThrowsError(
try builder.includeModuleDefinitionsToTheArtifact(
arch: "arm64",
moduleURL: swiftmoduleFile
)
)
}
}
@@ -63,6 +63,7 @@ class BuildArtifactCreatorTests: FileXCTestCase {
moduleName: "Target",
modulesFolderPath: "",
dSYMPath: dSYM,
metaWriter: JsonMetaWriter(fileWriter: fileManager, 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 {
@@ -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"
])
}
}
@@ -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 {
@@ -87,4 +88,45 @@ class PostbuildContextTests: FileXCTestCase {
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")
}
}
@@ -50,7 +50,8 @@ class PostbuildTests: FileXCTestCase {
bundleDir: nil,
derivedSourcesDir: "",
thinnedTargets: [],
action: .build
action: .build,
modeMarkerPath: ""
)
private var network = RemoteNetworkClientImpl(
NetworkClientFake(fileManager: .default),
@@ -84,6 +85,7 @@ class PostbuildTests: FileXCTestCase {
)
private var modeController = CacheModeControllerFake()
private var metaReader = JsonMetaReader(fileAccessor: FileManager.default)
private var metaWriter = JsonMetaWriter(fileWriter: FileManager.default, pretty: false)
private static let SampleMeta = MainArtifactSampleMeta.defaults
private var sampleMetaFile: URL!
@@ -122,6 +124,7 @@ class PostbuildTests: FileXCTestCase {
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [],
consumerPlugins: []
)
@@ -151,6 +154,7 @@ class PostbuildTests: FileXCTestCase {
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [],
consumerPlugins: []
)
@@ -178,6 +182,7 @@ class PostbuildTests: FileXCTestCase {
dSYMOrganizer: dsymOrganizer,
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [],
consumerPlugins: []
)
@@ -205,6 +210,7 @@ class PostbuildTests: FileXCTestCase {
dSYMOrganizer: dsymOrganizer,
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [],
consumerPlugins: []
)
@@ -242,6 +248,7 @@ class PostbuildTests: FileXCTestCase {
dSYMOrganizer: dsymOrganizer,
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [],
consumerPlugins: []
)
@@ -282,6 +289,7 @@ class PostbuildTests: FileXCTestCase {
dSYMOrganizer: dsymOrganizer,
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [],
consumerPlugins: []
)
@@ -307,6 +315,7 @@ class PostbuildTests: FileXCTestCase {
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
modeController: fakeModeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [],
consumerPlugins: []
)
@@ -354,6 +363,7 @@ class PostbuildTests: FileXCTestCase {
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [plugin],
consumerPlugins: []
)
@@ -385,6 +395,7 @@ class PostbuildTests: FileXCTestCase {
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [plugin],
consumerPlugins: []
)
@@ -417,6 +428,7 @@ class PostbuildTests: FileXCTestCase {
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [],
consumerPlugins: [consumerPlugin]
)
@@ -454,6 +466,7 @@ class PostbuildTests: FileXCTestCase {
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [],
consumerPlugins: [consumerPlugin]
)
@@ -485,6 +498,7 @@ class PostbuildTests: FileXCTestCase {
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [],
consumerPlugins: [consumerPlugin]
)
@@ -518,6 +532,7 @@ class PostbuildTests: FileXCTestCase {
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [],
consumerPlugins: []
)
@@ -550,6 +565,7 @@ class PostbuildTests: FileXCTestCase {
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [],
consumerPlugins: []
)
@@ -558,5 +574,68 @@ class PostbuildTests: FileXCTestCase {
try postbuild.deleteFingerprintOverrides()
XCTAssertFalse(fileManager.fileExists(atPath: previousFingerprintOverride.path))
} // swiftlint:disable:next file_length
}
func testUploadingMeta() throws {
let postbuild = Postbuild(
context: postbuildContext,
networkClient: network,
remapper: remapper,
fingerprintAccumulator: fingerprintGenerator,
artifactsOrganizer: organizer,
artifactCreator: artifactCreator,
fingerprintSyncer: syncer,
dependenciesReader: dependenciesReader,
dependencyProcessor: processor,
fingerprintOverrideManager: overrideManager,
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [],
consumerPlugins: []
)
try postbuild.performMetaUpload(meta: Self.SampleMeta, for: "33")
let data = try network.fetch(.meta(commit: "33"))
let downloadedMeta = try metaReader.read(data: data)
XCTAssertEqual(downloadedMeta, Self.SampleMeta)
}
func testUploadingMetaWithNewPluginKeys() throws {
let plugin = MetaAppenderArtifactCreatorPlugin(["New": "Value"])
let postbuild = Postbuild(
context: postbuildContext,
networkClient: network,
remapper: remapper,
fingerprintAccumulator: fingerprintGenerator,
artifactsOrganizer: organizer,
artifactCreator: artifactCreator,
fingerprintSyncer: syncer,
dependenciesReader: dependenciesReader,
dependencyProcessor: processor,
fingerprintOverrideManager: overrideManager,
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [plugin],
consumerPlugins: []
)
var meta = Self.SampleMeta
meta.pluginsKeys = ["Previous": "Value"]
var expectedMeta = meta
expectedMeta.pluginsKeys = ["New": "Value"]
try postbuild.performMetaUpload(meta: meta, for: "33")
let data = try network.fetch(.meta(commit: "33"))
let downloadedMeta = try metaReader.read(data: data)
XCTAssertEqual(downloadedMeta, expectedMeta)
}
}
// swiftlint:disable:next file_length
@@ -28,6 +28,7 @@ class FileLLDBInitPatcherTests: XCTestCase {
private var patcher: FileLLDBInitPatcher!
override func setUp() {
super.setUp()
accessor = FileAccessorFake(mode: .normal)
patcher = FileLLDBInitPatcher(
file: lldbInitPath,
@@ -62,4 +62,19 @@ class SwiftcContextTests: FileXCTestCase {
XCTAssertEqual(context.mode, .consumer(commit: .unavailable))
}
func testProducerModeWhenFileWithCommitShaExistsIsResolvedToProducerFast() throws {
config.mode = .producerFast
let context = try SwiftcContext(config: config, input: input)
XCTAssertEqual(context.mode, .producerFast)
}
func testProducerModeWhenFileWithCommitShaDoesntExxistIsResolvedToProducer() throws {
config.mode = .producerFast
try fileManager.spt_deleteItem(at: remoteCommitFile)
let context = try SwiftcContext(config: config, input: input)
XCTAssertEqual(context.mode, .producer)
}
}
@@ -21,8 +21,8 @@
import XCTest
class SwiftcFilemapInputEditorTests: FileXCTestCase {
private let sampleInfo = SwiftCompilationInfo(info: SwiftModuleCompilationInfo(
private let sampleInfo = SwiftCompilationInfo(
info: SwiftModuleCompilationInfo(
dependencies: nil,
swiftDependencies: "/"
), files: [])
@@ -65,17 +65,19 @@ class SwiftcFilemapInputEditorTests: FileXCTestCase {
}
}
"""#.data(using: .utf8)!
let expectedInfo = SwiftCompilationInfo(info: SwiftModuleCompilationInfo(
dependencies: "/master.d",
swiftDependencies: "/master.swiftdeps"
), files: [
SwiftFileCompilationInfo(
file: "/file1.swift",
dependencies: "/file1.d",
object: "/file1.o",
swiftDependencies: "/file1.swiftdeps"
let expectedInfo = SwiftCompilationInfo(
info: SwiftModuleCompilationInfo(
dependencies: "/master.d",
swiftDependencies: "/master.swiftdeps"
),
])
files: [
SwiftFileCompilationInfo(
file: "/file1.swift",
dependencies: "/file1.d",
object: "/file1.o",
swiftDependencies: "/file1.swiftdeps"
),
])
try fileManager.spt_writeToFile(atPath: inputFile.path, contents: infoContentData)
let readInfo = try editor.read()
@@ -93,17 +95,18 @@ class SwiftcFilemapInputEditorTests: FileXCTestCase {
}
func testWritingSavesContentWithOptionalParameters() throws {
let extendedInfo = SwiftCompilationInfo(info: SwiftModuleCompilationInfo(
dependencies: "/master.d",
swiftDependencies: "/master.swiftdeps"
), files: [
SwiftFileCompilationInfo(
file: "/file1.swift",
dependencies: "/file1.d",
object: "/file1.o",
swiftDependencies: "/file1.swiftdeps"
),
])
let extendedInfo = SwiftCompilationInfo(
info: SwiftModuleCompilationInfo(
dependencies: "/master.d",
swiftDependencies: "/master.swiftdeps"
), files: [
SwiftFileCompilationInfo(
file: "/file1.swift",
dependencies: "/file1.d",
object: "/file1.o",
swiftDependencies: "/file1.swiftdeps"
),
])
try editor.write(extendedInfo)
@@ -190,4 +190,45 @@ class SwiftcOrchestratorTests: XCTestCase {
XCTAssertNotNil(shellOutSpy.switchedProcess)
}
func testForFailedCompilationMockInProducerFastModeBuildsArtifactObjCHeader() throws {
let swiftc = SwiftcMock(mockingResult: .forceFallback)
let orchestrator = SwiftcOrchestrator(
mode: .producerFast,
swiftc: swiftc,
swiftcCommand: "",
objcHeaderOutput: objcHeaderURL,
moduleOutput: moduleOutputURL,
arch: "archTest",
artifactBuilder: artifactBuilder,
producerFallbackCommandProcessors: [],
invocationStorage: invocationStorage,
shellOut: shellOutSpy
)
try orchestrator.run()
XCTAssertEqual(artifactBuilder.addedObjCHeaders, ["archTest": [objcHeaderURL]])
}
func testSuccessedMockInProducerFastModeDoesntFillObjCHeader() throws {
let swiftc = SwiftcMock(mockingResult: .success)
let orchestrator = SwiftcOrchestrator(
mode: .producerFast,
swiftc: swiftc,
swiftcCommand: "",
objcHeaderOutput: objcHeaderURL,
moduleOutput: moduleOutputURL,
arch: "arch",
artifactBuilder: artifactBuilder,
producerFallbackCommandProcessors: [],
invocationStorage: invocationStorage,
shellOut: shellOutSpy
)
try orchestrator.run()
XCTAssertEqual(artifactBuilder.addedObjCHeaders, [:])
}
}
@@ -277,7 +277,9 @@ class SwiftcTests: FileXCTestCase {
let artifactObjCHeader = URL(fileURLWithPath: "/cachedArtifact/include/archTest/Target-Swift.h")
let artifactSwiftmodule = URL(fileURLWithPath: "/cachedArtifact/swiftmodule/archTest/Target.swiftmodule")
let artifactSwiftdoc = URL(fileURLWithPath: "/cachedArtifact/swiftmodule/archTest/Target.swiftdoc")
let artifactSwiftSourceInfo = URL(fileURLWithPath: "/cachedArtifact/swiftmodule/archTest/Target.swiftsourceinfo")
let artifactSwiftSourceInfo = URL(
fileURLWithPath: "/cachedArtifact/swiftmodule/archTest/Target.swiftsourceinfo"
)
artifactOrganizer = ArtifactOrganizerFake(artifactRoot: artifactRoot)
let swiftc = Swiftc(
@@ -457,5 +459,5 @@ class SwiftcTests: FileXCTestCase {
)
XCTAssertNoThrow(try swiftc.mockCompilation())
}
} // swiftlint:disable:next file_length
}
@@ -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)
@@ -25,6 +25,7 @@ class ActionSpecificCacheHitLoggerTests: FileXCTestCase {
private var coordinator: StatsCoordinator!
override func setUp() {
super.setUp()
coordinator = InMemoryStatsCoordinator()
}
+14
View File
@@ -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,8 @@ 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` | ⬜️ |
| `disable_certificate_verification` | A Boolean value that opts-in SSL certificate validation is disabled | `false` | ⬜️ |
## Uninstalling
@@ -52,3 +62,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
@@ -28,6 +29,7 @@ module CocoapodsXCRemoteCacheModifier
LLDB_INIT_PATH = "#{ENV['HOME']}/.lldbinit"
FAT_ARCHIVE_NAME_INFIX = 'arm64-x86_64'
# List of plugins' user properties that should be copied to .rcinfo
CUSTOM_CONFIGURATION_KEYS = [
'enabled',
'xcrc_location',
@@ -36,7 +38,9 @@ module CocoapodsXCRemoteCacheModifier
'final_target',
'check_build_configuration',
'check_platform',
'modify_lldb_init'
'modify_lldb_init',
'prettify_meta_files',
'disable_certificate_verification'
]
class XCRemoteCache
@@ -46,18 +50,20 @@ 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,
'disable_certificate_verification' => false
}
@@configuration.merge! default_values.select { |k, v| !@@configuration.key?(k) }
end
@@ -89,61 +95,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
@@ -251,6 +292,7 @@ module CocoapodsXCRemoteCacheModifier
# Returns the content (array of lines) of the lldbinit with stripped XCRemoteCache rewrite
def self.clean_lldbinit_content(lldbinit_path)
all_lines = []
return all_lines unless File.exist?(lldbinit_path)
File.open(lldbinit_path) { |file|
while(line = file.gets) != nil
line = line.strip
@@ -290,7 +332,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"
@@ -299,7 +341,6 @@ module CocoapodsXCRemoteCacheModifier
end
validate_configuration()
mode = @@configuration['mode']
xccc_location = @@configuration['xccc_file']
remote_commit_file = @@configuration['remote_commit_file']
@@ -310,22 +351,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)
@@ -338,26 +416,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.5"
end
+2
View File
@@ -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