Compare commits

...

107 Commits

Author SHA1 Message Date
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
Bartosz Polaczyk 41829e769f Merge pull request #12 from spotify/20211116-cocoapods-artifact
Download XCRemoteCache binary from GitHub for CocoaPods
2021-11-16 08:49:27 +01:00
Bartosz Polaczyk e3261d9bb1 Download XCRemoteCache binary from GitHub 2021-11-16 08:12:03 +01:00
Bartosz Polaczyk b6318e9785 Merge pull request #11 from polac24/add-logo
Add logo and TOC
2021-11-14 19:24:49 +01:00
Bartosz Polaczyk 5a77ab1766 Add TOC 2021-11-14 15:50:28 +01:00
Bartosz Polaczyk faec67a2ff Add logo image 2021-11-14 15:37:54 +01:00
Bartosz Polaczyk 3e309f2b64 Merge pull request #6 from spotify/readme-spm-limitation
Document releasing process and add SPM limitation
2021-10-22 09:47:47 +02:00
Bartosz Polaczyk 88f4d30735 Be explicit about tag formatting
Co-authored-by: Vadim Smal <kenshin312@gmail.com>
2021-10-20 14:31:21 +02:00
Bartosz Polaczyk e3d45b6ad5 Merge pull request #8 from spotify/20211017-place-swiftsourceinfo
Place sourceinfo in Project subdirectory
2021-10-20 11:19:03 +02:00
Bartosz Polaczyk 1c09a7a242 Merge pull request #10 from spotify/20211018-add-indexing-stats
Introduce separate stats for indexbuild
2021-10-20 11:18:37 +02:00
Bartosz Polaczyk 1468d315ab Merge pull request #9 from spotify/20211018-fix-indexbuild-caching
Support not defined object for Swift output map
2021-10-20 11:17:17 +02:00
Bartosz Polaczyk 2077897871 Add unit tests for action 2021-10-18 22:27:45 +02:00
Bartosz Polaczyk b2b1a93911 Add unit test 2021-10-18 21:50:59 +02:00
Bartosz Polaczyk 1205fec047 Add indexing stats 2021-10-18 21:50:32 +02:00
Bartosz Polaczyk a9ee8dceb3 Revert "Merge pull request #5 from CognitiveDisson/fix-cache-miss-for-xcode13"
This reverts commit 0f44da7bea, reversing
changes made to caed3b253c.
2021-10-18 21:50:32 +02:00
Bartosz Polaczyk 80515205cc Revert "Merge pull request #7 from spotify/20211017-fix-indexbuild"
This reverts commit 764219fa7d, reversing
changes made to 0f44da7bea.
2021-10-18 21:50:32 +02:00
Bartosz Polaczyk 999dabed84 Support no object for Swift output map 2021-10-18 11:47:18 +02:00
Bartosz Polaczyk 4fbb063094 Update release package creation snippet 2021-10-18 07:52:30 +02:00
Bartosz Polaczyk 600648d2d5 Place swiftsourceinfo in a Project directory for the thinning plugin 2021-10-18 07:47:03 +02:00
Bartosz Polaczyk 764219fa7d Merge pull request #7 from spotify/20211017-fix-indexbuild
Produce step .d file for indexbuild
2021-10-17 22:42:33 +02:00
Bartosz Polaczyk 86c74e61e2 Place sourceinfo in a subdirectory 2021-10-17 22:34:13 +02:00
Bartosz Polaczyk 0139392ec9 Produce step .d file for indexbuild 2021-10-17 20:03:21 +02:00
Bartosz Polaczyk d25b4e0e06 Document releasing process and add SPM limitations 2021-10-15 14:23:54 +02:00
Vadim Smal 0f44da7bea Merge pull request #5 from CognitiveDisson/fix-cache-miss-for-xcode13
Add ACTION env check for prebuild and postbuild
2021-10-15 08:59:09 +01:00
Vadim Smal 9052558d60 Add action type check for prebuild and postbuild 2021-10-14 16:27:15 +01:00
75 changed files with 1599 additions and 230 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"
+1 -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"
+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
+94 -8
View File
@@ -1,15 +1,61 @@
# XCRemoteCache
<p align="center">
<img src="docs/img/logo.png#gh-light-mode-only" width="75%">
<img src="docs/img/logo-dark.png#gh-dark-mode-only" width="75%">
</p>
XCRemoteCache is a remote cache tool for Xcode projects. It reuses target artifacts generated on a remote machine, served from a simple REST server.
_XCRemoteCache is a remote cache tool for Xcode projects. It reuses target artifacts generated on a remote machine, served from a simple REST server._
[![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)
* [Accurate target input files](#accurate-target-input-files)
+ [New file added to the target](#new-file-added-to-the-target)
* [Debug symbols](#debug-symbols)
* [Performance optimizations](#performance-optimizations)
* [Focused targets](#focused-targets)
- [How to integrate XCRemoteCache with your Xcode project?](#how-to-integrate-xcremotecache-with-your-xcode-project)
* [1. Download XCRemoteCache](#1-download-xcremotecache)
* [A. Automatic integration](#a-automatic-integration)
+ [2. Create a minimal XCRemoteCache configuration](#2-create-a-minimal-xcremotecache-configuration)
+ [3. Run automatic integration script](#3-run-automatic-integration-script)
- [3a. Producer side](#3a-producer-side)
- [3b. Consumer side](#3b-consumer-side)
* [A full list of `xcprepare integrate` supported options](#a-full-list-of-xcprepare-integrate-supported-options)
* [B. Manual integration](#b-manual-integration)
+ [2. Configure XCRemoteCache](#2-configure-xcremotecache)
+ [3. Call xcprepare](#3-call-xcprepare)
+ [4. Integrate with the Xcode project](#4-integrate-with-the-xcode-project)
+ [5. Configure LLDB source-map (Optional)](#5-configure-lldb-source-map-optional)
+ [6. Producer mode - Artifacts generation](#6-producer-mode---artifacts-generation)
- [6a. Configure producer mode](#6a-configure-producer-mode)
- [6b. Fill the cache](#6b-fill-the-cache)
- [6c. Mark commit sha](#6c-mark-commit-sha)
- [A full list of configuration parameters:](#a-full-list-of-configuration-parameters)
- [Backend cache server](#backend-cache-server)
* [Sample REST cache server from a docker image](#sample-rest-cache-server-from-a-docker-image)
* [Amazon S3 and Google Cloud Storage](#amazon-s3-and-google-cloud-storage)
- [CocoaPods plugin](#cocoapods-plugin)
- [Requirements](#requirements)
- [Apple silicon support](#apple-silicon-support)
* [Artifacts per architecture (Recommended)](#artifacts-per-architecture-recommended)
* [Fat artifacts](#fat-artifacts)
- [Limitations](#limitations)
- [FAQ](#faq)
- [Development](#development)
- [Release](#release)
* [Building release package](#building-release-package)
- [Contributing](#contributing)
- [Code of conduct](#code-of-conduct)
- [License](#license)
- [Security Issues?](#security-issues)
## How and Why?
The caching mechanism is based on remote artifacts that should be generated and uploaded to the cache server for each commit on a `master` branch, preferably as a part of CI/CD step. Xcode products are not portable between different Xcode versions, each XCRemoteCache artifact is linked with a specific Xcode build number that generated it. To support multiple Xcode versions, artifacts generation should happen for each Xcode version.
The artifact resue flow is as follows: XCRemoteCache performs a target precheck (aka prebuild) and if a fingerprint for local sources matches the one computed on a generation side, several compilation steps wrappers (e.g. `xcswiftc`, `xccc`, `xclibtool`) mock corresponding compilation step(s) and linking (or archiving) moves the cached build artifact to the expected location.
The artifact reuse flow is as follows: XCRemoteCache performs a target precheck (aka prebuild) and if a fingerprint for local sources matches the one computed on a generation side, several compilation steps wrappers (e.g. `xcswiftc`, `xccc`, `xclibtool`) mock corresponding compilation step(s) and linking (or archiving) moves the cached build artifact to the expected location.
> Multiple commits that have the same target sources reuse artifact package on a remote server.
@@ -93,7 +139,7 @@ xcremotecache/xcprepare integrate --input <yourProject.xcodeproj> --mode consume
| Argument | Description | Default | Required |
| ------------- | ------------- | ------------- | ------------- |
| `--input` | .xcodeproj location | N/A | ✅ |
| `--mode` | mode. Supported values: `consumer`, `producer` | N/A | ✅ |
| `--mode` | mode. Supported values: `consumer`, `producer`, `producer-fast`(experimental) | N/A | ✅ |
| `--targets-include` | comma-separated list of targets to integrate XCRemoteCache. | `""` | ⬜️ |
| `--targets-exclude` | comma-separated list of targets to not integrate XCRemoteCache. Takes priority over --targets-include. | `""` | ⬜️ |
| `--configurations-include` | comma-separated list of configurations to integrate XCRemoteCache. | `""` | ⬜️ |
@@ -148,6 +194,7 @@ Configure Xcode targets that **should use** XCRemoteCache:
* `SWIFT_EXEC` - location of `xcprepare` (e.g. `xcremotecache/xcswiftc`)
* `LIBTOOL` - location of `xclibtool` (e.g. `xcremotecache/xclibtool`)
* `LD` - location of `xcld` (e.g. `xcremotecache/xcld`)
* `XCRC_PLATFORM_PREFERRED_ARCH` - `$(LINK_FILE_LIST_$(CURRENT_VARIANT)_$(PLATFORM_PREFERRED_ARCH):dir:standardizepath:file:default=arm64)`
<details>
<summary>Screenshot</summary>
@@ -167,8 +214,8 @@ Configure Xcode targets that **should use** XCRemoteCache:
* command: `"$SCRIPT_INPUT_FILE_0"`
* input files: location of `xcpostbuild` command (e.g. `xcremotecache/xcpostbuild`)
* output files:
* `$(TARGET_BUILD_DIR)/$(MODULES_FOLDER_PATH)/$(PRODUCT_MODULE_NAME).swiftmodule/$(PLATFORM_PREFERRED_ARCH).swiftmodule.md5`
* `$(TARGET_BUILD_DIR)/$(MODULES_FOLDER_PATH)/$(PRODUCT_MODULE_NAME).swiftmodule/$(PLATFORM_PREFERRED_ARCH)-$(LLVM_TARGET_TRIPLE_VENDOR)-$(SWIFT_PLATFORM_TARGET_PREFIX)$(LLVM_TARGET_TRIPLE_SUFFIX).swiftmodule.md5`
* `$(TARGET_BUILD_DIR)/$(MODULES_FOLDER_PATH)/$(PRODUCT_MODULE_NAME).swiftmodule/$(XCRC_PLATFORM_PREFERRED_ARCH).swiftmodule.md5`
* `$(TARGET_BUILD_DIR)/$(MODULES_FOLDER_PATH)/$(PRODUCT_MODULE_NAME).swiftmodule/$(XCRC_PLATFORM_PREFERRED_ARCH)-$(LLVM_TARGET_TRIPLE_VENDOR)-$(SWIFT_PLATFORM_TARGET_PREFIX)$(LLVM_TARGET_TRIPLE_SUFFIX).swiftmodule.md5`
* discovery dependency file: `$(TARGET_TEMP_DIR)/postbuild.d`
<details>
@@ -257,10 +304,12 @@ _Note that for the `producer` mode, the prebuild build phase and `xccc`, `xcld`,
| `product_files_extensions_with_content_override ` | List of all extensions that should carry over source fingerprints. Extensions of all product files that contain non-deterministic content (absolute paths, timestamp, etc) should be included. | `["swiftmodule"]` | ⬜️ |
| `thinning_enabled ` | If true, support for thin projects is enabled | `false` | ⬜️ |
| `thinning_target_module_name ` | Module name of a target that works as a helper for thinned targets | `"ThinningRemoteCacheModule"` | ⬜️ |
| `prettify_meta_files` | A Boolean value that opts-in pretty JSON formatting for meta files | `false` | ⬜️ |
| `aws_secret_key` | Secret key for AWS V4 Signature Authorization. If this is set to a non-empty String - an Authentication Header will be added based on this and the other `aws_*` parameters.| `""` | ⬜️ |
| `aws_access_key` | Access key for AWS V4 Signature Authorization. | `""` | ⬜️ |
| `aws_region` | Region for AWS V4 Signature Authorization. E.g. `eu`. | `""` | ⬜️ |
| `aws_service` | Service for AWS V4 Signature Authorization. E.g. `storage`. | `""` | ⬜️ |
| `out_of_band_mappings` | A dictionary of files path remapping that should be applied to make it absolute path agnostic on a list of dependencies. Useful if a project refers files out of repo root, either compilation files or precompiled dependencies. Keys represent generic replacement and values are substrings that should be replaced. Example: for mapping `["COOL_LIBRARY": "/CoolLibrary"]` `/CoolLibrary/main.swift`will be represented as `$(COOL_LIBRARY)/main.swift`). Warning: remapping order is not-deterministic so avoid remappings with multiple matchings. | `[:]` | ⬜️ |
## Backend cache server
@@ -317,6 +366,31 @@ Retention Policy: Buckets usually have a retention policy option which ensures o
Head over to our [cocoapods-plugin](cocoapods-plugin/README.md) docs to see how to integrate XCRemoteCache in your CocoaPods project.
## Apple silicon support
### Artifacts per architecture (Recommended)
_If all of your machines (both producer and all consumers have the same architecture, either Intel or Apple Silicon), you don't have to do anything._
XCRemoteCache supports building artifacts for Apple silicon consumers. Is it recommended to build separately for `x86_64` and `arm64` architectures to have single-architecture artifacts that do not require downloading irrelevant binaries. Here are required steps if you want to support both Intel and Apple silicon consumers.
* Building for a simulator on a producer: run a first build for `x86_64`, clean a build and build again for `arm64`, e.g.:
```
xcodebuild ARCHS=x86_64 ONLY_ACTIVE_ARCH=NO build ...
xcodebuild clean
xcodebuild ARCHS=arm64 ONLY_ACTIVE_ARCH=NO build ...
```
### Fat artifacts
If you prefer to generate far artifacts (with both Intel and Apple silicon binaries), you can disable "Build Archive Architecture Only" on a producer side, e.g.
```
xcodebuild ONLY_ACTIVE_ARCH=NO build ...
```
Note: This setup is not recommended and may not be supported in future XCRemoteCache releases.
## Requirements
* The repo under `git` version control
@@ -330,6 +404,7 @@ Head over to our [cocoapods-plugin](cocoapods-plugin/README.md) docs to see how
## Limitations
* Swift Package Manager (SPM) dependencies are not supported. _Because SPM does not allow customizing Build Settings, XCRemoteCache cannot specify `clang` and `swiftc` wrappers that control if the local compilation should be skipped (cache hit) or not (cache miss)_
* Filenames with `_vers.c` suffix are reserved and cannot be used as a source file
* All compilation files should be referenced via the git repo root. Referencing `/AbsolutePath/someOther.swift` or `../../someOther.swift` that resolve to the location outside of the git repo root is prohibited.
@@ -343,14 +418,25 @@ Follow the [Development](docs/Development.md) guide. It has all the information
## Release
To build a release zip package, call:
To release a version, in [Releases](https://github.com/spotify/XCRemoteCache/releases) draft a new release with `v0.3.0{-rc0}` tag format.
Packages with binaries will be automatically uploaded to the GitHub [Releases](https://github.com/spotify/XCRemoteCache/releases) page.
### Building release package
To build a release zip package for a single platform (e.g. `x86_64-apple-macosx`, `arm64-apple-macosx`), call:
```shell
CONFIG=Release rake build
rake 'build[release, x86_64-apple-macosx]'
```
The zip package will be generated at `releases/XCRemoteCache.zip`.
## Support
Create a [new issue](https://github.com/spotify/XCRemoteCache/issues/new) with as many details as possible.
Reach us at the `#xcremotecache` channel in [Slack](https://slackin.spotify.com/).
## Contributing
We feel that a welcoming community is important and we ask that you follow Spotify's
+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
}
}
@@ -80,7 +80,7 @@ class ThinningConsumerUnzippedArtifactSwiftProductsOrganizerFactory: ThinningCon
moduleName: moduleName
)
return DiskSwiftcProductsGenerator(
return ThinningDiskSwiftcProductsGenerator(
modulePathOutput: modulePathOutput,
objcHeaderOutput: objcHeaderOutput,
diskCopier: diskCopier
@@ -44,7 +44,10 @@ class DefaultSwiftProductsArchitecturesRecognizer: SwiftProductsArchitecturesRec
let moduleDirectory = builtProductsDir
.appendingPathComponent(moduleName)
.appendingPathExtension(Self.SwiftmoduleDirExtension)
let productFiles = try dirAccessor.items(at: moduleDirectory)
// Skip folders (e.g. 'Project' dir that stores .sourceinfo, introduced in Xcode13)
let productFiles = try dirAccessor.items(at: moduleDirectory).filter { url in
try dirAccessor.itemType(atPath: url.path) == .file
}
/// files in a moduleDirectory have basename corresponding to the
/// architecture (e.g. 'x86_64-apple-ios-simulator.swiftmodule', 'x86_64.swiftmodule' ...)
let architectures = productFiles.map { file -> String in
@@ -55,7 +58,7 @@ class DefaultSwiftProductsArchitecturesRecognizer: SwiftProductsArchitecturesRec
}
return basenameFile.lastPathComponent
}
// remove duplicates comming from files with different extensions (swiftmodule, swiftdoc etc.)
// remove duplicates coming from files with different extensions (swiftmodule, swiftdoc etc.)
return Set(architectures).sorted()
}
}
@@ -33,13 +33,16 @@ enum ThinningCreatorPluginError: Error {
/// Warning! This plugin assumes that producer's DerivedData are always cleaned before a build
class ThinningCreatorPlugin: ArtifactCreatorPlugin {
private let targetTempDir: URL
private let modeMarkerPath: String
private let dirScanner: DirScanner
/// Default Initializer
/// - Parameter targetTempDir: Location of current target-specific temp dir (TARGET_TEMP_DIR)
/// - Parameter modeMarkerPath: path of maker file that informs if a given target can reuse remote artifacts.
/// - Parameter dirScanner: scanner to access disk and read files and directories hierarchy
init(targetTempDir: URL, dirScanner: DirScanner) {
init(targetTempDir: URL, modeMarkerPath: String, dirScanner: DirScanner) {
self.targetTempDir = targetTempDir
self.modeMarkerPath = modeMarkerPath
self.dirScanner = dirScanner
}
@@ -57,31 +60,19 @@ class ThinningCreatorPlugin: ArtifactCreatorPlugin {
let fileKey: String
}
let uploadedTargetArtifacts = try allURLs.compactMap { tempDir -> TargetTuple? in
// All targets that uploaded their artifacts, have it placed in the
// `$(TARGET_TEMP_DIR)/xccache/produced/{{fileKey}}.zip` location. Find all targets that have such a file
let targetGeneratedArtifactRootDir = tempDir
.appendingPathComponent("xccache")
.appendingPathComponent("produced")
guard try dirScanner.itemType(atPath: targetGeneratedArtifactRootDir.path) == ItemType.dir else {
// given target didn't generate any artifacts (e.g. it is never cached with XCRemoteCache)
return nil
}
let allFilesProduced = try dirScanner.items(at: targetGeneratedArtifactRootDir)
let allArtifacts = allFilesProduced.filter { $0.pathExtension == "zip" }
guard !allArtifacts.isEmpty else {
let potentialArtifacts = try findTargetPackageZip(tempDir: tempDir)
guard !potentialArtifacts.isEmpty else {
// there is no generated *.zip file, so given target didn't create an artifact - it could be
// just a helper target (like the target we integrate this plugin with)
return nil
}
// Find {{fileKey}} based on the .zip file basename
guard allArtifacts.count == 1 else {
guard potentialArtifacts.count == 1 else {
throw ThinningCreatorPluginError.noSingleTargetArtifactsGenerated(
rootDir: targetGeneratedArtifactRootDir
rootDir: tempDir
)
}
let fileKey = allArtifacts[0].deletingPathExtension().lastPathComponent
let fileKey = potentialArtifacts[0].deletingPathExtension().lastPathComponent
// Taking target name from tempDir, which has a structures "*.build"
let targetName = tempDir.deletingPathExtension().lastPathComponent
return TargetTuple(targetName: targetName, fileKey: fileKey)
@@ -96,6 +87,41 @@ class ThinningCreatorPlugin: ArtifactCreatorPlugin {
return Dictionary(uniqueKeysWithValues: extraKeysTuples)
}
private func findTargetPackageZip(tempDir: URL) throws -> [URL] {
// Producer mode:
// All targets that uploaded their artifacts, have it placed in the
// `$(TARGET_TEMP_DIR)/xccache/produced/{{fileKey}}.zip` location. Find all targets that have such a file
// ProducerFast mode:
// If a target reused already existing artifact, it still has `$(TARGET_TEMP_DIR)/rc.enabled` marker file
// and the reused zip is placed in:
// `$(TARGET_TEMP_DIR)/xccache/{{fileKey}}.zip` location.
let targetEnabledMarker = tempDir.appendingPathComponent(modeMarkerPath)
let targetReusedArtifactRootDir = tempDir.appendingPathComponent("xccache")
let targetGeneratedArtifactRootDir = tempDir
.appendingPathComponent("xccache")
.appendingPathComponent("produced")
let pathToDirWithZipArtifacts: URL
// try the `.producerFast` scenario first (the artifact was not locally
// generated but just reused from the remote cache)
if try dirScanner.itemType(atPath: targetEnabledMarker.path) == ItemType.file {
pathToDirWithZipArtifacts = targetReusedArtifactRootDir
} else {
// cover a case when a target was build locally and an artifact
// has just been created (locally)
guard try dirScanner.itemType(atPath: targetGeneratedArtifactRootDir.path) == ItemType.dir else {
// given target didn't generate any artifacts (e.g. it is never cached with XCRemoteCache)
return []
}
pathToDirWithZipArtifacts = targetGeneratedArtifactRootDir
}
let allFilesProduced = try dirScanner.items(at: pathToDirWithZipArtifacts)
let allArtifacts = allFilesProduced.filter { $0.pathExtension == "zip" }
return allArtifacts
}
func artifactToUpload(main: MainArtifactMeta) throws -> [Artifact] {
return []
}
@@ -0,0 +1,84 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Generator that produces all products in the DerivedData's Products locations, using provided disk copier
class ThinningDiskSwiftcProductsGenerator: SwiftcProductsGenerator {
private let destinationSwiftmodulePaths: [SwiftmoduleFileExtension: URL]
private let modulePathOutput: URL
private let objcHeaderOutput: URL
private let diskCopier: DiskCopier
init(
modulePathOutput: URL,
objcHeaderOutput: URL,
diskCopier: DiskCopier
) {
self.modulePathOutput = modulePathOutput
let modulePathBasename = modulePathOutput.deletingPathExtension()
let modulePathDir = modulePathOutput.deletingLastPathComponent()
let moduleName = modulePathBasename.lastPathComponent
// all swiftmodule-related should be located next to the ".swiftmodule"
// except of '.swiftsourceinfo', which should be placed in 'Project' dir
destinationSwiftmodulePaths = Dictionary(
uniqueKeysWithValues: SwiftmoduleFileExtension.SwiftmoduleExtensions
.map { ext, _ in
switch ext {
case .swiftsourceinfo:
let dest = modulePathDir.appendingPathComponent("Project")
.appendingPathComponent(moduleName)
.appendingPathExtension(ext.rawValue)
return (ext, dest)
default:
return (ext, modulePathBasename.appendingPathExtension(ext.rawValue))
}
}
)
self.objcHeaderOutput = objcHeaderOutput
self.diskCopier = diskCopier
}
func generateFrom(
artifactSwiftModuleFiles sourceAtifactSwiftModuleFiles: [SwiftmoduleFileExtension: URL],
artifactSwiftModuleObjCFile: URL
) throws -> URL {
// Move cached -Swift.h file to the expected location
try diskCopier.copy(file: artifactSwiftModuleObjCFile, destination: objcHeaderOutput)
for (ext, url) in sourceAtifactSwiftModuleFiles {
let dest = destinationSwiftmodulePaths[ext]
guard let destination = dest else {
throw DiskSwiftcProductsGeneratorError.unknownSwiftmoduleFile
}
do {
// Move cached .swiftmodule to the expected location
try diskCopier.copy(file: url, destination: destination)
} catch {
if case .required = SwiftmoduleFileExtension.SwiftmoduleExtensions[ext] {
throw error
} else {
infoLog("Optional .\(ext) file not found in the artifact at: \(destination.path)")
}
}
}
// Build parent dir of the .swiftmodule file that contains a module
return modulePathOutput.deletingLastPathComponent()
}
}
@@ -41,6 +41,7 @@ class Postbuild {
private let dSYMOrganizer: DSYMOrganizer
private let modeController: CacheModeController
private let metaReader: MetaReader
private let metaWriter: MetaWriter
private let creatorPlugins: [ArtifactCreatorPlugin]
private let consumerPlugins: [ArtifactConsumerPostbuildPlugin]
@@ -58,6 +59,7 @@ class Postbuild {
dSYMOrganizer: DSYMOrganizer,
modeController: CacheModeController,
metaReader: MetaReader,
metaWriter: MetaWriter,
creatorPlugins: [ArtifactCreatorPlugin],
consumerPlugins: [ArtifactConsumerPostbuildPlugin]
) {
@@ -74,6 +76,7 @@ class Postbuild {
self.dSYMOrganizer = dSYMOrganizer
self.modeController = modeController
self.metaReader = metaReader
self.metaWriter = metaWriter
self.creatorPlugins = creatorPlugins
self.consumerPlugins = consumerPlugins
}
@@ -125,6 +128,22 @@ class Postbuild {
try generateFingerprintOverrides(contextSpecificFingerprint: fingerprint.contextSpecific)
}
/// Uploads only a meta to the remote server - useful when the file artifact (.zip) already exists on a remote
/// server and only a meta for a current commit sha has to be uploaded
public func performMetaUpload(meta: MainArtifactMeta, for commit: String) throws {
// Reset plugins keys as these are unique to each
var meta = meta
meta.pluginsKeys = [:]
meta = try creatorPlugins.reduce(meta) { prevMeta, plugin in
var meta = prevMeta
// add extra keys from the plugin. A plugin overrides previously defined keys in case of duplication
meta.pluginsKeys = try meta.pluginsKeys.merging(plugin.extraMetaKeys(prevMeta), uniquingKeysWith: { $1 })
return meta
}
let metaPath = try metaWriter.write(meta, locationDir: context.targetTempDir)
try networkClient.uploadSynchronously(metaPath, as: .meta(commit: commit))
}
/// Builds an artifact package and uploads it to the remote server
public func performBuildUpload(for commit: String) throws {
let dependencies = try generateDependencies()
@@ -32,6 +32,8 @@ enum MachOType: String, Codable {
enum PostbuildContextError: Error {
/// URL address is not a valid URL
case invalidAddress(String)
/// ARCHS env does not contain any architecture to build
case missingArchitecture
}
public struct PostbuildContext {
@@ -64,6 +66,9 @@ public struct PostbuildContext {
var machOType: MachOType
var wasDsymGenerated: Bool
var dSYMPath: URL
// building architecture. Used to find all dependencies from *.d files
// Warning: if two architectures are built (e.g. for disabled "Build Archive
// Architecture Only"), a first architecture one is picked
let arch: String
let builtProductsDir: URL
/// Location to the product bundle. Can be nil for libraries
@@ -71,6 +76,9 @@ public struct PostbuildContext {
let derivedSourcesDir: URL
/// List of all targets to downloaded from the thinning aggregation target
var thinnedTargets: [String]
/// Action type: build, indexbuild etc.
var action: BuildActionType
let modeMarkerPath: String
}
extension PostbuildContext {
@@ -79,8 +87,13 @@ extension PostbuildContext {
let targetNameValue: String = try env.readEnv(key: "TARGET_NAME")
targetName = targetNameValue
targetTempDir = try env.readEnv(key: "TARGET_TEMP_DIR")
arch = try env.readEnv(key: "PLATFORM_PREFERRED_ARCH")
compilationTempDir = try env.readEnv(key: "OBJECT_FILE_DIR_normal").appendingPathComponent(arch)
let archs: [String] = try env.readEnv(key: "ARCHS").split(separator: " ").map(String.init)
guard let firstArch = archs.first, !firstArch.isEmpty else {
throw PostbuildContextError.missingArchitecture
}
arch = firstArch
let variant: String = try env.readEnv(key: "CURRENT_VARIANT")
compilationTempDir = try env.readEnv(key: "OBJECT_FILE_DIR_\(variant)").appendingPathComponent(arch)
configuration = try env.readEnv(key: "CONFIGURATION")
platform = try env.readEnv(key: "PLATFORM_NAME")
xcodeBuildNumber = try env.readEnv(key: "XCODE_PRODUCT_BUILD_VERSION")
@@ -112,5 +125,7 @@ extension PostbuildContext {
derivedSourcesDir = try env.readEnv(key: "DERIVED_SOURCES_DIR")
let thinFocusedTargetsString: String = env.readEnv(key: "SPT_XCREMOTE_CACHE_THINNED_TARGETS") ?? ""
thinnedTargets = thinFocusedTargetsString.split(separator: ",").map(String.init)
action = (try? BuildActionType(rawValue: env.readEnv(key: "ACTION"))) ?? .unknown
modeMarkerPath = config.modeMarkerPath
}
}
@@ -31,18 +31,19 @@ public class XCPostbuild {
let fileManager = FileManager.default
let config: XCRemoteCacheConfig
let context: PostbuildContext
let statsLogger: StatsLogger
let cacheHitLogger: CacheHitLogger
do {
config = try XCRemoteCacheConfigReader(env: env, fileManager: fileManager).readConfiguration()
context = try PostbuildContext(config, env: env)
let counterFactory: FileStatsCoordinator.CountersFactory = { file, count in
ExclusiveFileCounter(ExclusiveFile(file, mode: .override), countersCount: count)
}
statsLogger = try FileStatsLogger(
let statsLogger = try FileStatsLogger(
statsLocation: context.statsLocation,
counterFactory: counterFactory,
fileManager: fileManager
)
cacheHitLogger = ActionSpecificCacheHitLogger(action: context.action, statsLogger: statsLogger)
} catch {
exit(1, "FATAL: Postbuild initialization failed with error: \(error)")
}
@@ -65,9 +66,10 @@ public class XCPostbuild {
// Initialize dependencies
let primaryGitBranch = GitBranch(repoLocation: config.primaryRepo, branch: config.primaryBranch)
let gitClient = GitClientImpl(repoRoot: config.repoRoot, primary: primaryGitBranch, shell: shellGetStdout)
let pathRemapper = try StringDependenciesRemapper.buildFromEnvs(
keys: DependenciesMapping.rewrittenEnvs,
envs: env
let pathRemapper = try StringDependenciesRemapperFactory().build(
orderKeys: DependenciesMapping.rewrittenEnvs,
envs: env,
customMappings: config.outOfBandMappings
)
let envFingerprint = try EnvironmentFingerprintGenerator(
configuration: config,
@@ -84,6 +86,7 @@ public class XCPostbuild {
algorithm: MD5Algorithm()
)
let organizer = ZipArtifactOrganizer(targetTempDir: context.targetTempDir, fileManager: fileManager)
let metaWriter = JsonMetaWriter(fileWriter: fileManager, pretty: config.prettifyMetaFiles)
let artifactCreator = BuildArtifactCreator(
buildDir: context.productsDir,
tempDir: context.targetTempDir,
@@ -91,6 +94,7 @@ public class XCPostbuild {
moduleName: context.moduleName,
modulesFolderPath: context.modulesFolderPath,
dSYMPath: context.dSYMPath,
metaWriter: metaWriter,
fileManager: fileManager
)
let dirAccessor = DirAccessorComposer(
@@ -204,9 +208,10 @@ public class XCPostbuild {
worker: DispatchGroupParallelizationWorker(qos: .userInitiated)
)
consumerPlugins.append(thinningPlugin)
case .producer:
case .producer, .producerFast:
let thinningPlugin = ThinningCreatorPlugin(
targetTempDir: context.targetTempDir,
modeMarkerPath: context.modeMarkerPath,
dirScanner: fileManager
)
creatorPlugins.append(thinningPlugin)
@@ -228,6 +233,7 @@ public class XCPostbuild {
dSYMOrganizer: dSYMOrganizer,
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: creatorPlugins,
consumerPlugins: consumerPlugins
)
@@ -242,11 +248,22 @@ public class XCPostbuild {
try postbuildAction.deleteFingerprintOverrides()
}
// Trigger uploading the artifact
if context.mode == .producer {
switch (context.mode, try modeController.isEnabled(), context.remoteCommit) {
case (.producerFast, true, .available(commit: let commitToReuse)):
// Upload only updated meta. Artifact zip is already on a remote server
let referenceCommit = try config.publishingSha ?? gitClient.getCurrentSha()
let metaData = try remoteNetworkClient.fetch(.meta(commit: commitToReuse))
let meta = try metaReader.read(data: metaData)
try postbuildAction.performMetaUpload(meta: meta, for: referenceCommit)
case (.producer, _, _), (.producerFast, _, _):
// Generate artifacts and upload to the remote server for a reference sha
let referenceCommit = try config.publishingSha ?? gitClient.getCurrentSha()
try postbuildAction.performBuildUpload(for: referenceCommit)
default:
// Consumer does not upload anything
break
}
let executableURL = context.productsDir.appendingPathComponent(context.executablePath)
@@ -255,14 +272,13 @@ public class XCPostbuild {
// Populate stats event for a final RC state
// Doing it in a postmerge, as xcswiftc (and xccc) has a right to disable RC
if try modeController.isEnabled() {
try statsLogger.log(.targetCacheHit)
try cacheHitLogger.logHit()
printToUser("Cached build for \(context.targetName) target")
} else {
try postbuildAction.performBuildCleanup()
try statsLogger.log(.targetCacheMiss)
// Producer mode doesn't use cached artifacts so modeController is not enabled. If producer
// reaches this point, there were no issues with publishing
let actionName = context.mode == .producer ? "Published" : "Disabled"
try cacheHitLogger.logMiss()
// If producers reach this point, there were no issues with publishing
let actionName = context.mode == .consumer ? "Disabled" : "Published"
printToUser("\(actionName) remote cache for \(context.targetName)")
}
} catch PluginError.unrecoverableError(let error) {
@@ -277,7 +293,7 @@ public class XCPostbuild {
do {
try modeController.disable()
// TODO: consider tracking errors in stats
try statsLogger.log(.targetCacheMiss)
try cacheHitLogger.logMiss()
printToUser("Disabled remote cache for \(context.targetName)")
} catch {
exit(1, "FATAL: Postbuild finishing failed with error: \(error)")
@@ -55,6 +55,7 @@ class Prebuild {
self.artifactConsumerPrebuildPlugins = artifactConsumerPrebuildPlugins
}
// swiftlint:disable:next function_body_length
public func perform() throws -> PrebuildResult {
guard case .available(let commit) = context.remoteCommit else {
return .incompatible
@@ -115,9 +115,10 @@ public class XCPrebuild {
)
let client: NetworkClient = config.disableHttpCache ? networkClient : cacheNetworkClient
let remoteNetworkClient = RemoteNetworkClientImpl(client, urlBuilder)
let pathRemapper = try StringDependenciesRemapper.buildFromEnvs(
keys: DependenciesMapping.rewrittenEnvs,
envs: env
let pathRemapper = try StringDependenciesRemapperFactory().build(
orderKeys: DependenciesMapping.rewrittenEnvs,
envs: env,
customMappings: config.outOfBandMappings
)
let filesFingerprintGenerator = FingerprintAccumulatorImpl(
algorithm: MD5Algorithm(),
@@ -129,7 +130,10 @@ public class XCPrebuild {
algorithm: MD5Algorithm()
)
let organizer = ZipArtifactOrganizer(targetTempDir: context.targetTempDir, fileManager: fileManager)
let compilationHistoryOrganizer = CompilationHistoryFileOrganizer(context.compilationHistoryFile, fileManager: fileManager)
let compilationHistoryOrganizer = CompilationHistoryFileOrganizer(
context.compilationHistoryFile,
fileManager: fileManager
)
let metaReader = JsonMetaReader(fileAccessor: fileManager)
var consumerPlugins: [ArtifactConsumerPrebuildPlugin] = []
@@ -72,13 +72,14 @@ class TemplateBasedCCWrapperBuilder: CCWrapperBuilder {
)
infoLog("ClangWrapperBuilder compiles file at \(compilationFile).")
// -O3: optimize for faster execution
let args = [clangCommand, "-O3", compilationFile.path, "-o", destination.path]
let args = [clangCommand, "-arch", "arm64", "-arch", "x86_64", "-O3", compilationFile.path, "-o", destination.path]
let compilationOutput = try shell("xcrun", args, URL(fileURLWithPath: "").path, nil)
infoLog("Clang compilation output: \(compilationOutput)")
}
/// Generates source of the cc wrapper
// swiftlint:disable line_length
// swiftlint:disable:next function_body_length
private func buildWrapperSource(clangCommand: String, markerFilename: String, commitSha: String) -> String {
return """
@@ -516,5 +517,5 @@ class TemplateBasedCCWrapperBuilder: CCWrapperBuilder {
#pragma GCC diagnostic pop
}
"""
} // swiftlint:disable:next file_length
}
} // swiftlint:disable:next file_length line_length
} // swiftlint:enable line_length
@@ -62,6 +62,7 @@ class XcodeProjBuildSettingsIntegrateAppender: BuildSettingsIntegrateAppender {
result["OTHER_CFLAGS"] = clangFlags.settingValue
result["XCRC_FAKE_SRCROOT"] = "/\(String(repeating: "x", count: 10))"
result["XCRC_PLATFORM_PREFERRED_ARCH"] = "$(LINK_FILE_LIST_$(CURRENT_VARIANT)_$(PLATFORM_PREFERRED_ARCH):dir:standardizepath:file:default=arm64)"
return result
}
}
@@ -56,7 +56,7 @@ class FileLLDBInitPatcher: LLDBInitPatcher {
}
private func findIndices(in collection: [String], value: String) -> [Int] {
collection.enumerated().reduce([]) { (result, line) -> [Int] in
collection.enumerated().reduce([]) { result, line -> [Int] in
if line.element == Self.preambleString {
return result + [line.offset]
}
@@ -75,7 +75,7 @@ class FileLLDBInitPatcher: LLDBInitPatcher {
var contentLines = originalContentLines
let preambleIndices = findIndices(in: contentLines, value: Self.preambleString)
if preambleIndices.count > 0 {
if !preambleIndices.isEmpty {
let firstLLDBCommandIndex = preambleIndices[0] + 1
if firstLLDBCommandIndex >= contentLines.count {
// corrupted file, append the script line at the bottom
@@ -64,6 +64,7 @@ public class XCIntegrate {
self.output = output
}
// swiftlint:disable:next function_body_length
public func main() {
do {
let env = ProcessInfo.processInfo.environment
@@ -114,7 +115,7 @@ public class XCIntegrate {
let integrator = XcodeProjIntegrate(
project: context.projectPath,
mode:context.mode,
mode: context.mode,
binaries: context.binaries,
configurationIncludeOracle: configurationOracle,
targetIncludeOracle: targetOracle,
@@ -100,11 +100,11 @@ struct XcodeProjIntegrate: Integrate {
outputPaths: [
"""
$(TARGET_BUILD_DIR)/$(MODULES_FOLDER_PATH)/$(PRODUCT_MODULE_NAME).swiftmodule/\
$(PLATFORM_PREFERRED_ARCH).swiftmodule.md5
$(XCRC_PLATFORM_PREFERRED_ARCH).swiftmodule.md5
""",
"""
$(TARGET_BUILD_DIR)/$(MODULES_FOLDER_PATH)/$(PRODUCT_MODULE_NAME).swiftmodule/\
$(PLATFORM_PREFERRED_ARCH)-$(LLVM_TARGET_TRIPLE_VENDOR)-$(SWIFT_PLATFORM_TARGET_PREFIX)\
$(XCRC_PLATFORM_PREFERRED_ARCH)-$(LLVM_TARGET_TRIPLE_VENDOR)-$(SWIFT_PLATFORM_TARGET_PREFIX)\
$(LLVM_TARGET_TRIPLE_SUFFIX).swiftmodule.md5
""",
],
@@ -114,7 +114,9 @@ struct XcodeProjIntegrate: Integrate {
markPhase = PBXShellScriptBuildPhase(
name: "\(Self.BuildStepPrefix)RemoteCache_mark",
inputPaths: [binaries.prepare.path],
shellScript: "\"$SCRIPT_INPUT_FILE_0\" mark --configuration \"$CONFIGURATION\" --platform \"$PLATFORM_NAME\""
shellScript:
"\"$SCRIPT_INPUT_FILE_0\" mark " +
"--configuration \"$CONFIGURATION\" --platform \"$PLATFORM_NAME\""
)
}
@@ -129,6 +131,7 @@ struct XcodeProjIntegrate: Integrate {
try encodedYAML.write(to: configOverrideLocation, atomically: false, encoding: .utf8)
}
// swiftlint:disable:next function_body_length
func run() throws {
let outputFile = output ?? projectURL
let projectRoot = projectURL.deletingLastPathComponent()
@@ -100,7 +100,7 @@ struct XcodeSettingsCFlags: XcodeSettingsFlags {
case (.some(let existing), _):
var flagsComponents: [String] = existing.split(separator: " ").map(String.init)
// remove (if exists)
let existingFlagIndex = flagsComponents.firstIndex { (component) -> Bool in
let existingFlagIndex = flagsComponents.firstIndex { component -> Bool in
component.hasPrefix("\(Self.prefix)\(key)=")
}
if let index = existingFlagIndex {
@@ -38,6 +38,7 @@ public class XCPrepareMark {
self.commit = commit
}
// swiftlint:disable:next function_body_length
public func main() {
let env = ProcessInfo.processInfo.environment
let fileManager = FileManager.default
@@ -120,9 +120,9 @@ class Swiftc: SwiftcProtocol {
let prebuildDiscoveryURL = context.tempDir.appendingPathComponent(context.prebuildDependenciesPath)
let prebuildDiscoverWriter = dependenciesWriterFactory(prebuildDiscoveryURL, fileManager)
try prebuildDiscoverWriter.write(skipForSha: remoteCommit)
case .consumer, .producer:
// Never skips prebuild phase and fallbacks to the swiftc compilation for:
// 1) Not enabled remote cache or 2) producer
case .consumer, .producer, .producerFast:
// Never skip prebuild phase and fallback to the swiftc compilation for:
// 1) Not enabled remote cache, 2) producer(s)
break
}
return .forceFallback
@@ -166,9 +166,11 @@ class Swiftc: SwiftcProtocol {
// Save individual .d and touch .o for each .swift file
for compilation in allCompilations.files {
// Touching .o is required to invalidate already existing .a or linked library
let touch = touchFactory(compilation.object, fileManager)
try touch.touch()
if let object = compilation.object {
// Touching .o is required to invalidate already existing .a or linked library
let touch = touchFactory(object, fileManager)
try touch.touch()
}
if let individualDeps = compilation.dependencies {
// swiftc product should be invalidated if any of dependencies file has changed
try cachedDependenciesWriterFactory.generate(output: individualDeps)
@@ -24,6 +24,8 @@ public struct SwiftcContext {
case producer
/// Commit sha of the commit to use during remote cache
case consumer(commit: RemoteCommitInfo)
/// Remote artifact exists and can be optimistically used in place of a local compilation
case producerFast
}
let objcHeaderOutput: URL
@@ -74,6 +76,14 @@ public struct SwiftcContext {
mode = .consumer(commit: remoteCommit)
case .producer:
mode = .producer
case .producerFast:
let remoteCommit = RemoteCommitInfo(try? String(contentsOf: remoteCommitLocation).trim())
switch remoteCommit {
case .unavailable:
mode = .producer
case .available:
mode = .producerFast
}
}
invocationHistoryFile = URL(fileURLWithPath: config.compilationHistoryFile, relativeTo: tempDir)
}
@@ -52,7 +52,8 @@ struct SwiftFileCompilationInfo: Encodable, Equatable {
let file: URL
// not present for WMO builds
let dependencies: URL?
let object: URL
// not present for 'indexbuild' builds
let object: URL?
// not present for WMO builds
let swiftDependencies: URL?
}
@@ -129,14 +130,14 @@ extension SwiftFileCompilationInfo {
}
file = URL(fileURLWithPath: name)
dependencies = dict.readURL(key: "dependencies")
object = try dict.readURL(key: "object")
object = dict.readURL(key: "object")
swiftDependencies = dict.readURL(key: "swift-dependencies")
}
func dump() -> [String: String] {
return [
"dependencies": dependencies?.path,
"object": object.path,
"object": object?.path,
"swift-dependencies": swiftDependencies?.path,
].compactMapValues { $0 }
}
@@ -116,6 +116,12 @@ class SwiftcOrchestrator {
}
case .consumer:
fallbackToDefault(command: swiftcCommand)
case .producerFast:
let compileStepResult = try swiftc.mockCompilation()
if case .forceFallback = compileStepResult {
// cannot reuse cached artifact. Build it locally and upload to the server just as for the producer
fallthrough
}
case .producer:
var swiftcArgs = ProcessInfo().arguments
swiftcArgs = try producerFallbackCommandProcessors.reduce(swiftcArgs) { args, processor in
@@ -101,7 +101,10 @@ public class XCSwiftc {
objcHeaderOutput: context.objcHeaderOutput,
diskCopier: HardLinkDiskCopier(fileManager: fileManager)
)
let allInvocationsStorage = ExistingFileStorage(storageFile: context.invocationHistoryFile, command: swiftcCommand)
let allInvocationsStorage = ExistingFileStorage(
storageFile: context.invocationHistoryFile,
command: swiftcCommand
)
// When fallbacking to local compilation do not call historical `swiftc` invocations
// The current fallback invocation already compiles all files in a target
let invocationStorage = FilteredInvocationStorage(
+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,19 @@ public struct XCRemoteCacheConfig: Encodable {
var AWSRegion: String = ""
/// Service for AWS V4 Signature (e.g. `storage`)
var AWSService: String = ""
/// A dictionary of files path remapping that should be applied to make it absolute path agnostic on a list of dependencies.
/// Useful if a project refers files out of repo root, either compilation files or precompiled dependencies.
/// Keys represent generic replacement and values are substrings that should be replaced.
/// Example: for mapping `["COOL_LIBRARY": "/CoolLibrary"]`
/// `/CoolLibrary/main.swift`will be represented as `$(COOL_LIBRARY)/main.swift`).
/// Warning: remapping order is not-deterministic so avoid remappings with multiple matchings.
var outOfBandMappings: [String: String] = [:]
}
extension XCRemoteCacheConfig {
/// Merges existing config with the other config and returns a final result
/// `other` scheme overrides existing configuration
// swiftlint:disable:next function_body_length
func merged(with scheme: ConfigFileScheme) -> XCRemoteCacheConfig {
var merge = self
merge.mode = scheme.mode ?? mode
@@ -163,10 +174,12 @@ extension XCRemoteCacheConfig {
scheme.productFilesExtensionsWithContentOverride ?? productFilesExtensionsWithContentOverride
merge.thinningEnabled = scheme.thinningEnabled ?? thinningEnabled
merge.thinningTargetModuleName = scheme.thinningTargetModuleName ?? thinningTargetModuleName
merge.prettifyMetaFiles = scheme.prettifyMetaFiles ?? prettifyMetaFiles
merge.AWSAccessKey = scheme.AWSAccessKey ?? AWSAccessKey
merge.AWSSecretKey = scheme.AWSSecretKey ?? AWSSecretKey
merge.AWSRegion = scheme.AWSRegion ?? AWSRegion
merge.AWSService = scheme.AWSService ?? AWSService
merge.outOfBandMappings = scheme.outOfBandMappings ?? outOfBandMappings
return merge
}
@@ -221,10 +234,12 @@ struct ConfigFileScheme: Decodable {
let productFilesExtensionsWithContentOverride: [String]?
let thinningEnabled: Bool?
let thinningTargetModuleName: String?
let prettifyMetaFiles: Bool?
let AWSSecretKey: String?
let AWSAccessKey: String?
let AWSRegion: String?
let AWSService: String?
let outOfBandMappings: [String: String]?
// Yams library doesn't support encoding strategy, see https://github.com/jpsim/Yams/issues/84
enum CodingKeys: String, CodingKey {
@@ -262,10 +277,12 @@ struct ConfigFileScheme: Decodable {
case productFilesExtensionsWithContentOverride = "product_files_extensions_with_content_override"
case thinningEnabled = "thinning_enabled"
case thinningTargetModuleName = "thinning_target_module_name"
case prettifyMetaFiles = "prettify_meta_files"
case AWSSecretKey = "aws_secret_key"
case AWSAccessKey = "aws_access_key"
case AWSRegion = "aws_region"
case AWSService = "aws_service"
case outOfBandMappings = "out_of_band_mappings"
}
}
@@ -35,7 +35,7 @@ class DependenciesRemapperComposite: DependenciesRemapper {
}
func replace(genericPaths: [String]) -> [String] {
remappers.reduce(genericPaths) { prev, mapper in
remappers.reversed().reduce(genericPaths) { prev, mapper in
mapper.replace(genericPaths: prev)
}
}
@@ -61,7 +61,7 @@ final class StringDependenciesRemapper: DependenciesRemapper {
func replace(genericPaths: [String]) -> [String] {
return genericPaths.map { path in
let localPath = mappings.reduce(path) { prevPath, mapping in
let localPath = mappings.reversed().reduce(path) { prevPath, mapping in
prevPath.replacingOccurrences(of: mapping.generic, with: mapping.local)
}
return localPath
@@ -77,14 +77,3 @@ final class StringDependenciesRemapper: DependenciesRemapper {
}
}
}
extension StringDependenciesRemapper {
static func buildFromEnvs(keys: [String], envs: [String: String]) throws -> Self {
let mappings: [Mapping] = try keys.map { key in
let localValue: String = try envs.readEnv(key: key)
return Mapping(generic: "$(\(key))", local: localValue)
}
return Self(mappings: mappings)
}
}
@@ -0,0 +1,43 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
enum StringDependenciesRemapperFactoryError: Error {
/// Remapping keys are duplicated and can lead to undetermined results
case mappingKeyDuplication
}
class StringDependenciesRemapperFactory {
func build(
orderKeys: [String],
envs: [String: String],
customMappings: [String: String]
) throws -> StringDependenciesRemapper {
let mappingMap = try envs.merging(customMappings) { envValue, outOfBandMapping in
throw StringDependenciesRemapperFactoryError.mappingKeyDuplication
}
let mappingOrderKeys = orderKeys + customMappings.keys
let mappings: [StringDependenciesRemapper.Mapping] = try mappingOrderKeys.map { key in
let localValue: String = try mappingMap.readEnv(key: key)
return StringDependenciesRemapper.Mapping(generic: "$(\(key))", local: localValue)
}
return StringDependenciesRemapper(mappings: mappings)
}
}
@@ -19,10 +19,10 @@
/// Generates a fingerprint string of the environment (compilation context)
class EnvironmentFingerprintGenerator {
/// Default ENV variables constituing the environment fingerprint
/// Default ENV variables constituting the environment fingerprint
private static let defaultEnvFingerprintKeys = [
"GCC_PREPROCESSOR_DEFINITIONS",
"CLANG_PROFILE_DATA_DIRECTORY",
"CLANG_COVERAGE_MAPPING",
"TARGET_NAME",
"CONFIGURATION",
"PLATFORM_NAME",
@@ -31,6 +31,7 @@ class EnvironmentFingerprintGenerator {
"DYLIB_COMPATIBILITY_VERSION",
"DYLIB_CURRENT_VERSION",
"PRODUCT_MODULE_NAME",
"ARCHS"
]
private let version: String
private let customFingerprintEnvs: [String]
@@ -0,0 +1,45 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
protocol MetaWriter {
func write<T>(_ meta: T, locationDir : URL) throws -> URL where T : Meta
}
class JsonMetaWriter: MetaWriter {
private let metaEncoder: JSONEncoder
private let fileWriter: FileWriter
init(fileWriter: FileWriter, pretty: Bool) {
self.fileWriter = fileWriter
let encoder = JSONEncoder()
if pretty {
encoder.outputFormatting = .prettyPrinted
}
self.metaEncoder = encoder
}
func write<T>(_ meta: T, locationDir : URL) throws -> URL where T : Meta {
let metaURL = locationDir.appendingPathComponent(meta.fileKey).appendingPathExtension("json")
let metaData = try metaEncoder.encode(meta)
try fileWriter.write(toPath: metaURL.path, contents: metaData)
return metaURL
}
}
@@ -35,9 +35,22 @@ struct AWSV4Signature {
request.setValue((request.httpBody ?? Data()).sha256(), forHTTPHeaderField: "x-amz-content-sha256")
let canonicalRequest = CanonicalRequest(request: request)
let stringToSign = StringToSign(region: region, service: service, canonicalRequestHash: canonicalRequest.hash, date: date)
let awsV4SigningKey = AWSV4SigningKey(secretAccessKey: secretKey, region: region, service: service, date: date)
let signature = HMAC.calcHMAC(keyArray: awsV4SigningKey.value, value: stringToSign.value).map { String(format: "%02hhx", $0) }.joined()
let stringToSign = StringToSign(
region: region,
service: service,
canonicalRequestHash: canonicalRequest.hash,
date: date
)
let awsV4SigningKey = AWSV4SigningKey(
secretAccessKey: secretKey,
region: region,
service: service,
date: date
)
let signature = HMAC.calcHMAC(
keyArray: awsV4SigningKey.value,
value: stringToSign.value
).map { String(format: "%02hhx", $0) }.joined()
let authValue =
"AWS4-HMAC-SHA256 " +
@@ -52,7 +52,14 @@ struct HMAC {
private static func calcHMAC(keyUnsafeBytes: UnsafeRawBufferPointer, value: String, out: UnsafeMutableRawPointer!) {
value.data(using: .utf8)!.withUnsafeBytes { value in
CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA256), keyUnsafeBytes.baseAddress, Int(keyUnsafeBytes.count), value.baseAddress, Int(value.count), out)
CCHmac(
CCHmacAlgorithm(kCCHmacAlgSHA256),
keyUnsafeBytes.baseAddress,
Int(keyUnsafeBytes.count),
value.baseAddress,
Int(value.count),
out
)
}
}
}
@@ -44,7 +44,7 @@ class RemoteNetworkClientAbstractFactory {
return RemoteNetworkClientImpl(networkClient, downloadURLBuilder)
}
switch mode {
case .producer:
case .producer, .producerFast:
let upstreamBuilders = try upstreamStreamURL.map(urlBuilderFactory)
return ReplicatedRemotesNetworkClient(
networkClient,
@@ -0,0 +1,60 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/// Logger to log events to the statistics database
protocol CacheHitLogger {
/// Increments a counter related to the cache hit
func logHit() throws
/// Increments a counter related to the cache miss
func logMiss() throws
}
/// Logs target hit or miss, based on an action of a build
class ActionSpecificCacheHitLogger: CacheHitLogger {
private let statsLogger: StatsLogger
private let hitCounter: XCRemoteCacheStatistics.Counter?
private let missCounter: XCRemoteCacheStatistics.Counter?
init(action: BuildActionType, statsLogger: StatsLogger) {
self.statsLogger = statsLogger
switch action {
case .index:
hitCounter = .indexingTargetHitCount
missCounter = .indexingTargetMissCount
case .build:
hitCounter = .targetCacheHit
missCounter = .targetCacheMiss
case .unknown:
hitCounter = nil
missCounter = nil
}
}
func logHit() throws {
if let hitCounter = hitCounter {
try statsLogger.log(hitCounter)
}
}
func logMiss() throws {
if let missCounter = missCounter {
try statsLogger.log(missCounter)
}
}
}
@@ -75,7 +75,9 @@ class FileStatsCoordinator: FileStatsLogger, StatsCoordinator {
return try XCRemoteCacheStatistics(
hitCount: counters[XCRemoteCacheStatistics.Counter.targetCacheHit.rawValue],
missCount: counters[XCRemoteCacheStatistics.Counter.targetCacheMiss.rawValue],
localCacheBytes: countLocalCacheSize()
localCacheBytes: countLocalCacheSize(),
indexingHitCount: counters[XCRemoteCacheStatistics.Counter.indexingTargetHitCount.rawValue],
indexingMissCount: counters[XCRemoteCacheStatistics.Counter.indexingTargetMissCount.rawValue]
)
}
@@ -18,45 +18,63 @@
// under the License.
/// Representation of all statistics related to XCRemoteCache
struct XCRemoteCacheStatistics: Encodable {
struct XCRemoteCacheStatistics: Encodable, Equatable {
// Yams library doesn't support encoding strategy, see https://github.com/jpsim/Yams/issues/84
enum CodingKeys: String, CodingKey {
case hitCount = "hit_count"
case missCount = "miss_count"
case localCacheBytes = "local_cache_bytes"
case indexingHitCount = "indexing_hit_count"
case indexingMissCount = "indexing_miss_count"
}
/// Counters fields position: rawValue defines the index of the counter
/// that backs up the statistic metric
enum Counter: Int, CaseIterable {
// Warning! Do not add between existing fieds, only add new one at the bottom
// Warning! Do not add between existing fields, only add new one at the bottom
// rawValue represents the counter position
// e.g. '0' means that 'hitCount' metric will
// be stored in a first counter (in a file)
case targetCacheHit = 0
case targetCacheMiss
case indexingTargetHitCount
case indexingTargetMissCount
}
/// Number of cache hits
let hitCount: Int
/// Number of cache mises
/// Number of cache misses
let missCount: Int
/// Size of a local cache in bytes
let localCacheBytes: Int
/// Number of cache hits for 'indexbuild' actions
let indexingHitCount: Int
/// Number of cache misses for 'indexbuild' actions
let indexingMissCount: Int
static let initial = XCRemoteCacheStatistics(hitCount: 0, missCount: 0, localCacheBytes: 0)
static let initial = XCRemoteCacheStatistics(
hitCount: 0,
missCount: 0,
localCacheBytes: 0,
indexingHitCount: 0,
indexingMissCount: 0
)
}
extension XCRemoteCacheStatistics {
func with(
hitCount: Int? = nil,
missCount: Int? = nil,
localCacheBytes: Int? = nil
localCacheBytes: Int? = nil,
indexingHitCount: Int? = nil,
indexingMissCount: Int? = nil
) -> XCRemoteCacheStatistics {
return XCRemoteCacheStatistics(
hitCount: hitCount ?? self.hitCount,
missCount: missCount ?? self.missCount,
localCacheBytes: localCacheBytes ?? self.localCacheBytes
localCacheBytes: localCacheBytes ?? self.localCacheBytes,
indexingHitCount: indexingHitCount ?? self.indexingHitCount,
indexingMissCount: indexingMissCount ?? self.indexingMissCount
)
}
}
@@ -0,0 +1,30 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Represents a type of a build invoked by the Xcode
enum BuildActionType: String, Codable {
/// Standard build
case build
/// An extra build, exclusive for indexing (Introduced in Xcode 13)
case index = "indexbuild"
/// Unknown type, probably incompatible Xcode version used
case unknown
}
@@ -31,6 +31,7 @@ class ArtifactSwiftProductsBuilderImplTests: FileXCTestCase {
private var builder: ArtifactSwiftProductsBuilderImpl!
override func setUpWithError() throws {
try super.setUpWithError()
let rootDir = try prepareTempDir()
moduleDir = rootDir.appendingPathComponent("Products")
swiftmoduleFile = moduleDir.appendingPathComponent("MyModule.swiftmodule")
@@ -47,24 +48,41 @@ class ArtifactSwiftProductsBuilderImplTests: FileXCTestCase {
func testIncludesRequiredSwiftmoduleFiles() throws {
try fileManager.spt_createFile(swiftmoduleFile, content: "swiftmodule")
try fileManager.spt_createFile(swiftmoduleDocFile, content: "swiftdoc")
let builderSwiftmoduleDir = builder.buildingArtifactSwiftModulesLocation().appendingPathComponent("arm64")
let expectedBuildedSwiftmoduleFile = builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftmodule")
let expectedBuildedSwiftmoduledocFile = builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftdoc")
let builderSwiftmoduleDir =
builder
.buildingArtifactSwiftModulesLocation()
.appendingPathComponent("arm64")
let expectedBuildedSwiftmoduleFile =
builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftmodule")
let expectedBuildedSwiftmoduledocFile =
builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftdoc")
try builder.includeModuleDefinitionsToTheArtifact(arch: "arm64", moduleURL: swiftmoduleFile)
XCTAssertEqual(fileManager.contents(atPath: expectedBuildedSwiftmoduleFile.path), "swiftmodule".data(using: .utf8))
XCTAssertEqual(fileManager.contents(atPath: expectedBuildedSwiftmoduledocFile.path), "swiftdoc".data(using: .utf8))
XCTAssertEqual(
fileManager.contents(atPath: expectedBuildedSwiftmoduleFile.path),
"swiftmodule".data(using: .utf8)
)
XCTAssertEqual(
fileManager.contents(atPath: expectedBuildedSwiftmoduledocFile.path),
"swiftdoc".data(using: .utf8)
)
}
func testIncludesAllSwiftmoduleFiles() throws {
try fileManager.spt_createEmptyFile(swiftmoduleFile)
try fileManager.spt_createEmptyFile(swiftmoduleDocFile)
try fileManager.spt_createEmptyFile(swiftmoduleSourceInfoFile)
let builderSwiftmoduleDir = builder.buildingArtifactSwiftModulesLocation().appendingPathComponent("arm64")
let expectedBuildedSwiftmoduleFile = builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftmodule")
let expectedBuildedSwiftmoduledocFile = builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftdoc")
let expectedBuildedSwiftSourceInfoFile = builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftsourceinfo")
let builderSwiftmoduleDir =
builder
.buildingArtifactSwiftModulesLocation()
.appendingPathComponent("arm64")
let expectedBuildedSwiftmoduleFile =
builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftmodule")
let expectedBuildedSwiftmoduledocFile =
builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftdoc")
let expectedBuildedSwiftSourceInfoFile =
builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftsourceinfo")
try builder.includeModuleDefinitionsToTheArtifact(arch: "arm64", moduleURL: swiftmoduleFile)
@@ -74,6 +92,11 @@ class ArtifactSwiftProductsBuilderImplTests: FileXCTestCase {
}
func testFailsIncludingWhenMissingRequiredSwiftmoduleFiles() throws {
XCTAssertThrowsError(try builder.includeModuleDefinitionsToTheArtifact(arch: "arm64", moduleURL: swiftmoduleFile))
XCTAssertThrowsError(
try builder.includeModuleDefinitionsToTheArtifact(
arch: "arm64",
moduleURL: swiftmoduleFile
)
)
}
}
@@ -63,6 +63,7 @@ class BuildArtifactCreatorTests: FileXCTestCase {
moduleName: "Target",
modulesFolderPath: "",
dSYMPath: dSYM,
metaWriter: JsonMetaWriter(fileWriter: fileManager, pretty: false),
fileManager: fileManager
)
}
@@ -35,7 +35,11 @@ class ZipArtifactCreatorTests: FileXCTestCase {
override func setUpWithError() throws {
try super.setUpWithError()
workingDir = try prepareTempDir().appendingPathComponent("creator")
creator = ZipArtifactCreator(workingDir: workingDir, fileManager: fileManager)
creator = ZipArtifactCreator(
workingDir: workingDir,
metaWriter: JsonMetaWriter(fileWriter: fileManager, pretty: false),
fileManager: fileManager
)
}
func testCreatingArtifactGeneratesValidArtifactId() throws {
@@ -84,4 +84,18 @@ class DefaultSwiftProductsArchitecturesRecognizerTests: FileXCTestCase {
XCTAssertEqual(architectures, ["x86"])
}
func testRecognizesArchitectureFromFilesOnly() throws {
let swiftmodule = swiftmoduleDir.appendingPathComponent("x86.swiftmodule")
let swiftmoduleExtraDir = swiftmoduleDir.appendingPathComponent("Dir")
try fileManager.spt_createEmptyFile(swiftmodule)
try fileManager.spt_createEmptyDir(swiftmoduleExtraDir)
let architectures = try recognizer.recognizeArchitectures(
builtProductsDir: builtProductsDir,
moduleName: "MyModule"
)
XCTAssertEqual(architectures, ["x86"])
}
}
@@ -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"
])
}
}
@@ -0,0 +1,74 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
@testable import XCRemoteCache
import XCTest
class ThinningDiskSwiftcProductsGeneratorTests: FileXCTestCase {
func testLinksSwiftProductsToValidLocations() throws {
let workingDir = try prepareTempDir()
let moduleFile = try fileManager.spt_createFile(
workingDir.appendingPathComponent("MyModule.swiftmodule"),
content: "module"
)
let headerFile = try fileManager.spt_createFile(
workingDir.appendingPathComponent("MyModule-Swift.h"),
content: "header"
)
let docsFile = try fileManager.spt_createFile(
workingDir.appendingPathComponent("MyModule.swiftdoc"),
content: "docs"
)
let sourceInfoFile = try fileManager.spt_createFile(
workingDir.appendingPathComponent("MyModule.swiftsourceinfo"),
content: "sourceInfo"
)
let artifactSwiftModuleFiles: [SwiftmoduleFileExtension: URL] = [
.swiftmodule: moduleFile,
.swiftdoc: docsFile,
.swiftsourceinfo: sourceInfoFile,
]
let buildDir = workingDir.appendingPathComponent("build")
let headersDir = workingDir.appendingPathComponent("headers")
let destinationSwiftModuleDir = buildDir
.appendingPathComponent("MyModule.swiftmodule", isDirectory: true)
let objCHeader = headersDir
.appendingPathComponent("MyModule-Swift.h")
let destinationSwiftModule = destinationSwiftModuleDir
.appendingPathComponent("arm64.swiftmodule")
let expectedSwiftSourceInfoFile = destinationSwiftModuleDir
.appendingPathComponent("Project")
.appendingPathComponent("arm64.swiftsourceinfo")
let generator = ThinningDiskSwiftcProductsGenerator(
modulePathOutput: destinationSwiftModule,
objcHeaderOutput: objCHeader,
diskCopier: HardLinkDiskCopier(fileManager: .default)
)
let generatedModulePath = try generator.generateFrom(
artifactSwiftModuleFiles: artifactSwiftModuleFiles,
artifactSwiftModuleObjCFile: headerFile
)
XCTAssertEqual(generatedModulePath, destinationSwiftModuleDir)
XCTAssertEqual(fileManager.contents(atPath: expectedSwiftSourceInfoFile.path), "sourceInfo".data(using: .utf8))
XCTAssertEqual(fileManager.contents(atPath: objCHeader.path), "header".data(using: .utf8))
}
}
@@ -26,8 +26,8 @@ class PostbuildContextTests: FileXCTestCase {
private static let SampleEnvs = [
"TARGET_NAME": "TARGET_NAME",
"TARGET_TEMP_DIR": "TARGET_TEMP_DIR",
"PLATFORM_PREFERRED_ARCH": "PLATFORM_PREFERRED_ARCH",
"OBJECT_FILE_DIR_normal": "OBJECT_FILE_DIR_normal" ,
"ARCHS": "x86_64",
"OBJECT_FILE_DIR_normal": "/OBJECT_FILE_DIR_normal" ,
"CONFIGURATION": "CONFIGURATION",
"PLATFORM_NAME": "PLATFORM_NAME",
"XCODE_PRODUCT_BUILD_VERSION": "XCODE_PRODUCT_BUILD_VERSION",
@@ -42,6 +42,7 @@ class PostbuildContextTests: FileXCTestCase {
"DWARF_DSYM_FILE_NAME": "DWARF_DSYM_FILE_NAME",
"BUILT_PRODUCTS_DIR": "BUILT_PRODUCTS_DIR",
"DERIVED_SOURCES_DIR": "DERIVED_SOURCES_DIR",
"CURRENT_VARIANT": "normal"
]
override func setUpWithError() throws {
@@ -73,4 +74,59 @@ class PostbuildContextTests: FileXCTestCase {
XCTAssertEqual(context.remoteCommit, .unavailable)
}
func testFallbacksActionToUnknown() throws {
let context = try PostbuildContext(config, env: Self.SampleEnvs)
XCTAssertEqual(context.action, .unknown)
}
func testReadsActionFromEnv() throws {
var envs = Self.SampleEnvs
envs["ACTION"] = "indexbuild"
let context = try PostbuildContext(config, env: envs)
XCTAssertEqual(context.action, .index)
}
func testReadsSingleArch() throws {
var envs = Self.SampleEnvs
envs["ARCHS"] = "x86_64"
let context = try PostbuildContext(config, env: envs)
XCTAssertEqual(context.arch, "x86_64")
}
func testReadsFirstArch() throws {
var envs = Self.SampleEnvs
envs["ARCHS"] = "x86_64 arm64"
let context = try PostbuildContext(config, env: envs)
XCTAssertEqual(context.arch, "x86_64")
}
func testFailsForEmptyArch() throws {
var envs = Self.SampleEnvs
envs["ARCHS"] = ""
XCTAssertThrowsError(try PostbuildContext(config, env: envs))
}
func testFailsForMissingArch() throws {
var envs = Self.SampleEnvs
envs["ARCHS"] = nil
XCTAssertThrowsError(try PostbuildContext(config, env: envs))
}
func testFindTempDirForCustomVariant() throws {
var envs = Self.SampleEnvs
envs["ARCHS"] = "x86_64"
envs["CURRENT_VARIANT"] = "custom"
envs["OBJECT_FILE_DIR_custom"] = "/OBJECT_FILE_DIR_custom"
let context = try PostbuildContext(config, env: envs)
XCTAssertEqual(context.compilationTempDir, "/OBJECT_FILE_DIR_custom/x86_64")
}
}
@@ -49,7 +49,9 @@ class PostbuildTests: FileXCTestCase {
builtProductsDir: "",
bundleDir: nil,
derivedSourcesDir: "",
thinnedTargets: []
thinnedTargets: [],
action: .build,
modeMarkerPath: ""
)
private var network = RemoteNetworkClientImpl(
NetworkClientFake(fileManager: .default),
@@ -83,6 +85,7 @@ class PostbuildTests: FileXCTestCase {
)
private var modeController = CacheModeControllerFake()
private var metaReader = JsonMetaReader(fileAccessor: FileManager.default)
private var metaWriter = JsonMetaWriter(fileWriter: FileManager.default, pretty: false)
private static let SampleMeta = MainArtifactSampleMeta.defaults
private var sampleMetaFile: URL!
@@ -121,6 +124,7 @@ class PostbuildTests: FileXCTestCase {
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [],
consumerPlugins: []
)
@@ -150,6 +154,7 @@ class PostbuildTests: FileXCTestCase {
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [],
consumerPlugins: []
)
@@ -177,6 +182,7 @@ class PostbuildTests: FileXCTestCase {
dSYMOrganizer: dsymOrganizer,
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [],
consumerPlugins: []
)
@@ -204,6 +210,7 @@ class PostbuildTests: FileXCTestCase {
dSYMOrganizer: dsymOrganizer,
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [],
consumerPlugins: []
)
@@ -241,6 +248,7 @@ class PostbuildTests: FileXCTestCase {
dSYMOrganizer: dsymOrganizer,
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [],
consumerPlugins: []
)
@@ -281,6 +289,7 @@ class PostbuildTests: FileXCTestCase {
dSYMOrganizer: dsymOrganizer,
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [],
consumerPlugins: []
)
@@ -306,6 +315,7 @@ class PostbuildTests: FileXCTestCase {
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
modeController: fakeModeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [],
consumerPlugins: []
)
@@ -353,6 +363,7 @@ class PostbuildTests: FileXCTestCase {
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [plugin],
consumerPlugins: []
)
@@ -384,6 +395,7 @@ class PostbuildTests: FileXCTestCase {
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [plugin],
consumerPlugins: []
)
@@ -416,6 +428,7 @@ class PostbuildTests: FileXCTestCase {
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [],
consumerPlugins: [consumerPlugin]
)
@@ -453,6 +466,7 @@ class PostbuildTests: FileXCTestCase {
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [],
consumerPlugins: [consumerPlugin]
)
@@ -484,6 +498,7 @@ class PostbuildTests: FileXCTestCase {
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [],
consumerPlugins: [consumerPlugin]
)
@@ -517,6 +532,7 @@ class PostbuildTests: FileXCTestCase {
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [],
consumerPlugins: []
)
@@ -549,6 +565,7 @@ class PostbuildTests: FileXCTestCase {
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [],
consumerPlugins: []
)
@@ -557,5 +574,68 @@ class PostbuildTests: FileXCTestCase {
try postbuild.deleteFingerprintOverrides()
XCTAssertFalse(fileManager.fileExists(atPath: previousFingerprintOverride.path))
} // swiftlint:disable:next file_length
}
func testUploadingMeta() throws {
let postbuild = Postbuild(
context: postbuildContext,
networkClient: network,
remapper: remapper,
fingerprintAccumulator: fingerprintGenerator,
artifactsOrganizer: organizer,
artifactCreator: artifactCreator,
fingerprintSyncer: syncer,
dependenciesReader: dependenciesReader,
dependencyProcessor: processor,
fingerprintOverrideManager: overrideManager,
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [],
consumerPlugins: []
)
try postbuild.performMetaUpload(meta: Self.SampleMeta, for: "33")
let data = try network.fetch(.meta(commit: "33"))
let downloadedMeta = try metaReader.read(data: data)
XCTAssertEqual(downloadedMeta, Self.SampleMeta)
}
func testUploadingMetaWithNewPluginKeys() throws {
let plugin = MetaAppenderArtifactCreatorPlugin(["New": "Value"])
let postbuild = Postbuild(
context: postbuildContext,
networkClient: network,
remapper: remapper,
fingerprintAccumulator: fingerprintGenerator,
artifactsOrganizer: organizer,
artifactCreator: artifactCreator,
fingerprintSyncer: syncer,
dependenciesReader: dependenciesReader,
dependencyProcessor: processor,
fingerprintOverrideManager: overrideManager,
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [plugin],
consumerPlugins: []
)
var meta = Self.SampleMeta
meta.pluginsKeys = ["Previous": "Value"]
var expectedMeta = meta
expectedMeta.pluginsKeys = ["New": "Value"]
try postbuild.performMetaUpload(meta: meta, for: "33")
let data = try network.fetch(.meta(commit: "33"))
let downloadedMeta = try metaReader.read(data: data)
XCTAssertEqual(downloadedMeta, expectedMeta)
}
}
// swiftlint:disable:next file_length
@@ -28,6 +28,7 @@ class FileLLDBInitPatcherTests: XCTestCase {
private var patcher: FileLLDBInitPatcher!
override func setUp() {
super.setUp()
accessor = FileAccessorFake(mode: .normal)
patcher = FileLLDBInitPatcher(
file: lldbInitPath,
@@ -62,4 +62,19 @@ class SwiftcContextTests: FileXCTestCase {
XCTAssertEqual(context.mode, .consumer(commit: .unavailable))
}
func testProducerModeWhenFileWithCommitShaExistsIsResolvedToProducerFast() throws {
config.mode = .producerFast
let context = try SwiftcContext(config: config, input: input)
XCTAssertEqual(context.mode, .producerFast)
}
func testProducerModeWhenFileWithCommitShaDoesntExxistIsResolvedToProducer() throws {
config.mode = .producerFast
try fileManager.spt_deleteItem(at: remoteCommitFile)
let context = try SwiftcContext(config: config, input: input)
XCTAssertEqual(context.mode, .producer)
}
}
@@ -21,8 +21,8 @@
import XCTest
class SwiftcFilemapInputEditorTests: FileXCTestCase {
private let sampleInfo = SwiftCompilationInfo(info: SwiftModuleCompilationInfo(
private let sampleInfo = SwiftCompilationInfo(
info: SwiftModuleCompilationInfo(
dependencies: nil,
swiftDependencies: "/"
), files: [])
@@ -65,17 +65,19 @@ class SwiftcFilemapInputEditorTests: FileXCTestCase {
}
}
"""#.data(using: .utf8)!
let expectedInfo = SwiftCompilationInfo(info: SwiftModuleCompilationInfo(
dependencies: "/master.d",
swiftDependencies: "/master.swiftdeps"
), files: [
SwiftFileCompilationInfo(
file: "/file1.swift",
dependencies: "/file1.d",
object: "/file1.o",
swiftDependencies: "/file1.swiftdeps"
let expectedInfo = SwiftCompilationInfo(
info: SwiftModuleCompilationInfo(
dependencies: "/master.d",
swiftDependencies: "/master.swiftdeps"
),
])
files: [
SwiftFileCompilationInfo(
file: "/file1.swift",
dependencies: "/file1.d",
object: "/file1.o",
swiftDependencies: "/file1.swiftdeps"
),
])
try fileManager.spt_writeToFile(atPath: inputFile.path, contents: infoContentData)
let readInfo = try editor.read()
@@ -93,17 +95,18 @@ class SwiftcFilemapInputEditorTests: FileXCTestCase {
}
func testWritingSavesContentWithOptionalParameters() throws {
let extendedInfo = SwiftCompilationInfo(info: SwiftModuleCompilationInfo(
dependencies: "/master.d",
swiftDependencies: "/master.swiftdeps"
), files: [
SwiftFileCompilationInfo(
file: "/file1.swift",
dependencies: "/file1.d",
object: "/file1.o",
swiftDependencies: "/file1.swiftdeps"
),
])
let extendedInfo = SwiftCompilationInfo(
info: SwiftModuleCompilationInfo(
dependencies: "/master.d",
swiftDependencies: "/master.swiftdeps"
), files: [
SwiftFileCompilationInfo(
file: "/file1.swift",
dependencies: "/file1.d",
object: "/file1.o",
swiftDependencies: "/file1.swiftdeps"
),
])
try editor.write(extendedInfo)
@@ -190,4 +190,45 @@ class SwiftcOrchestratorTests: XCTestCase {
XCTAssertNotNil(shellOutSpy.switchedProcess)
}
func testForFailedCompilationMockInProducerFastModeBuildsArtifactObjCHeader() throws {
let swiftc = SwiftcMock(mockingResult: .forceFallback)
let orchestrator = SwiftcOrchestrator(
mode: .producerFast,
swiftc: swiftc,
swiftcCommand: "",
objcHeaderOutput: objcHeaderURL,
moduleOutput: moduleOutputURL,
arch: "archTest",
artifactBuilder: artifactBuilder,
producerFallbackCommandProcessors: [],
invocationStorage: invocationStorage,
shellOut: shellOutSpy
)
try orchestrator.run()
XCTAssertEqual(artifactBuilder.addedObjCHeaders, ["archTest": [objcHeaderURL]])
}
func testSuccessedMockInProducerFastModeDoesntFillObjCHeader() throws {
let swiftc = SwiftcMock(mockingResult: .success)
let orchestrator = SwiftcOrchestrator(
mode: .producerFast,
swiftc: swiftc,
swiftcCommand: "",
objcHeaderOutput: objcHeaderURL,
moduleOutput: moduleOutputURL,
arch: "arch",
artifactBuilder: artifactBuilder,
producerFallbackCommandProcessors: [],
invocationStorage: invocationStorage,
shellOut: shellOutSpy
)
try orchestrator.run()
XCTAssertEqual(artifactBuilder.addedObjCHeaders, [:])
}
}
@@ -277,7 +277,9 @@ class SwiftcTests: FileXCTestCase {
let artifactObjCHeader = URL(fileURLWithPath: "/cachedArtifact/include/archTest/Target-Swift.h")
let artifactSwiftmodule = URL(fileURLWithPath: "/cachedArtifact/swiftmodule/archTest/Target.swiftmodule")
let artifactSwiftdoc = URL(fileURLWithPath: "/cachedArtifact/swiftmodule/archTest/Target.swiftdoc")
let artifactSwiftSourceInfo = URL(fileURLWithPath: "/cachedArtifact/swiftmodule/archTest/Target.swiftsourceinfo")
let artifactSwiftSourceInfo = URL(
fileURLWithPath: "/cachedArtifact/swiftmodule/archTest/Target.swiftsourceinfo"
)
artifactOrganizer = ArtifactOrganizerFake(artifactRoot: artifactRoot)
let swiftc = Swiftc(
@@ -422,4 +424,40 @@ class SwiftcTests: FileXCTestCase {
XCTAssertNoThrow(try swiftc.mockCompilation())
}
func testSkipsGeneratingObjectFileWhenNotProvidedInCompilationInfo() throws {
let outputFilesDir = workingDir.appendingPathComponent("outputFiles")
try fileManager.spt_createEmptyDir(outputFilesDir)
let input = SwiftCompilationInfo(
info: SwiftModuleCompilationInfo(
dependencies: nil,
swiftDependencies: outputFilesDir.appendingPathComponent("master.swiftdeps")
),
files: [
SwiftFileCompilationInfo(
file: "/file1.swift",
dependencies: nil,
object: nil,
swiftDependencies: nil
),
]
)
swiftcInputReader = SwiftcInputReaderStub(info: input)
let swiftc = Swiftc(
inputFileListReader: inputFileListReader,
markerReader: markerReader,
allowedFilesListScanner: allowedFilesListScanner,
artifactOrganizer: artifactOrganizer,
inputReader: swiftcInputReader,
context: context,
markerWriter: markerWriter,
productsGenerator: productsGenerator,
fileManager: FileManager.default,
dependenciesWriterFactory: FileDependenciesWriter.init,
touchFactory: touchFactory,
plugins: []
)
XCTAssertNoThrow(try swiftc.mockCompilation())
} // swiftlint:disable:next file_length
}
@@ -28,7 +28,7 @@ class TemplateBasedCCWrapperBuilderTests: FileXCTestCase {
private static let history = "history.compile"
private static let prebuild = "prebuild.d"
private static let commitSha = "321"
private static let timeout = 5.0
private static let timeout = 10.0
static let xccc: URL = {
let fileManager = FileManager.default
@@ -0,0 +1,33 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
@testable import XCRemoteCache
import XCTest
import Yams
class ModeTests: XCTestCase {
func testProducerFast() throws {
let yaml = "producer-fast"
let decoder = YAMLDecoder(encoding: .utf8)
let mode: Mode = try decoder.decode(from: yaml)
XCTAssertEqual(mode, .producerFast)
}
}
@@ -78,4 +78,41 @@ class DependenciesRemapperCompositeTests: XCTestCase {
XCTAssertEqual(localPath, ["/tmp/root/some.swift", "/pwd/other.swift"])
}
func testRemapsMultipleMatchingMappers() throws {
let remapper = DependenciesRemapperComposite([
StringDependenciesRemapper(mappings: [StringDependenciesRemapper.Mapping(generic: "$(ROOT)", local: "/root")]),
StringDependenciesRemapper(mappings: [StringDependenciesRemapper.Mapping(generic: "$(SPECIFIC)", local: "$(ROOT)/specific")])
])
let localPaths = ["/root/specific/file"]
let genericPaths = remapper.replace(localPaths: localPaths)
XCTAssertEqual(genericPaths, ["$(SPECIFIC)/file"])
}
func testRemapsBackToLocalWithRevertedRemappersOrder() throws {
let remapper = DependenciesRemapperComposite([
StringDependenciesRemapper(mappings: [StringDependenciesRemapper.Mapping(generic: "$(ROOT)", local: "/root")]),
StringDependenciesRemapper(mappings: [StringDependenciesRemapper.Mapping(generic: "$(SPECIFIC)", local: "$(ROOT)/specific")])
])
let genericPaths = ["$(SPECIFIC)/file"]
let localPaths = remapper.replace(genericPaths: genericPaths)
XCTAssertEqual(localPaths, ["/root/specific/file"])
}
func testRemappingTwoMappingsBackAndForthIsIdentical() throws {
let remapper = DependenciesRemapperComposite([
StringDependenciesRemapper(mappings: [StringDependenciesRemapper.Mapping(generic: "$(ROOT)", local: "/root")]),
StringDependenciesRemapper(mappings: [StringDependenciesRemapper.Mapping(generic: "$(SPECIFIC)", local: "$(ROOT)/specific")])
])
let localPaths = ["/root/specific/file"]
let genericPaths = remapper.replace(localPaths: localPaths)
let remappedLocalPaths = remapper.replace(genericPaths: genericPaths)
XCTAssertEqual(localPaths, remappedLocalPaths)
}
}
@@ -27,6 +27,7 @@ class FileFingerprintSyncerTests: FileXCTestCase {
private var swiftmoduleDir: URL!
override func setUpWithError() throws {
try super.setUpWithError()
syncer = FileFingerprintSyncer(
fingerprintOverrideExtension: "md5",
dirAccessor: fileManager,
@@ -0,0 +1,71 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
@testable import XCRemoteCache
import XCTest
class StringDependenciesRemapperFactoryTests: XCTestCase {
private var factory: StringDependenciesRemapperFactory!
override func setUp() {
factory = StringDependenciesRemapperFactory()
}
func testMappingsFromEnvMaps() throws {
let remapper = try factory.build(
orderKeys: ["SRC_ROOT"],
envs: ["SRC_ROOT": "/tmp/root"],
customMappings: [:]
)
let localPaths = remapper.replace(genericPaths: ["$(SRC_ROOT)/some.swift"])
XCTAssertEqual(localPaths, ["/tmp/root/some.swift"])
}
func testInvalidMappingsFromEnvFails() throws {
XCTAssertThrowsError(
try factory.build(
orderKeys: ["SRC_ROOT"],
envs: ["NO_SRC_ROOT": ""],
customMappings: [:]
)
)
}
func testBuildingRemapperWithMergedCustomMappings() throws {
let remapper = try factory.build(
orderKeys: ["PWD"],
envs: ["PWD": "/some"],
customMappings: ["TMP": "/tmp"]
)
let genericPaths = remapper.replace(localPaths: ["/some/repoFile.swift", "/tmp/externalFile.swift"])
XCTAssertEqual(genericPaths, ["$(PWD)/repoFile.swift", "$(TMP)/externalFile.swift"])
}
func testFailsBuildingRemapperWithConflictedMappings() throws {
XCTAssertThrowsError(
try factory.build(
orderKeys: ["PWD"],
envs: ["PWD": "/some"],
customMappings: ["PWD": "/other"]
)
)
}
}
@@ -70,17 +70,29 @@ class StringDependenciesRemapperTests: XCTestCase {
XCTAssertEqual(genericPaths, ["$(SRC_ROOT)/some.swift", "$(PWD)/extra.swift"])
}
func testMappingsFromEnvMaps() throws {
remapper = try StringDependenciesRemapper.buildFromEnvs(keys: ["SRC_ROOT"], envs: ["SRC_ROOT": "/tmp/root"])
func testMappingsLocalPathsIsDoneInOrder() {
let mappings: [StringDependenciesRemapper.Mapping] = [
.init(generic: "$(TMP)", local: "/tmp"),
.init(generic: "$(ROOT)", local: "$(TMP)/root"),
]
remapper = StringDependenciesRemapper(mappings: mappings)
let localPaths = remapper.replace(genericPaths: ["$(SRC_ROOT)/some.swift"])
let genericPaths = remapper.replace(localPaths: ["/tmp/root/some.swift"])
XCTAssertEqual(genericPaths, ["$(ROOT)/some.swift"])
}
func testMappingsGenericPathsIsDoneInReversedOrder() {
let mappings: [StringDependenciesRemapper.Mapping] = [
.init(generic: "$(TMP)", local: "/tmp"),
.init(generic: "$(ROOT)", local: "$(TMP)/root"),
]
remapper = StringDependenciesRemapper(mappings: mappings)
let localPaths = remapper.replace(genericPaths: ["$(ROOT)/some.swift"])
XCTAssertEqual(localPaths, ["/tmp/root/some.swift"])
}
func testInvalidMappingsFromEnvFAils() throws {
XCTAssertThrowsError(
try StringDependenciesRemapper.buildFromEnvs(keys: ["SRC_ROOT"], envs: ["NO_SRC_ROOT": ""])
)
}
}
@@ -27,6 +27,7 @@ class TargetDependenciesReaderTests: XCTestCase {
private var reader: TargetDependenciesReader!
override func setUp() {
super.setUp()
dirAccessor = DirAccessorFake()
/// A Factory that builds a faked dependency reader that returns a single dependency,
/// a basename of the input .d file and the ".swift" extension
@@ -27,6 +27,7 @@ class CopyDiskCopierTests: FileXCTestCase {
private var emptySourceFile: URL!
override func setUpWithError() throws {
try super.setUpWithError()
workingDir = try prepareTempDir()
emptySourceFile = workingDir.appendingPathComponent("source")
try fileManager.spt_writeToFile(atPath: emptySourceFile.path, contents: Data())
@@ -24,7 +24,7 @@ class EnvironmentFingerprintGeneratorTests: XCTestCase {
private static let defaultENV = [
"GCC_PREPROCESSOR_DEFINITIONS": "GCC",
"CLANG_PROFILE_DATA_DIRECTORY": "CLANG",
"CLANG_COVERAGE_MAPPING": "YES",
"TARGET_NAME": "TARGET",
"CONFIGURATION": "CONG",
"PLATFORM_NAME": "PLAT",
@@ -33,6 +33,7 @@ class EnvironmentFingerprintGeneratorTests: XCTestCase {
"DYLIB_COMPATIBILITY_VERSION": "2",
"DYLIB_CURRENT_VERSION": "3",
"PRODUCT_MODULE_NAME": "4",
"ARCHS": "AR"
]
/// Corresponds to EnvironmentFingerprintGenerator.version
private static let currentVersion = "5"
@@ -55,7 +56,7 @@ class EnvironmentFingerprintGeneratorTests: XCTestCase {
func testConsidersDefaultEnvs() throws {
let fingerprint = try fingerprintGenerator.generateFingerprint()
XCTAssertEqual(fingerprint, "GCC,CLANG,TARGET,CONG,PLAT,XC,1,2,3,4,\(Self.currentVersion)")
XCTAssertEqual(fingerprint, "GCC,YES,TARGET,CONG,PLAT,XC,1,2,3,4,AR,\(Self.currentVersion)")
}
func testFingerprintIncludesVersionAsLastComponent() throws {
@@ -73,7 +74,7 @@ class EnvironmentFingerprintGeneratorTests: XCTestCase {
let fingerprint = try fingerprintGenerator.generateFingerprint()
XCTAssertEqual(fingerprint, ",,,,,,,,,,\(Self.currentVersion)")
XCTAssertEqual(fingerprint, ",,,,,,,,,,,\(Self.currentVersion)")
}
func testConsidersCustomEnvs() throws {
@@ -89,6 +90,6 @@ class EnvironmentFingerprintGeneratorTests: XCTestCase {
let fingerprint = try fingerprintGenerator.generateFingerprint()
XCTAssertEqual(fingerprint, "GCC,CLANG,TARGET,CONG,PLAT,XC,1,2,3,4,CUSTOM_VALUE,\(Self.currentVersion)")
XCTAssertEqual(fingerprint, "GCC,YES,TARGET,CONG,PLAT,XC,1,2,3,4,AR,CUSTOM_VALUE,\(Self.currentVersion)")
}
}
@@ -0,0 +1,61 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
@testable import XCRemoteCache
import XCTest
class JsonMetaWriterTests: XCTestCase {
func testWritesToFileWithFilekeyFilename() throws {
let fileAccessor = FileAccessorFake(mode: .normal)
let writer = JsonMetaWriter(fileWriter: fileAccessor, pretty: false)
let workingDir: URL = "/"
let meta = MainArtifactSampleMeta.defaults
let url = try writer.write(MainArtifactSampleMeta.defaults, locationDir: workingDir)
XCTAssertEqual(url, workingDir.appendingPathComponent(meta.fileKey).appendingPathExtension("json"))
}
func testWritesMetaInValidFormat() throws {
let fileAccessor = FileAccessorFake(mode: .normal)
let writer = JsonMetaWriter(fileWriter: fileAccessor, pretty: false)
let reader = JsonMetaReader(fileAccessor: fileAccessor)
let workingDir: URL = "/"
let meta = MainArtifactSampleMeta.defaults
let url = try writer.write(MainArtifactSampleMeta.defaults, locationDir: workingDir)
let readMeta = try reader.read(localFile: url)
XCTAssertEqual(readMeta, meta)
}
func testWritesPrettyMetaInValidFormat() throws {
let fileAccessor = FileAccessorFake(mode: .normal)
let writer = JsonMetaWriter(fileWriter: fileAccessor, pretty: true)
let reader = JsonMetaReader(fileAccessor: fileAccessor)
let workingDir: URL = "/"
let meta = MainArtifactSampleMeta.defaults
let url = try writer.write(MainArtifactSampleMeta.defaults, locationDir: workingDir)
let readMeta = try reader.read(localFile: url)
XCTAssertEqual(readMeta, meta)
}
}
@@ -26,6 +26,7 @@ class FilteredInvocationStorageTests: XCTestCase {
var storage: FilteredInvocationStorage!
override func setUp() {
super.setUp()
storage = FilteredInvocationStorage(storage: underlyingStorage, retrieveIgnoredCommands: ["to_ignore"])
}
@@ -28,6 +28,7 @@ class InvocationFileStorageTests: FileXCTestCase {
private var storage: ExistingFileStorage!
override func setUpWithError() throws {
try super.setUpWithError()
file = try prepareTempDir().appendingPathComponent("file.history")
try fileManager.spt_createEmptyFile(file)
storage = ExistingFileStorage(storageFile: file, command: command)
@@ -0,0 +1,103 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
@testable import XCRemoteCache
import XCTest
class ActionSpecificCacheHitLoggerTests: FileXCTestCase {
private var coordinator: StatsCoordinator!
override func setUp() {
super.setUp()
coordinator = InMemoryStatsCoordinator()
}
func testReportsBuildHitLogsToAStandardBuild() throws {
let logger = ActionSpecificCacheHitLogger(action: .build, statsLogger: coordinator)
try logger.logHit()
let allStats = try coordinator.readStats()
XCTAssertEqual(
allStats,
.init(
hitCount: 1,
missCount: 0,
localCacheBytes: 0,
indexingHitCount: 0,
indexingMissCount: 0
)
)
}
func testReportsBuildMissToAStandardBuild() throws {
let logger = ActionSpecificCacheHitLogger(action: .build, statsLogger: coordinator)
try logger.logMiss()
let allStats = try coordinator.readStats()
XCTAssertEqual(
allStats,
.init(
hitCount: 0,
missCount: 1,
localCacheBytes: 0,
indexingHitCount: 0,
indexingMissCount: 0
)
)
}
func testReportsIndexbuildHitToIndexingBuild() throws {
let logger = ActionSpecificCacheHitLogger(action: .index, statsLogger: coordinator)
try logger.logHit()
let allStats = try coordinator.readStats()
XCTAssertEqual(
allStats,
.init(
hitCount: 0,
missCount: 0,
localCacheBytes: 0,
indexingHitCount: 1,
indexingMissCount: 0
)
)
}
func testReportsIndexbuildMissToIndexingBuild() throws {
let logger = ActionSpecificCacheHitLogger(action: .index, statsLogger: coordinator)
try logger.logMiss()
let allStats = try coordinator.readStats()
XCTAssertEqual(
allStats,
.init(
hitCount: 0,
missCount: 0,
localCacheBytes: 0,
indexingHitCount: 0,
indexingMissCount: 1
)
)
}
}
@@ -0,0 +1,45 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
@testable import XCRemoteCache
/// Fake that manages all logs in memory and reports 0 cached bytes
/// Note: This fake is thread unsafe
class InMemoryStatsCoordinator: StatsCoordinator {
private var counters = [XCRemoteCacheStatistics.Counter: Int]()
func log(_ event: XCRemoteCacheStatistics.Counter) throws {
counters[event, default: 0] += 1
}
func readStats() throws -> XCRemoteCacheStatistics {
XCRemoteCacheStatistics(
hitCount: counters[.targetCacheHit] ?? 0,
missCount: counters[.targetCacheMiss] ?? 0,
localCacheBytes: 0,
indexingHitCount: counters[.indexingTargetHitCount] ?? 0,
indexingMissCount: counters[.indexingTargetMissCount] ?? 0
)
}
func reset() throws {
counters = [:]
}
}
+13
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,7 @@ An object that is passed to the `xcremotecache` can contain all properties suppo
| `modify_lldb_init` | Controls if the pod integration should modify `~/.lldbinit` | `true` | ⬜️ |
| `xccc_file` | The path where should be placed the `xccc` binary (in the pod installation phase) | `{podfile_dir}/.rc/xccc` | ⬜️ |
| `remote_commit_file` | The path of the file with the remote commit sha (in the pod installation phase) | `{podfile_dir}/.rc/arc.rc`| ⬜️ |
| `prettify_meta_files` | A Boolean value that opts-in pretty JSON formatting for meta files | `false` | ⬜️ |
## Uninstalling
@@ -52,3 +61,7 @@ To fully uninstall the plugin, call:
```bash
gem uninstall cocoapods-xcremotecache
```
## Limitations
* When `generate_multiple_pod_projects` mode is enabled, only first-party targets are cached by XCRemoteCache (all dependencies are compiled locally).
@@ -17,6 +17,7 @@ require 'cocoapods/resolver'
require 'open-uri'
require 'yaml'
require 'json'
require 'pathname'
module CocoapodsXCRemoteCacheModifier
@@ -26,6 +27,7 @@ module CocoapodsXCRemoteCacheModifier
FAKE_SRCROOT = "/#{'x' * 10 }"
LLDB_INIT_COMMENT="#RemoteCacheCustomSourceMap"
LLDB_INIT_PATH = "#{ENV['HOME']}/.lldbinit"
FAT_ARCHIVE_NAME_INFIX = 'arm64-x86_64'
CUSTOM_CONFIGURATION_KEYS = [
'enabled',
@@ -45,18 +47,19 @@ module CocoapodsXCRemoteCacheModifier
@@configuration = c
end
def self.set_configuration_default_values(user_proj_directory)
def self.set_configuration_default_values
default_values = {
'mode' => 'consumer',
'enabled' => true,
'xcrc_location' => "#{user_proj_directory}/XCRC",
'xcrc_location' => "XCRC",
'exclude_build_configurations' => [],
'check_build_configuration' => 'Debug',
'check_platform' => 'iphonesimulator',
'modify_lldb_init' => true,
'xccc_file' => "#{user_proj_directory}/#{BIN_DIR}/xccc",
'remote_commit_file' => "#{user_proj_directory}/#{BIN_DIR}/arc.rc",
'xccc_file' => "#{BIN_DIR}/xccc",
'remote_commit_file' => "#{BIN_DIR}/arc.rc",
'exclude_targets' => [],
'prettify_meta_files' => false
}
@@configuration.merge! default_values.select { |k, v| !@@configuration.key?(k) }
end
@@ -88,61 +91,96 @@ module CocoapodsXCRemoteCacheModifier
@@configuration.select { |key, value| !CUSTOM_CONFIGURATION_KEYS.include?(key) }
end
def self.enable_xcremotecache(target, user_proj_directory, xc_location, xc_cc_path, mode, exclude_build_configurations, check_build_configuration, check_platform, final_target)
target.build_configurations.each do |config|
# apply only for relevant Configurations
next if exclude_build_configurations.include?(config.name)
if mode == 'consumer'
config.build_settings['CC'] = [xc_cc_path]
end
config.build_settings['SWIFT_EXEC'] = ["#{xc_location}/xcswiftc"]
config.build_settings['LIBTOOL'] = ["#{xc_location}/xclibtool"]
config.build_settings['LD'] = ["#{xc_location}/xcld"]
def self.parent_dir(path, parent_count)
"../" * parent_count + path
end
config.build_settings['XCREMOTE_CACHE_FAKE_SRCROOT'] = FAKE_SRCROOT
add_cflags!(config.build_settings, '-fdebug-prefix-map', "#{user_proj_directory}=$(XCREMOTE_CACHE_FAKE_SRCROOT)")
add_swiftflags!(config.build_settings, '-debug-prefix-map', "#{user_proj_directory}=$(XCREMOTE_CACHE_FAKE_SRCROOT)")
end
# @param target [Target] target to apply XCRemoteCache
# @param repo_distance [Integer] distance from the git repo root to the target's $SRCROOT
# @param xc_location [String] path to the dir with all XCRemoteCache binaries, relative to the repo root
# @param xc_cc_path [String] path to the XCRemoteCache clang wrapper, relative to the repo root
# @param mode [String] mode name ('consumer', 'producer' etc.)
# @param exclude_build_configurations [String[]] list of targets that should have disabled remote cache
# @param final_target [String] name of target that should trigger marking
def self.enable_xcremotecache(target, repo_distance, xc_location, xc_cc_path, mode, exclude_build_configurations, final_target)
srcroot_relative_xc_location = parent_dir(xc_location, repo_distance)
# User project is not generated from scratch (contrary to `Pods`), delete all previous XCRemoteCache phases
target.build_phases.delete_if {|phase|
# Some phases (e.g. PBXSourcesBuildPhase) don't have strict name check respond_to?
if phase.respond_to?(:name)
phase.name != nil && phase.name.start_with?("[XCRC]")
target.build_configurations.each do |config|
# apply only for relevant Configurations
next if exclude_build_configurations.include?(config.name)
if mode == 'consumer'
config.build_settings['CC'] = ["$SRCROOT/#{parent_dir(xc_cc_path, repo_distance)}"]
end
}
config.build_settings['SWIFT_EXEC'] = ["$SRCROOT/#{srcroot_relative_xc_location}/xcswiftc"]
config.build_settings['LIBTOOL'] = ["$SRCROOT/#{srcroot_relative_xc_location}/xclibtool"]
config.build_settings['LD'] = ["$SRCROOT/#{srcroot_relative_xc_location}/xcld"]
config.build_settings['XCREMOTE_CACHE_FAKE_SRCROOT'] = FAKE_SRCROOT
config.build_settings['XCRC_PLATFORM_PREFERRED_ARCH'] = ["$(LINK_FILE_LIST_$(CURRENT_VARIANT)_$(PLATFORM_PREFERRED_ARCH):dir:standardizepath:file:default=arm64)"]
debug_prefix_map_replacement = '$(SRCROOT' + ':dir:standardizepath' * repo_distance + ')'
add_cflags!(config.build_settings, '-fdebug-prefix-map', "#{debug_prefix_map_replacement}=$(XCREMOTE_CACHE_FAKE_SRCROOT)")
add_swiftflags!(config.build_settings, '-debug-prefix-map', "#{debug_prefix_map_replacement}=$(XCREMOTE_CACHE_FAKE_SRCROOT)")
end
# Prebuild
if mode == 'consumer'
prebuild_script = target.new_shell_script_build_phase("[XCRC] Prebuild")
existing_prebuild_script = target.build_phases.detect do |phase|
if phase.respond_to?(:name)
phase.name != nil && phase.name.start_with?("[XCRC] Prebuild")
end
end
prebuild_script = existing_prebuild_script || target.new_shell_script_build_phase("[XCRC] Prebuild")
prebuild_script.shell_script = "\"$SCRIPT_INPUT_FILE_0\""
prebuild_script.input_paths = ["#{xc_location}/xcprebuild"]
prebuild_script.input_paths = ["$SRCROOT/#{srcroot_relative_xc_location}/xcprebuild"]
prebuild_script.output_paths = [
"$(TARGET_TEMP_DIR)/rc.enabled",
"$(TARGET_TEMP_DIR)/rc.enabled",
"$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)"
]
prebuild_script.dependency_file = "$(TARGET_TEMP_DIR)/prebuild.d"
# Move prebuild (last element) to the first position (to make it real 'prebuild')
target.build_phases.rotate!(-1)
target.build_phases.rotate!(-1) if existing_prebuild_script.nil?
elsif mode == 'producer'
# Delete existing prebuild build phase (to support switching between modes)
target.build_phases.delete_if do |phase|
if phase.respond_to?(:name)
phase.name != nil && phase.name.start_with?("[XCRC] Prebuild")
end
end
end
# Postbuild
postbuild_script = target.new_shell_script_build_phase("[XCRC] Postbuild")
existing_postbuild_script = target.build_phases.detect do |phase|
if phase.respond_to?(:name)
phase.name != nil && phase.name.start_with?("[XCRC] Postbuild")
end
end
postbuild_script = existing_postbuild_script || target.new_shell_script_build_phase("[XCRC] Postbuild")
postbuild_script.shell_script = "\"$SCRIPT_INPUT_FILE_0\""
postbuild_script.input_paths = ["#{xc_location}/xcpostbuild"]
postbuild_script.input_paths = ["$SRCROOT/#{srcroot_relative_xc_location}/xcpostbuild"]
postbuild_script.output_paths = [
"$(TARGET_BUILD_DIR)/$(MODULES_FOLDER_PATH)/$(PRODUCT_MODULE_NAME).swiftmodule/$(PLATFORM_PREFERRED_ARCH).swiftmodule.md5",
"$(TARGET_BUILD_DIR)/$(MODULES_FOLDER_PATH)/$(PRODUCT_MODULE_NAME).swiftmodule/$(PLATFORM_PREFERRED_ARCH)-$(LLVM_TARGET_TRIPLE_VENDOR)-$(SWIFT_PLATFORM_TARGET_PREFIX)$(LLVM_TARGET_TRIPLE_SUFFIX).swiftmodule.md5"
"$(TARGET_BUILD_DIR)/$(MODULES_FOLDER_PATH)/$(PRODUCT_MODULE_NAME).swiftmodule/$(XCRC_PLATFORM_PREFERRED_ARCH).swiftmodule.md5",
"$(TARGET_BUILD_DIR)/$(MODULES_FOLDER_PATH)/$(PRODUCT_MODULE_NAME).swiftmodule/$(XCRC_PLATFORM_PREFERRED_ARCH)-$(LLVM_TARGET_TRIPLE_VENDOR)-$(SWIFT_PLATFORM_TARGET_PREFIX)$(LLVM_TARGET_TRIPLE_SUFFIX).swiftmodule.md5"
]
postbuild_script.dependency_file = "$(TARGET_TEMP_DIR)/postbuild.d"
# Mark a sha as ready for a given platform and configuration when building the final_target
if mode == 'producer' && target.name == final_target
mark_script = target.new_shell_script_build_phase("[XCRC] Mark")
existing_mark_script = target.build_phases.detect do |phase|
if phase.respond_to?(:name)
phase.name != nil && phase.name.start_with?("[XCRC] Mark")
end
end
mark_script = existing_mark_script || target.new_shell_script_build_phase("[XCRC] Mark")
mark_script.shell_script = "\"$SCRIPT_INPUT_FILE_0\" mark --configuration $CONFIGURATION --platform $PLATFORM_NAME"
mark_script.input_paths = ["#{xc_location}/xcprepare"]
mark_script.input_paths = ["$SRCROOT/#{srcroot_relative_xc_location}/xcprepare"]
else
# Delete existing mark build phase (to support switching between modes or changing the final target)
target.build_phases.delete_if do |phase|
if phase.respond_to?(:name)
phase.name != nil && phase.name.start_with?("[XCRC] Mark")
end
end
end
end
@@ -196,15 +234,18 @@ module CocoapodsXCRemoteCacheModifier
release_url = 'https://api.github.com/repos/spotify/XCRemoteCache/releases/latest'
asset_url = nil
open(release_url, :http_basic_authentication => credentials) do |f|
asset_url = JSON.parse(f.read)['assets'][0]['url']
URI.open(release_url) do |f|
assets_array = JSON.parse(f.read)['assets']
# Pick fat archive
asset_array = assets_array.detect{|arr| arr['name'].include?(FAT_ARCHIVE_NAME_INFIX)}
asset_url = asset_array['url']
end
if asset_url.nil?
throw "Downloading XCRemoteCache failed"
end
open(asset_url, :http_basic_authentication => credentials, "accept" => 'application/octet-stream') do |f|
URI.open(asset_url, "accept" => 'application/octet-stream') do |f|
File.open(local_package_location, "wb") do |file|
file.puts f.read
end
@@ -247,6 +288,7 @@ module CocoapodsXCRemoteCacheModifier
# Returns the content (array of lines) of the lldbinit with stripped XCRemoteCache rewrite
def self.clean_lldbinit_content(lldbinit_path)
all_lines = []
return all_lines unless File.exist?(lldbinit_path)
File.open(lldbinit_path) { |file|
while(line = file.gets) != nil
line = line.strip
@@ -286,7 +328,7 @@ module CocoapodsXCRemoteCacheModifier
begin
user_proj_directory = File.dirname(user_project.path)
set_configuration_default_values(user_proj_directory)
set_configuration_default_values
unless @@configuration['enabled']
Pod::UI.puts "[XCRC] XCRemoteCache disabled"
@@ -306,22 +348,59 @@ module CocoapodsXCRemoteCacheModifier
check_build_configuration = @@configuration['check_build_configuration']
check_platform = @@configuration['check_platform']
xccc_location_absolute = "#{user_proj_directory}/#{xccc_location}"
xcrc_location_absolute = "#{user_proj_directory}/#{xcrc_location}"
remote_commit_file_absolute = "#{user_proj_directory}/#{remote_commit_file}"
# Download XCRC
download_xcrc_if_needed(xcrc_location)
download_xcrc_if_needed(xcrc_location_absolute)
# Save .rcinfo
save_rcinfo(generate_rcinfo(), user_proj_directory)
root_rcinfo = generate_rcinfo()
save_rcinfo(root_rcinfo, user_proj_directory)
# Create directory for xccc & arc.rc location
Dir.mkdir(BIN_DIR) unless File.exist?(BIN_DIR)
# Remove previous xccc & arc.rc
File.delete(remote_commit_file) if File.exist?(remote_commit_file)
File.delete(xccc_location) if File.exist?(xccc_location)
File.delete(remote_commit_file_absolute) if File.exist?(remote_commit_file_absolute)
File.delete(xccc_location_absolute) if File.exist?(xccc_location_absolute)
# Prepare XCRC
# Pods projects can be generated only once (if incremental_installation is enabled)
# Always integrate XCRemoteCache to all Pods, in case it will be needed later
unless installer_context.pods_project.nil?
# Attach XCRemoteCache to Pods targets
installer_context.pods_project.targets.each do |target|
next if target.name.start_with?("Pods-")
next if target.name.end_with?("Tests")
next if exclude_targets.include?(target.name)
enable_xcremotecache(target, 1, xcrc_location, xccc_location, mode, exclude_build_configurations, final_target)
end
# Create .rcinfo into `Pods` directory as that .xcodeproj reads configuration from .xcodeproj location
pods_proj_directory = installer_context.sandbox_root
# Manual Pods/.rcinfo generation
# all paths in .rcinfo are relative to the root so paths used in Pods.xcodeproj need to be aligned
pods_path = Pathname.new(pods_proj_directory)
root_path = Pathname.new(user_proj_directory)
root_path_to_pods = root_path.relative_path_from(pods_path)
pods_rcinfo = root_rcinfo.merge({
'remote_commit_file' => "#{root_path_to_pods}/#{remote_commit_file}",
'xccc_file' => "#{root_path_to_pods}/#{xccc_location}"
})
save_rcinfo(pods_rcinfo, pods_proj_directory)
installer_context.pods_project.save()
end
# Enabled/disable XCRemoteCache for the main (user) project
begin
prepare_result = YAML.load`#{xcrc_location}/xcprepare --configuration #{check_build_configuration} --platform #{check_platform}`
prepare_result = YAML.load`#{xcrc_location_absolute}/xcprepare --configuration #{check_build_configuration} --platform #{check_platform}`
unless prepare_result['result'] || mode != 'consumer'
# Uninstall the XCRemoteCache for the consumer mode
disable_xcremotecache(user_project)
@@ -334,26 +413,11 @@ module CocoapodsXCRemoteCacheModifier
next
end
# Attach XCRemoteCache to Pods targets
installer_context.pods_project.targets.each do |target|
next if target.name.start_with?("Pods-")
next if target.name.end_with?("Tests")
next if exclude_targets.include?(target.name)
enable_xcremotecache(target, user_proj_directory, xcrc_location, xccc_location, mode, exclude_build_configurations, check_build_configuration, check_platform, final_target)
end
# Create .rcinfo into `Pods` directory as that .xcodeproj reads configuration from .xcodeproj location
pods_proj_directory = installer_context.sandbox_root
# Manual .rcinfo generation (in YAML format)
save_rcinfo({'extra_configuration_file' => "#{user_proj_directory}/.rcinfo"}, pods_proj_directory)
installer_context.pods_project.save()
# Attach XCRC to the app targets
user_project.targets.each do |target|
next if exclude_targets.include?(target.name)
enable_xcremotecache(target, user_proj_directory, xcrc_location, xccc_location, mode, exclude_build_configurations, check_build_configuration, check_platform, final_target)
enable_xcremotecache(target, 0, xcrc_location, xccc_location, mode, exclude_build_configurations, final_target)
end
# Set Target sourcemap
@@ -13,5 +13,5 @@
# limitations under the License.
module CocoapodsXcremotecache
VERSION = "0.0.1"
VERSION = "0.0.4"
end
+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

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB