Compare commits
286 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 25ff5a790b | |||
| f446f6061d | |||
| b46d1e2ca1 | |||
| 88d0666da9 | |||
| 08bf5c21bf | |||
| be97a6a247 | |||
| dff71f8070 | |||
| cda5fc1188 | |||
| 15586755d2 | |||
| e978eb182c | |||
| 7e98cebdd9 | |||
| fbc2982aa3 | |||
| cbcc028cad | |||
| 2acf97eca1 | |||
| 40803cf747 | |||
| 97496ed7b8 | |||
| 45222f8e33 | |||
| 91b5b5e590 | |||
| f2a7880c24 | |||
| 00cb8cc23d | |||
| a3cd6bea07 | |||
| 86c762b070 | |||
| bd21156695 | |||
| 3a82ad91b2 | |||
| 5a5bf35c4a | |||
| 1f766ad4a4 | |||
| 3c3cd84d81 | |||
| 7728733aef | |||
| 50580bf9fd | |||
| ceaff318d6 | |||
| 4cc932a592 | |||
| 75bef0baf6 | |||
| 03671e71b7 | |||
| 91505a59d2 | |||
| 59c1d999b1 | |||
| ba58c1c21e | |||
| 96ce18bc31 | |||
| 30e49ef7bc | |||
| d46f09c6dd | |||
| 2fdf6c39e2 | |||
| 064b22a2d1 | |||
| ddeffe75d6 | |||
| 181997e3d2 | |||
| 1d6ac2c171 | |||
| 3dfd6e296f | |||
| c39b286222 | |||
| a55a4547ac | |||
| 51c2007c5b | |||
| fc092fc4e3 | |||
| 3f9596f6e6 | |||
| 15f4ee7eab | |||
| a6325db0a5 | |||
| cfa4fbd070 | |||
| c573ced4f4 | |||
| 0c2afc15fc | |||
| 586e8df56e | |||
| ef3a88c6ec | |||
| 95f95be29e | |||
| 4d12800096 | |||
| fbb456b0e0 | |||
| a321ea3362 | |||
| 963e6858ee | |||
| f9999a402f | |||
| dbfe1dd8d4 | |||
| def12fab6f | |||
| 6fee69f081 | |||
| b74bafa5d7 | |||
| 33de361317 | |||
| 0aeb5aee36 | |||
| 8682731f30 | |||
| 62b637bdaa | |||
| 0bca5e5bc4 | |||
| eb0d4b17b7 | |||
| d9ea5bfd49 | |||
| ee8bf69814 | |||
| 2c0f78056f | |||
| b6b17bdc8a | |||
| a387359930 | |||
| 456233b90e | |||
| d6f6e2e9bc | |||
| a35bd0b394 | |||
| def131f2a0 | |||
| c052ed8ed6 | |||
| d4f9486b92 | |||
| 8241914543 | |||
| b5ff16484f | |||
| 2b9dde9aec | |||
| 136e7a99ff | |||
| c626d51f97 | |||
| e8ddc9297d | |||
| 3b614c6172 | |||
| 87a214104e | |||
| 599e1e229d | |||
| 423da7cc4a | |||
| 4b082e9dd2 | |||
| 36803d6b5d | |||
| 5809bc963c | |||
| cb6626cfbc | |||
| 3e18711e09 | |||
| ccda424791 | |||
| 90d784cc8d | |||
| 6715824195 | |||
| c4e08d4288 | |||
| a2067f8dfe | |||
| 253e9597bd | |||
| 4a93f944fd | |||
| f839b4064b | |||
| e71837b8b2 | |||
| b45792646b | |||
| 1966562eef | |||
| 883b207c5b | |||
| ca137d0ce4 | |||
| b9a633c86f | |||
| 55a87eb4e9 | |||
| e6b56024b9 | |||
| 8c34a31110 | |||
| f7c32d6e80 | |||
| b3a16ae5d0 | |||
| d013fe4c81 | |||
| 4aefee078e | |||
| 9363e68d51 | |||
| 4af8156da5 | |||
| f086feb005 | |||
| d98d4cd0a3 | |||
| a38d445ae9 | |||
| 0436f6ae27 | |||
| be78959437 | |||
| cccae0d9f6 | |||
| e7d1e905cf | |||
| 20d53a1c71 | |||
| f4ba03d581 | |||
| 0e48d39818 | |||
| 3b30939f99 | |||
| e263cb6e25 | |||
| b805bf4a99 | |||
| fd7b68a344 | |||
| a63488a043 | |||
| 48bcdd8ce9 | |||
| 41dd1cae03 | |||
| 1e86cac3ec | |||
| 22faa5dbdb | |||
| 522900748d | |||
| 4998cc4f87 | |||
| 9221f9d2b5 | |||
| 1127257ad5 | |||
| 599e5fe561 | |||
| 478649a1d7 | |||
| 1c5aa569dd | |||
| c6b31d3086 | |||
| f0a4d361b1 | |||
| 0778639ae2 | |||
| c44b793a19 | |||
| 2b383f046e | |||
| cf0b27d03c | |||
| b2d47760cf | |||
| 714c9ef35b | |||
| b872a8d7f9 | |||
| 56b6722dbe | |||
| 92875bcdee | |||
| 46a99bbe32 | |||
| 408698f1e8 | |||
| e54ce770e7 | |||
| 49be11184e | |||
| d8850b555a | |||
| 93a60b2b40 | |||
| cde81d852f | |||
| 97328144fd | |||
| 3f8333c07b | |||
| 2160645f2c | |||
| d9c2213f50 | |||
| 327d282e23 | |||
| 06781763aa | |||
| 60c7a586c7 | |||
| 2ac3da9035 | |||
| 4dbc5c9b19 | |||
| 6d10ac8c7d | |||
| 8b8c2d627c | |||
| b1026b16da | |||
| e1cc629c55 | |||
| f75c77efa2 | |||
| 3f8ec5b453 | |||
| 94b57475e8 | |||
| a30d56ac16 | |||
| 81077edfa4 | |||
| da3a5d59fa | |||
| 94490532f7 | |||
| 326cac7668 | |||
| 44a09befa0 | |||
| cdddc5bf19 | |||
| 93b8dcd0c3 | |||
| da0d1c20d2 | |||
| 8155e042b5 | |||
| f3832c31bd | |||
| 6a7e6d1135 | |||
| da59c2a211 | |||
| eaba7f3c67 | |||
| bab3326175 | |||
| dc0a82058b | |||
| c5c2732cc9 | |||
| 46debcfa8a | |||
| 3f6d3af5a5 | |||
| c42cb7ac55 | |||
| c65eccc5ee | |||
| a7316d35cc | |||
| 62a7fea0be | |||
| 88e4dceb99 | |||
| e8db767d4a | |||
| 12c635e5ca | |||
| 7f95da7b7c | |||
| 5892a92546 | |||
| 7aa44f20c1 | |||
| 9df2bd5a8e | |||
| 508b11d6ac | |||
| 5f2a8409f2 | |||
| df627ca374 | |||
| a905cdbddc | |||
| 6995c7c1b7 | |||
| 7f43cb87bd | |||
| 5ff9888c11 | |||
| ad545c7802 | |||
| 057c5c3e28 | |||
| 4116dba33d | |||
| 46cc3b75aa | |||
| 2285822ae6 | |||
| e518d28723 | |||
| 3b453b15bc | |||
| 73edc2c7aa | |||
| a0c21471b9 | |||
| 610946f0c4 | |||
| 53d4f07286 | |||
| bc9a77a58f | |||
| e0205f749a | |||
| 0d259b56d3 | |||
| 366c485453 | |||
| 8f86917597 | |||
| f9524a6854 | |||
| 9a27fa81a4 | |||
| c55f1a5803 | |||
| e4d277c8db | |||
| 6cd662bf7d | |||
| 56081f75b3 | |||
| c1cd1ac565 | |||
| 768a296175 | |||
| 6a1a8c6919 | |||
| 3d02af8ade | |||
| bf90d518f4 | |||
| 634afb3f3f | |||
| c2b80c0112 | |||
| d0604e9042 | |||
| 297c1a90cb | |||
| 34cb54b675 | |||
| 14b2b3aceb | |||
| cbed913c63 | |||
| 63448ff0a0 | |||
| 64bccaed16 | |||
| 80a7abb4d5 | |||
| c50ee6f798 | |||
| 6e4bf25d1c | |||
| 71af03f227 | |||
| 0ebe6f5ceb | |||
| 29cba26c5d | |||
| 08b6115187 | |||
| bbbb0a5b0f | |||
| 758764ad95 | |||
| f332593076 | |||
| d0b2bc0f71 | |||
| e2f68c8f4e | |||
| dbff760716 | |||
| bb05b02bd8 | |||
| 5c568a1338 | |||
| 86273017b4 | |||
| 4f1f73132e | |||
| ec1ef567cb | |||
| 1c94e51059 | |||
| ba41e40bb0 | |||
| a0c88d9059 | |||
| 15173b9575 | |||
| fa82f920ad | |||
| 78031a3135 | |||
| 2156de1706 | |||
| ce2ef3ea69 | |||
| 3219ac24aa | |||
| d9ef32f24f | |||
| 3aaf483263 | |||
| cc691b8c67 | |||
| 4c95ff915f |
@@ -0,0 +1,65 @@
|
||||
---
|
||||
name: ⚠️ Bug Report
|
||||
about: Something isn't working as expected
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
PLEASE HELP US PROCESS GITHUB ISSUES FASTER BY PROVIDING THE FOLLOWING INFORMATION.
|
||||
-->
|
||||
|
||||
**My integration setup**
|
||||
|
||||
[ ] CocoaPods cocoapods-xcremotecache plugin
|
||||
[ ] Automatic integration using `xcprepare integrate ...`
|
||||
[ ] Manual integration
|
||||
[ ] Carthage
|
||||
|
||||
**Expected/desired behavior**
|
||||
<!-- Describe what the desired behavior would be. -->
|
||||
|
||||
**Minimal reproduction of the problem with instructions**
|
||||
<!-- Please provide the *STEPS TO REPRODUCE*. -->
|
||||
|
||||
**Producer Logs**
|
||||
<!-- Capture logs from 10 minutes: `log show --predicate 'sender BEGINSWITH "xc"' --style compact --info --debug -last 10m` -->
|
||||
|
||||
<details>
|
||||
<pre> [REPLACE THIS WITH YOUR INFORMATION] </pre>
|
||||
</details>
|
||||
|
||||
**Consumer Logs**
|
||||
<!-- Capture logs from 10 minutes: `log show --predicate 'sender BEGINSWITH "xc"' --style compact --info --debug -last 10m` -->
|
||||
|
||||
<details>
|
||||
<pre> [REPLACE THIS WITH YOUR INFORMATION] </pre>
|
||||
</details>
|
||||
|
||||
**Pods/Carthage file**
|
||||
<!-- Delete if you don't use CocoaPods or Carthage -->
|
||||
|
||||
<details>
|
||||
<pre> [REPLACE THIS WITH YOUR INFORMATION] </pre>
|
||||
</details>
|
||||
|
||||
**Environment**
|
||||
|
||||
* **XCRemoteCache:** X.Y.Z
|
||||
* **cocoapods-xcremotecache:** X.Y.Z <!-- check with `gem list cocoapods-xcremotecache` >
|
||||
* **HTTP cache server:** ... <!-- e.g. demo docker, nginx, AWS etc. >
|
||||
* **Xcode:** X.Y.Z
|
||||
|
||||
**Post build stats**
|
||||
<!--
|
||||
To capture build statistics:
|
||||
* call `xcprepare stats --reset` (or `XCRC/xcprepare stats --reset` for CocoaPods)
|
||||
* Build a project in Xcode
|
||||
* `xcprepare stats` (or `XCRC/xcprepare stats` for CocoaPods)
|
||||
-->
|
||||
|
||||
<details>
|
||||
<pre> [REPLACE THIS WITH YOUR INFORMATION] </pre>
|
||||
</details>
|
||||
|
||||
**Others**
|
||||
<!-- Anything else relevant? Operating system version, , ... -->
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
name: 📕 Documentation Issue
|
||||
about: Suggestion for a change in a documentation
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
PLEASE HELP US PROCESS GITHUB ISSUES FASTER BY PROVIDING THE FOLLOWING INFORMATION.
|
||||
-->
|
||||
|
||||
**A suggestion**
|
||||
<!-- Describe how could the documentation be improved. -->
|
||||
|
||||
**Which file, section, line**
|
||||
<!-- Provide a section it relates (if exist). -->
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: 🙏 Future Request
|
||||
about: Suggestion for an improvement, either behaviour or implementation
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
PLEASE HELP US PROCESS GITHUB ISSUES FASTER BY PROVIDING THE FOLLOWING INFORMATION.
|
||||
-->
|
||||
|
||||
|
||||
**Expected/desired behavior**
|
||||
<!-- Describe what the desired behavior would be. -->
|
||||
|
||||
**Relevant integration setup**
|
||||
|
||||
[ ] CocoaPods cocoapods-xcremotecache plugin
|
||||
[ ] Automatic integration using `xcprepare integrate ...`
|
||||
[ ] Manual integration
|
||||
[ ] Carthage
|
||||
@@ -9,11 +9,13 @@ jobs:
|
||||
- uses: actions/checkout@v1
|
||||
- name: SwiftLint
|
||||
uses: norio-nomura/action-swiftlint@3.1.0
|
||||
with:
|
||||
args: --strict
|
||||
|
||||
macOS:
|
||||
runs-on: macOS-latest
|
||||
runs-on: macos-12
|
||||
env:
|
||||
XCODE_VERSION: ${{ '12.4' }}
|
||||
XCODE_VERSION: ${{ '13.3.1' }}
|
||||
steps:
|
||||
- name: Select Xcode
|
||||
run: "sudo xcode-select -s /Applications/Xcode_$XCODE_VERSION.app"
|
||||
@@ -23,3 +25,5 @@ jobs:
|
||||
run: rake build[release]
|
||||
- name: Test
|
||||
run: rake test
|
||||
- name: E2ETests
|
||||
run: rake e2e_only
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
name: Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
docs:
|
||||
runs-on: macos-12
|
||||
env:
|
||||
XCODE_VERSION: ${{ '13.3.1' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Select Xcode
|
||||
run: "sudo xcode-select -s /Applications/Xcode_$XCODE_VERSION.app"
|
||||
- name: "Generate documentation"
|
||||
run: "swift package --allow-writing-to-directory ./docs generate-documentation --target XCRemoteCache --disable-indexing --transform-for-static-hosting --output-path ./docs --hosting-base-path XCRemoteCache/"
|
||||
- name: Deploy GH-pages
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./docs
|
||||
keep_files: false
|
||||
@@ -6,9 +6,9 @@ on:
|
||||
jobs:
|
||||
macOS:
|
||||
name: Add macOS binaries to release
|
||||
runs-on: macOS-latest
|
||||
runs-on: macos-12
|
||||
env:
|
||||
XCODE_VERSION: ${{ '12.4' }}
|
||||
XCODE_VERSION: ${{ '13.3.1' }}
|
||||
steps:
|
||||
- name: Select Xcode
|
||||
run: "sudo xcode-select -s /Applications/Xcode_$XCODE_VERSION.app"
|
||||
@@ -44,3 +44,29 @@ jobs:
|
||||
file_glob: true
|
||||
tag: ${{ github.ref }}
|
||||
overwrite: true
|
||||
cocoapods:
|
||||
name: Publish CocoaPods plugin
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
defaults:
|
||||
run:
|
||||
working-directory: cocoapods-plugin
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.0'
|
||||
- run: bundle install
|
||||
- name: Publish to RubyGems
|
||||
run: |
|
||||
mkdir -p $HOME/.gem
|
||||
touch $HOME/.gem/credentials
|
||||
chmod 0600 $HOME/.gem/credentials
|
||||
printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
|
||||
gem build *.gemspec
|
||||
CURRENT_VERSION=$(gem list cocoapods-xcremotecache --remote -q | sed 's/[^0-9\.]//g')
|
||||
[ -f cocoapods-xcremotecache-$CURRENT_VERSION.gem ] && echo "Version $CURRENT_VERSION already exists" || gem push *.gem
|
||||
env:
|
||||
GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}"
|
||||
|
||||
+7
-2
@@ -1,8 +1,13 @@
|
||||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
*.xcodeproj/
|
||||
*.xcworkspace/
|
||||
DerivedData
|
||||
/.swiftpm/
|
||||
releases
|
||||
tmp/
|
||||
tmp/
|
||||
.idea/
|
||||
xcuserdata
|
||||
*.gem
|
||||
Pods/
|
||||
|
||||
+22
-1
@@ -63,6 +63,8 @@ excluded:
|
||||
- docs/
|
||||
- fastlane/
|
||||
- DerivedData/
|
||||
- e2eTests/XCRemoteCacheSample/Pods
|
||||
- e2eTests/StandaloneSampleApp
|
||||
|
||||
attributes:
|
||||
always_on_same_line:
|
||||
@@ -87,6 +89,25 @@ file_header:
|
||||
\/\/ Created by .*? on .*\.
|
||||
\/\/ Copyright © \d{4} .*\. All rights reserved\.
|
||||
\/\/
|
||||
required_pattern: |
|
||||
\/\/ Copyright \(c\) \d{4} 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\.
|
||||
force_cast: warning
|
||||
force_try: warning
|
||||
implicit_getter: warning
|
||||
@@ -123,6 +144,6 @@ custom_rules:
|
||||
severity: warning
|
||||
trailing_dot_in_comments:
|
||||
name: "Trailing dot in comments"
|
||||
regex: '^[ ]*///?[^\n]*\.\n'
|
||||
regex: '^(?!\/\/\ Copyright\ \(c\)\ \d{4}\ Spotify AB\.|\/\/\ under\ the\ License\.)[ ]*///?[^\n]*\.\n'
|
||||
message: "There shouldn't be trailing dot in comments"
|
||||
severity: warning
|
||||
|
||||
+19
-10
@@ -3,16 +3,16 @@
|
||||
"pins": [
|
||||
{
|
||||
"package": "AEXML",
|
||||
"repositoryURL": "https://github.com/tadija/AEXML",
|
||||
"repositoryURL": "https://github.com/tadija/AEXML.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "8623e73b193386909566a9ca20203e33a09af142",
|
||||
"version": "4.5.0"
|
||||
"revision": "38f7d00b23ecd891e1ee656fa6aeebd6ba04ecc3",
|
||||
"version": "4.6.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "PathKit",
|
||||
"repositoryURL": "https://github.com/kylef/PathKit",
|
||||
"repositoryURL": "https://github.com/kylef/PathKit.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "73f8e9dca9b7a3078cb79128217dc8f2e585a511",
|
||||
@@ -37,13 +37,22 @@
|
||||
"version": "0.0.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "SwiftDocCPlugin",
|
||||
"repositoryURL": "https://github.com/apple/swift-docc-plugin",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "3303b164430d9a7055ba484c8ead67a52f7b74f6",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "XcodeProj",
|
||||
"repositoryURL": "https://github.com/tuist/XcodeProj.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "0b18c3e7a10c241323397a80cb445051f4494971",
|
||||
"version": "8.0.0"
|
||||
"revision": "c75c3acc25460195cfd203a04dde165395bf00e0",
|
||||
"version": "8.7.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -51,8 +60,8 @@
|
||||
"repositoryURL": "https://github.com/jpsim/Yams.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "53741ba55ecca5c7149d8c9f810913ec80845c69",
|
||||
"version": "3.0.0"
|
||||
"revision": "00c403debcd0a007b854bb35e598466207a2d58c",
|
||||
"version": "5.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -60,8 +69,8 @@
|
||||
"repositoryURL": "https://github.com/marmelroy/Zip.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "80b1c3005ee25b4c7ce46c4029ac3347e8d5e37e",
|
||||
"version": "2.0.0"
|
||||
"revision": "67fa55813b9e7b3b9acee9c0ae501def28746d76",
|
||||
"version": "2.1.2"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
+13
-6
@@ -1,4 +1,5 @@
|
||||
// swift-tools-version:5.1
|
||||
// swift-tools-version:5.3
|
||||
// swiftlint:disable:previous file_header
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package
|
||||
|
||||
import PackageDescription
|
||||
@@ -12,10 +13,11 @@ let package = Package(
|
||||
.executable(name: "xcprebuild", targets: ["xcprebuild"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/marmelroy/Zip.git", from: "2.0.0"),
|
||||
.package(url: "https://github.com/jpsim/Yams.git", from: "3.0.0"),
|
||||
.package(url: "https://github.com/marmelroy/Zip.git", from: "2.1.2"),
|
||||
.package(url: "https://github.com/jpsim/Yams.git", from: "5.0.0"),
|
||||
.package(url: "https://github.com/apple/swift-argument-parser", from: "0.0.1"),
|
||||
.package(url: "https://github.com/tuist/XcodeProj.git", from: "8.0.0"),
|
||||
.package(url: "https://github.com/tuist/XcodeProj.git", from: "8.7.1"),
|
||||
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
@@ -49,14 +51,19 @@ let package = Package(
|
||||
name: "xcld",
|
||||
dependencies: ["XCRemoteCache"]
|
||||
),
|
||||
.target(
|
||||
name: "xcldplusplus",
|
||||
dependencies: ["XCRemoteCache"]
|
||||
),
|
||||
.target(
|
||||
// Wrapper target that builds all binaries but does nothing in runtime
|
||||
name: "Aggregator",
|
||||
dependencies: ["xcprebuild", "xcswiftc", "xclibtool", "xcpostbuild", "xcprepare", "xcld"]
|
||||
dependencies: ["xcprebuild", "xcswiftc", "xclibtool", "xcpostbuild", "xcprepare", "xcld", "xcldplusplus"]
|
||||
),
|
||||
.testTarget(
|
||||
name: "XCRemoteCacheTests",
|
||||
dependencies: ["XCRemoteCache"]
|
||||
dependencies: ["XCRemoteCache"],
|
||||
resources: [.copy("TestData")]
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
<p align="center">
|
||||
<img src="docs/img/logo.png" width="75%">
|
||||
<img src="docs/img/logo.png#gh-light-mode-only" width="75%">
|
||||
<img src="docs/img/logo-dark.png#gh-dark-mode-only" width="75%">
|
||||
</p>
|
||||
|
||||
_XCRemoteCache is a remote cache tool for Xcode projects. It reuses target artifacts generated on a remote machine, served from a simple REST server._
|
||||
|
||||
[](https://github.com/spotify/XCRemoteCache/workflows/CI/badge.svg)
|
||||
[](LICENSE)
|
||||
[](https://slackin.spotify.com)
|
||||
[](https://github.com/spotify/XCRemoteCache/workflows/Docs/badge.svg)
|
||||
|
||||
- [How and Why?](#how-and-why-)
|
||||
- [How and Why?](#how-and-why)
|
||||
* [Accurate target input files](#accurate-target-input-files)
|
||||
+ [New file added to the target](#new-file-added-to-the-target)
|
||||
* [Debug symbols](#debug-symbols)
|
||||
@@ -36,10 +39,14 @@ _XCRemoteCache is a remote cache tool for Xcode projects. It reuses target artif
|
||||
* [Amazon S3 and Google Cloud Storage](#amazon-s3-and-google-cloud-storage)
|
||||
- [CocoaPods plugin](#cocoapods-plugin)
|
||||
- [Requirements](#requirements)
|
||||
- [Apple silicon support](#apple-silicon-support)
|
||||
* [Artifacts per architecture (Recommended)](#artifacts-per-architecture-recommended)
|
||||
* [Fat artifacts](#fat-artifacts)
|
||||
- [Limitations](#limitations)
|
||||
- [FAQ](#faq)
|
||||
- [Development](#development)
|
||||
- [Release](#release)
|
||||
* [Releasing CocoaPods plugin](#releasing-cocoapods-plugin)
|
||||
* [Building release package](#building-release-package)
|
||||
- [Contributing](#contributing)
|
||||
- [Code of conduct](#code-of-conduct)
|
||||
@@ -50,7 +57,7 @@ _XCRemoteCache is a remote cache tool for Xcode projects. It reuses target artif
|
||||
|
||||
The caching mechanism is based on remote artifacts that should be generated and uploaded to the cache server for each commit on a `master` branch, preferably as a part of CI/CD step. Xcode products are not portable between different Xcode versions, each XCRemoteCache artifact is linked with a specific Xcode build number that generated it. To support multiple Xcode versions, artifacts generation should happen for each Xcode version.
|
||||
|
||||
The artifact resue flow is as follows: XCRemoteCache performs a target precheck (aka prebuild) and if a fingerprint for local sources matches the one computed on a generation side, several compilation steps wrappers (e.g. `xcswiftc`, `xccc`, `xclibtool`) mock corresponding compilation step(s) and linking (or archiving) moves the cached build artifact to the expected location.
|
||||
The artifact reuse flow is as follows: XCRemoteCache performs a target precheck (aka prebuild) and if a fingerprint for local sources matches the one computed on a generation side, several compilation steps wrappers (e.g. `xcswiftc`, `xccc`, `xclibtool`) mock corresponding compilation step(s) and linking (or archiving) moves the cached build artifact to the expected location.
|
||||
|
||||
> Multiple commits that have the same target sources reuse artifact package on a remote server.
|
||||
|
||||
@@ -134,7 +141,7 @@ xcremotecache/xcprepare integrate --input <yourProject.xcodeproj> --mode consume
|
||||
| Argument | Description | Default | Required |
|
||||
| ------------- | ------------- | ------------- | ------------- |
|
||||
| `--input` | .xcodeproj location | N/A | ✅ |
|
||||
| `--mode` | mode. Supported values: `consumer`, `producer` | N/A | ✅ |
|
||||
| `--mode` | mode. Supported values: `consumer`, `producer`, `producer-fast`(experimental) | N/A | ✅ |
|
||||
| `--targets-include` | comma-separated list of targets to integrate XCRemoteCache. | `""` | ⬜️ |
|
||||
| `--targets-exclude` | comma-separated list of targets to not integrate XCRemoteCache. Takes priority over --targets-include. | `""` | ⬜️ |
|
||||
| `--configurations-include` | comma-separated list of configurations to integrate XCRemoteCache. | `""` | ⬜️ |
|
||||
@@ -189,6 +196,9 @@ 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`)
|
||||
* `LDPLUSPLUS` - location of `xcldplusplus` (e.g. `xcremotecache/xcldplusplus`)
|
||||
* `XCRC_PLATFORM_PREFERRED_ARCH` - `$(LINK_FILE_LIST_$(CURRENT_VARIANT)_$(PLATFORM_PREFERRED_ARCH):dir:standardizepath:file:default=arm64)`
|
||||
* `SWIFT_USE_INTEGRATED_DRIVER` - `NO` (required in Xcode 14.0+)
|
||||
|
||||
<details>
|
||||
<summary>Screenshot</summary>
|
||||
@@ -208,8 +218,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,7 +267,7 @@ $ xcremotecache/xcprepare mark --configuration Debug --platform iphonesimulator
|
||||
|
||||
That command creates an empty file on a remote server which informs that for given sha, configuration, platform, Xcode versions etc. all artifacts are available.
|
||||
|
||||
_Note that for the `producer` mode, the prebuild build phase and `xccc`, `xcld`, `xclibtool` wrappers become no-op, so it is recommended to not add them for the `producer` mode._
|
||||
_Note that for the `producer` mode, the prebuild build phase and `xccc`, `xcld`, `xcldplusplus`, `xclibtool` wrappers become no-op, so it is recommended to not add them for the `producer` mode._
|
||||
|
||||
## A full list of configuration parameters:
|
||||
|
||||
@@ -281,13 +291,14 @@ _Note that for the `producer` mode, the prebuild build phase and `xccc`, `xcld`,
|
||||
| `cache_commit_history` | Number of historical git commits to look for cache artifacts | `10` | ⬜️ |
|
||||
| `source_root` | Source root of the Xcode project | `""` | ⬜️ |
|
||||
| `fingerprint_override_extension` | Fingerprint override extension (sample override `Module.swiftmodule/x86_64.swiftmodule.md5`) | `md5` | ⬜️ |
|
||||
| `extra_configuration_file` | Configuration file that overrides project configuration | `user.rcinfo` | ⬜️ |
|
||||
| `extra_configuration_file` | Configuration file that overrides project configuration (this property can be overriden multiple times in different files to chain extra configuration files) | `user.rcinfo` | ⬜️ |
|
||||
| `publishing_sha` | Custom commit sha to publish artifact (producer only) | `nil` | ⬜️ |
|
||||
| `artifact_maximum_age` | Maximum age in days HTTP response should be locally cached before being evicted | `30` | ⬜️ |
|
||||
| `custom_fingerprint_envs` | Extra ENV keys that should be convoluted into the environment fingerprint | `[]` | ⬜️ |
|
||||
| `stats_dir` | Directory where all XCRemoteCache statistics (e.g. counters) are stored | `~/.xccache` | ⬜️ |
|
||||
| `download_retries` | Number of retries for download requests | `0` | ⬜️ |
|
||||
| `upload_retries` | Number of retries for upload requests | `3` | ⬜️ |
|
||||
| `retry_delay` | Delay between retries in seconds | `10` | ⬜️ |
|
||||
| `request_custom_headers` | Dictionary of extra HTTP headers for all remote server requests | `[]` | ⬜️ |
|
||||
| `thin_target_mock_filename` | Filename (without an extension) of the compilation input file that is used as a fake compilation for the forced-cached target (aka thin target) | `standin` | ⬜️ |
|
||||
| `focused_targets` | A list of all targets that are not thinned. If empty, all targets are meant to be non-thin | `[]` | ⬜️ |
|
||||
@@ -298,11 +309,17 @@ _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_security_token` | Temporary security token provided by the AWS Security Token Service. | `nil` | ⬜️ |
|
||||
| `aws_region` | Region for AWS V4 Signature Authorization. E.g. `eu`. | `""` | ⬜️ |
|
||||
| `aws_service` | Service for AWS V4 Signature Authorization. E.g. `storage`. | `""` | ⬜️ |
|
||||
|
||||
| `out_of_band_mappings` | A dictionary of files path remapping that should be applied to make it absolute path agnostic on a list of dependencies. Useful if a project refers files out of repo root, either compilation files or precompiled dependencies. Keys represent generic replacement and values are substrings that should be replaced. Example: for mapping `["COOL_LIBRARY": "/CoolLibrary"]` `/CoolLibrary/main.swift`will be represented as `$(COOL_LIBRARY)/main.swift`). Warning: remapping order is not-deterministic so avoid remappings with multiple matchings. | `[:]` | ⬜️ |
|
||||
| `disable_certificate_verification` | A Boolean value that opts-in SSL certificate validation is disabled | `false` | ⬜️ |
|
||||
| `disable_vfs_overlay` | A feature flag to disable virtual file system overlay support (temporary) | `false` | ⬜️ |
|
||||
| `custom_rewrite_envs` | A list of extra ENVs that should be used as placeholders in the dependency list. ENV rewrite process is optimistic - does nothing if an ENV is not defined in the pre/postbuild process. | `[]` | ⬜️ |
|
||||
| `irrelevant_dependencies_paths` | Regexes of files that should not be included in a list of dependencies. Warning! Add entries here with caution - excluding dependencies that are relevant might lead to a target overcaching. The regex can match either partially or fully the filepath, e.g. `\\.modulemap$` will exclude all `.modulemap` files. | `[]` | ⬜️ |
|
||||
|
||||
## Backend cache server
|
||||
|
||||
@@ -340,6 +357,8 @@ XCRemoteCache supports Amazon S3 and Google Cloud Storage buckets to be used as
|
||||
|
||||
To set it up use the configuration parameters `aws_secret_key`, `aws_access_key`, `aws_region`, and `aws_service` in the `.rcinfo` file. Specify the URL to the bucket in cache-addresses field in the same file.
|
||||
|
||||
XCRemoteCache also supports [AWS Temporary Access Keys](https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html#temporary-access-keys). Use additional `aws_security_token` parameter combined with `aws_secret_key`, `aws_access_key` to set it up. [This page](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html) describes how to receive a security token.
|
||||
|
||||
Example
|
||||
```yaml
|
||||
...
|
||||
@@ -358,6 +377,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
|
||||
@@ -368,12 +412,14 @@ Head over to our [cocoapods-plugin](cocoapods-plugin/README.md) docs to see how
|
||||
* Recommended: multi-targets Xcode project
|
||||
* Recommended: do not use fast-forward PR strategy (use merge or squash instead)
|
||||
* Recommended: avoid `DWARF with dSYM File` "Debug Information Format" build setting. Use `DWARF` instead
|
||||
* Recommended: avoid having a symbolic link in the source root (e.g. placing a project in `/tmp`)
|
||||
|
||||
## Limitations
|
||||
|
||||
* Swift Package Manager (SPM) projects are not supported
|
||||
* Swift Package Manager (SPM) dependencies are not supported. _Because SPM does not allow customizing Build Settings, XCRemoteCache cannot specify `clang` and `swiftc` wrappers that control if the local compilation should be skipped (cache hit) or not (cache miss)_
|
||||
* Filenames with `_vers.c` suffix are reserved and cannot be used as a source file
|
||||
* All compilation files should be referenced via the git repo root. Referencing `/AbsolutePath/someOther.swift` or `../../someOther.swift` that resolve to the location outside of the git repo root is prohibited.
|
||||
* The new Swift driver (introduced by default in Xcode 14.0) is not supported and has to be disabled when using XCRemoteCache
|
||||
|
||||
## FAQ
|
||||
|
||||
@@ -388,6 +434,12 @@ Follow the [Development](docs/Development.md) guide. It has all the information
|
||||
To release a version, in [Releases](https://github.com/spotify/XCRemoteCache/releases) draft a new release with `v0.3.0{-rc0}` tag format.
|
||||
Packages with binaries will be automatically uploaded to the GitHub [Releases](https://github.com/spotify/XCRemoteCache/releases) page.
|
||||
|
||||
### Releasing CocoaPods plugin
|
||||
|
||||
Bump a gem version defined in [gem_version.rb](cocoapods-plugin/lib/cocoapods-xcremotecache/gem_version.rb) and create a new release described above.
|
||||
|
||||
A plugin is automatically uploaded to [RubyGems](https://rubygems.org/gems/cocoapods-xcremotecache) if a given version doesn't exist yet.
|
||||
|
||||
### Building release package
|
||||
|
||||
To build a release zip package for a single platform (e.g. `x86_64-apple-macosx`, `arm64-apple-macosx`), call:
|
||||
@@ -398,6 +450,12 @@ rake 'build[release, x86_64-apple-macosx]'
|
||||
|
||||
The zip package will be generated at `releases/XCRemoteCache.zip`.
|
||||
|
||||
## Support
|
||||
|
||||
Create a [new issue](https://github.com/spotify/XCRemoteCache/issues/new) with as many details as possible.
|
||||
|
||||
Reach us at the `#xcremotecache` channel in [Slack](https://slackin.spotify.com/).
|
||||
|
||||
## Contributing
|
||||
|
||||
We feel that a welcoming community is important and we ask that you follow Spotify's
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# encoding: utf-8
|
||||
require_relative 'tasks/e2e'
|
||||
|
||||
################################
|
||||
# Rake configuration
|
||||
@@ -9,7 +10,7 @@ DERIVED_DATA_DIR = File.join('.build').freeze
|
||||
RELEASES_ROOT_DIR = File.join('releases').freeze
|
||||
|
||||
EXECUTABLE_NAME = 'XCRemoteCache'
|
||||
EXECUTABLE_NAMES = ['xclibtool', 'xcpostbuild', 'xcprebuild', 'xcprepare', 'xcswiftc', 'xcld']
|
||||
EXECUTABLE_NAMES = ['xclibtool', 'xcpostbuild', 'xcprebuild', 'xcprepare', 'xcswiftc', 'xcld', 'xcldplusplus']
|
||||
PROJECT_NAME = 'XCRemoteCache'
|
||||
|
||||
SWIFTLINT_ENABLED = true
|
||||
@@ -28,7 +29,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
|
||||
@@ -77,6 +78,12 @@ task :test do
|
||||
spm_test()
|
||||
end
|
||||
|
||||
desc 'build and run E2E tests'
|
||||
task :e2e => [:build, :e2e_only]
|
||||
|
||||
desc 'run E2E tests without building the XCRemoteCache binary'
|
||||
task :e2e_only => ['e2e:run']
|
||||
|
||||
################################
|
||||
# Helper functions
|
||||
################################
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -51,8 +51,6 @@ protocol ArtifactSwiftProductsBuilder {
|
||||
/// # {workingDir}/xccache/produced/include/#{moduleName} (if `moduleName` is defined)
|
||||
class ArtifactSwiftProductsBuilderImpl: ArtifactSwiftProductsBuilder {
|
||||
|
||||
/// List of all required swiftmodule related extensions that should be copied to the artifact
|
||||
private static let swiftmoduleExtensionsToInclude = ["swiftmodule", "swiftdoc", "swiftsourceinfo"]
|
||||
private let workingDir: URL
|
||||
private let moduleName: String?
|
||||
private let fileManager: FileManager
|
||||
|
||||
@@ -30,6 +30,7 @@ enum SwiftmoduleFileExtension: String {
|
||||
case swiftmodule
|
||||
case swiftdoc
|
||||
case swiftsourceinfo
|
||||
case swiftinterface
|
||||
}
|
||||
|
||||
extension SwiftmoduleFileExtension {
|
||||
@@ -38,5 +39,6 @@ extension SwiftmoduleFileExtension {
|
||||
.swiftmodule: .required,
|
||||
.swiftdoc: .required,
|
||||
.swiftsourceinfo: .optional,
|
||||
.swiftinterface: .optional,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ class XCLibtoolCreateUniversalBinary: XCLibtoolLogic {
|
||||
let config: XCRemoteCacheConfig
|
||||
do {
|
||||
let srcRoot: URL = URL(fileURLWithPath: fileManager.currentDirectoryPath)
|
||||
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileManager: fileManager)
|
||||
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileReader: fileManager)
|
||||
.readConfiguration()
|
||||
} catch {
|
||||
errorLog("Libtool initialization failed with error: \(error). Fallbacking to libtool")
|
||||
|
||||
@@ -33,13 +33,16 @@ enum ThinningCreatorPluginError: Error {
|
||||
/// Warning! This plugin assumes that producer's DerivedData are always cleaned before a build
|
||||
class ThinningCreatorPlugin: ArtifactCreatorPlugin {
|
||||
private let targetTempDir: URL
|
||||
private let modeMarkerPath: String
|
||||
private let dirScanner: DirScanner
|
||||
|
||||
/// Default Initializer
|
||||
/// - Parameter targetTempDir: Location of current target-specific temp dir (TARGET_TEMP_DIR)
|
||||
/// - Parameter modeMarkerPath: path of maker file that informs if a given target can reuse remote artifacts
|
||||
/// - Parameter dirScanner: scanner to access disk and read files and directories hierarchy
|
||||
init(targetTempDir: URL, dirScanner: DirScanner) {
|
||||
init(targetTempDir: URL, modeMarkerPath: String, dirScanner: DirScanner) {
|
||||
self.targetTempDir = targetTempDir
|
||||
self.modeMarkerPath = modeMarkerPath
|
||||
self.dirScanner = dirScanner
|
||||
}
|
||||
|
||||
@@ -57,31 +60,19 @@ class ThinningCreatorPlugin: ArtifactCreatorPlugin {
|
||||
let fileKey: String
|
||||
}
|
||||
let uploadedTargetArtifacts = try allURLs.compactMap { tempDir -> TargetTuple? in
|
||||
// All targets that uploaded their artifacts, have it placed in the
|
||||
// `$(TARGET_TEMP_DIR)/xccache/produced/{{fileKey}}.zip` location. Find all targets that have such a file
|
||||
|
||||
let targetGeneratedArtifactRootDir = tempDir
|
||||
.appendingPathComponent("xccache")
|
||||
.appendingPathComponent("produced")
|
||||
guard try dirScanner.itemType(atPath: targetGeneratedArtifactRootDir.path) == ItemType.dir else {
|
||||
// given target didn't generate any artifacts (e.g. it is never cached with XCRemoteCache)
|
||||
return nil
|
||||
}
|
||||
|
||||
let allFilesProduced = try dirScanner.items(at: targetGeneratedArtifactRootDir)
|
||||
let allArtifacts = allFilesProduced.filter { $0.pathExtension == "zip" }
|
||||
guard !allArtifacts.isEmpty else {
|
||||
let potentialArtifacts = try findTargetPackageZip(tempDir: tempDir)
|
||||
guard !potentialArtifacts.isEmpty else {
|
||||
// there is no generated *.zip file, so given target didn't create an artifact - it could be
|
||||
// just a helper target (like the target we integrate this plugin with)
|
||||
return nil
|
||||
}
|
||||
// Find {{fileKey}} based on the .zip file basename
|
||||
guard allArtifacts.count == 1 else {
|
||||
guard potentialArtifacts.count == 1 else {
|
||||
throw ThinningCreatorPluginError.noSingleTargetArtifactsGenerated(
|
||||
rootDir: targetGeneratedArtifactRootDir
|
||||
rootDir: tempDir
|
||||
)
|
||||
}
|
||||
let fileKey = allArtifacts[0].deletingPathExtension().lastPathComponent
|
||||
let fileKey = potentialArtifacts[0].deletingPathExtension().lastPathComponent
|
||||
// Taking target name from tempDir, which has a structures "*.build"
|
||||
let targetName = tempDir.deletingPathExtension().lastPathComponent
|
||||
return TargetTuple(targetName: targetName, fileKey: fileKey)
|
||||
@@ -96,6 +87,41 @@ class ThinningCreatorPlugin: ArtifactCreatorPlugin {
|
||||
return Dictionary(uniqueKeysWithValues: extraKeysTuples)
|
||||
}
|
||||
|
||||
private func findTargetPackageZip(tempDir: URL) throws -> [URL] {
|
||||
// Producer mode:
|
||||
// All targets that uploaded their artifacts, have it placed in the
|
||||
// `$(TARGET_TEMP_DIR)/xccache/produced/{{fileKey}}.zip` location. Find all targets that have such a file
|
||||
// ProducerFast mode:
|
||||
// If a target reused already existing artifact, it still has `$(TARGET_TEMP_DIR)/rc.enabled` marker file
|
||||
// and the reused zip is placed in:
|
||||
// `$(TARGET_TEMP_DIR)/xccache/{{fileKey}}.zip` location
|
||||
|
||||
let targetEnabledMarker = tempDir.appendingPathComponent(modeMarkerPath)
|
||||
let targetReusedArtifactRootDir = tempDir.appendingPathComponent("xccache")
|
||||
let targetGeneratedArtifactRootDir = tempDir
|
||||
.appendingPathComponent("xccache")
|
||||
.appendingPathComponent("produced")
|
||||
|
||||
let pathToDirWithZipArtifacts: URL
|
||||
// try the `.producerFast` scenario first (the artifact was not locally
|
||||
// generated but just reused from the remote cache)
|
||||
if try dirScanner.itemType(atPath: targetEnabledMarker.path) == ItemType.file {
|
||||
pathToDirWithZipArtifacts = targetReusedArtifactRootDir
|
||||
} else {
|
||||
// cover a case when a target was build locally and an artifact
|
||||
// has just been created (locally)
|
||||
guard try dirScanner.itemType(atPath: targetGeneratedArtifactRootDir.path) == ItemType.dir else {
|
||||
// given target didn't generate any artifacts (e.g. it is never cached with XCRemoteCache)
|
||||
return []
|
||||
}
|
||||
pathToDirWithZipArtifacts = targetGeneratedArtifactRootDir
|
||||
}
|
||||
|
||||
let allFilesProduced = try dirScanner.items(at: pathToDirWithZipArtifacts)
|
||||
let allArtifacts = allFilesProduced.filter { $0.pathExtension == "zip" }
|
||||
return allArtifacts
|
||||
}
|
||||
|
||||
func artifactToUpload(main: MainArtifactMeta) throws -> [Artifact] {
|
||||
return []
|
||||
}
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ class ThinningDiskSwiftcProductsGenerator: SwiftcProductsGenerator {
|
||||
destinationSwiftmodulePaths = Dictionary(
|
||||
uniqueKeysWithValues: SwiftmoduleFileExtension.SwiftmoduleExtensions
|
||||
.map { ext, _ in
|
||||
switch (ext) {
|
||||
switch ext {
|
||||
case .swiftsourceinfo:
|
||||
let dest = modulePathDir.appendingPathComponent("Project")
|
||||
.appendingPathComponent(moduleName)
|
||||
|
||||
@@ -25,7 +25,7 @@ class ThinningConsumerPlugin {
|
||||
|
||||
deinit {
|
||||
// initialised but never run plugin suggests that standard target fallbacks to the local development
|
||||
// and DerivedData still misses build artifacts.
|
||||
// and DerivedData still misses build artifacts
|
||||
guard wasRun else {
|
||||
let errorMessage = """
|
||||
\(type(of: self)) plugin has never been run, thinning cannot be supported. Verify you \
|
||||
|
||||
@@ -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()
|
||||
@@ -134,7 +153,7 @@ class Postbuild {
|
||||
// Replace all local paths to the generic ones (e.g. $SRCROOT)
|
||||
let remappers = [remapper] + creatorPlugins.compactMap(\.customPathsRemapper)
|
||||
let remapper = DependenciesRemapperComposite(remappers)
|
||||
let abstractFingerprintFiles = remapper.replace(localPaths: dependencies.map(\.path))
|
||||
let abstractFingerprintFiles = try remapper.replace(localPaths: dependencies.map(\.path))
|
||||
// TODO: use `inputs` read by dependenciesReader
|
||||
var meta = MainArtifactMeta(
|
||||
dependencies: abstractFingerprintFiles,
|
||||
|
||||
@@ -32,12 +32,15 @@ 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 {
|
||||
var mode: Mode
|
||||
var targetName: String
|
||||
var targetTempDir: URL
|
||||
var derivedFilesDir: URL
|
||||
/// Location where all compilation outputs (.o) are placed
|
||||
var compilationTempDir: URL
|
||||
var configuration: String
|
||||
@@ -64,6 +67,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,18 +77,31 @@ 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.
|
||||
/// Action type: build, indexbuild etc
|
||||
var action: BuildActionType
|
||||
let modeMarkerPath: String
|
||||
/// location of the json file that define virtual files system overlay
|
||||
/// (mappings of the virtual location file -> local file path)
|
||||
let overlayHeadersPath: URL
|
||||
/// Regexes of files that should not be included in the dependency list
|
||||
let irrelevantDependenciesPaths: [String]
|
||||
}
|
||||
|
||||
extension PostbuildContext {
|
||||
// swiftlint:disable:next function_body_length
|
||||
init(_ config: XCRemoteCacheConfig, env: [String: String]) throws {
|
||||
mode = config.mode
|
||||
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)
|
||||
derivedFilesDir = try env.readEnv(key: "DERIVED_FILE_DIR")
|
||||
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")
|
||||
@@ -106,7 +125,7 @@ extension PostbuildContext {
|
||||
dSYMPath = try env.readEnv(key: "DWARF_DSYM_FOLDER_PATH")
|
||||
.appendingPathComponent(env.readEnv(key: "DWARF_DSYM_FILE_NAME"))
|
||||
builtProductsDir = try env.readEnv(key: "BUILT_PRODUCTS_DIR")
|
||||
if let contentsFolderPath = env.readEnv(key: "CONTENTS_FOLDER_PATH") {
|
||||
if let contentsFolderPath: String = env.readEnv(key: "CONTENTS_FOLDER_PATH") {
|
||||
bundleDir = productsDir.appendingPathComponent(contentsFolderPath)
|
||||
} else {
|
||||
bundleDir = nil
|
||||
@@ -115,5 +134,9 @@ extension PostbuildContext {
|
||||
let thinFocusedTargetsString: String = env.readEnv(key: "SPT_XCREMOTE_CACHE_THINNED_TARGETS") ?? ""
|
||||
thinnedTargets = thinFocusedTargetsString.split(separator: ",").map(String.init)
|
||||
action = (try? BuildActionType(rawValue: env.readEnv(key: "ACTION"))) ?? .unknown
|
||||
modeMarkerPath = config.modeMarkerPath
|
||||
/// Note: The file has yaml extension, even it is in the json format
|
||||
overlayHeadersPath = targetTempDir.appendingPathComponent("all-product-headers.yaml")
|
||||
irrelevantDependenciesPaths = config.irrelevantDependenciesPaths
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
// swiftlint:disable type_body_length
|
||||
/// Checks current mode from a configuration and based on that:
|
||||
/// * triggers build completion
|
||||
/// * triggers uploading artifacts to the server for a 'producer' mode
|
||||
@@ -33,8 +34,9 @@ public class XCPostbuild {
|
||||
let context: PostbuildContext
|
||||
let cacheHitLogger: CacheHitLogger
|
||||
do {
|
||||
config = try XCRemoteCacheConfigReader(env: env, fileManager: fileManager).readConfiguration()
|
||||
config = try XCRemoteCacheConfigReader(env: env, fileReader: fileManager).readConfiguration()
|
||||
context = try PostbuildContext(config, env: env)
|
||||
updateProcessTag(context.targetName)
|
||||
let counterFactory: FileStatsCoordinator.CountersFactory = { file, count in
|
||||
ExclusiveFileCounter(ExclusiveFile(file, mode: .override), countersCount: count)
|
||||
}
|
||||
@@ -66,9 +68,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 envsRemapper = try PathDependenciesRemapperFactory().build(
|
||||
orderKeys: DependenciesMapping.rewrittenEnvs + config.customRewriteEnvs,
|
||||
envs: env,
|
||||
customMappings: config.outOfBandMappings
|
||||
)
|
||||
let envFingerprint = try EnvironmentFingerprintGenerator(
|
||||
configuration: config,
|
||||
@@ -85,6 +88,7 @@ public class XCPostbuild {
|
||||
algorithm: MD5Algorithm()
|
||||
)
|
||||
let organizer = ZipArtifactOrganizer(targetTempDir: context.targetTempDir, fileManager: fileManager)
|
||||
let metaWriter = JsonMetaWriter(fileWriter: fileManager, pretty: config.prettifyMetaFiles)
|
||||
let artifactCreator = BuildArtifactCreator(
|
||||
buildDir: context.productsDir,
|
||||
tempDir: context.targetTempDir,
|
||||
@@ -92,6 +96,7 @@ public class XCPostbuild {
|
||||
moduleName: context.moduleName,
|
||||
modulesFolderPath: context.modulesFolderPath,
|
||||
dSYMPath: context.dSYMPath,
|
||||
metaWriter: metaWriter,
|
||||
fileManager: fileManager
|
||||
)
|
||||
let dirAccessor = DirAccessorComposer(
|
||||
@@ -109,6 +114,7 @@ public class XCPostbuild {
|
||||
awsV4Signature = AWSV4Signature(
|
||||
secretKey: config.AWSSecretKey,
|
||||
accessKey: config.AWSAccessKey,
|
||||
securityToken: config.AWSSecurityToken,
|
||||
region: config.AWSRegion,
|
||||
service: config.AWSService,
|
||||
date: Date(timeIntervalSinceNow: 0)
|
||||
@@ -117,6 +123,7 @@ public class XCPostbuild {
|
||||
let networkClient = NetworkClientImpl(
|
||||
session: sessionFactory.build(),
|
||||
retries: config.uploadRetries,
|
||||
retryDelay: config.retryDelay,
|
||||
fileManager: fileManager,
|
||||
awsV4Signature: awsV4Signature
|
||||
)
|
||||
@@ -142,12 +149,32 @@ public class XCPostbuild {
|
||||
fileDependeciesReaderFactory: fileReaderFactory,
|
||||
dirScanner: fileManager
|
||||
)
|
||||
var remappers: [DependenciesRemapper] = []
|
||||
if !config.disableVFSOverlay {
|
||||
// As the PostbuildContext assumes file location and filename (`all-product-headers.yaml`)
|
||||
// do not fail in case of a missing headers overlay file. In the future, all overlay files could be
|
||||
// captured from the swiftc invocation similarly is stored in the `history.compile`
|
||||
// for the consumer mode
|
||||
let overlayReader = JsonOverlayReader(
|
||||
context.overlayHeadersPath,
|
||||
mode: .bestEffort,
|
||||
fileReader: fileManager
|
||||
)
|
||||
let overlayRemapper = OverlayDependenciesRemapper(
|
||||
overlayReader: overlayReader
|
||||
)
|
||||
remappers.append(overlayRemapper)
|
||||
}
|
||||
remappers.append(envsRemapper)
|
||||
let pathRemapper = DependenciesRemapperComposite(remappers)
|
||||
let dependencyProcessor = DependencyProcessorImpl(
|
||||
xcode: context.xcodeDir,
|
||||
product: context.productsDir,
|
||||
source: context.srcRoot,
|
||||
intermediate: context.targetTempDir,
|
||||
bundle: context.bundleDir
|
||||
derivedFiles: context.derivedFilesDir,
|
||||
bundle: context.bundleDir,
|
||||
skippedRegexes: context.irrelevantDependenciesPaths
|
||||
)
|
||||
// Override fingerprints for all produced '.swiftmodule' files
|
||||
let fingerprintOverrideManager = FingerprintOverrideManagerImpl(
|
||||
@@ -205,9 +232,10 @@ public class XCPostbuild {
|
||||
worker: DispatchGroupParallelizationWorker(qos: .userInitiated)
|
||||
)
|
||||
consumerPlugins.append(thinningPlugin)
|
||||
case .producer:
|
||||
case .producer, .producerFast:
|
||||
let thinningPlugin = ThinningCreatorPlugin(
|
||||
targetTempDir: context.targetTempDir,
|
||||
modeMarkerPath: context.modeMarkerPath,
|
||||
dirScanner: fileManager
|
||||
)
|
||||
creatorPlugins.append(thinningPlugin)
|
||||
@@ -229,6 +257,7 @@ public class XCPostbuild {
|
||||
dSYMOrganizer: dSYMOrganizer,
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: creatorPlugins,
|
||||
consumerPlugins: consumerPlugins
|
||||
)
|
||||
@@ -243,11 +272,22 @@ public class XCPostbuild {
|
||||
try postbuildAction.deleteFingerprintOverrides()
|
||||
}
|
||||
|
||||
|
||||
// Trigger uploading the artifact
|
||||
if context.mode == .producer {
|
||||
switch (context.mode, try modeController.isEnabled(), context.remoteCommit) {
|
||||
case (.producerFast, true, .available(commit: let commitToReuse)):
|
||||
// Upload only updated meta. Artifact zip is already on a remote server
|
||||
let referenceCommit = try config.publishingSha ?? gitClient.getCurrentSha()
|
||||
let metaData = try remoteNetworkClient.fetch(.meta(commit: commitToReuse))
|
||||
let meta = try metaReader.read(data: metaData)
|
||||
try postbuildAction.performMetaUpload(meta: meta, for: referenceCommit)
|
||||
case (.producer, _, _), (.producerFast, _, _):
|
||||
// Generate artifacts and upload to the remote server for a reference sha
|
||||
let referenceCommit = try config.publishingSha ?? gitClient.getCurrentSha()
|
||||
try postbuildAction.performBuildUpload(for: referenceCommit)
|
||||
default:
|
||||
// Consumer does not upload anything
|
||||
break
|
||||
}
|
||||
|
||||
let executableURL = context.productsDir.appendingPathComponent(context.executablePath)
|
||||
@@ -261,9 +301,8 @@ public class XCPostbuild {
|
||||
} else {
|
||||
try postbuildAction.performBuildCleanup()
|
||||
try cacheHitLogger.logMiss()
|
||||
// Producer mode doesn't use cached artifacts so modeController is not enabled. If producer
|
||||
// reaches this point, there were no issues with publishing
|
||||
let actionName = context.mode == .producer ? "Published" : "Disabled"
|
||||
// If producers reach this point, there were no issues with publishing
|
||||
let actionName = context.mode == .consumer ? "Disabled" : "Published"
|
||||
printToUser("\(actionName) remote cache for \(context.targetName)")
|
||||
}
|
||||
} catch PluginError.unrecoverableError(let error) {
|
||||
@@ -286,3 +325,4 @@ public class XCPostbuild {
|
||||
}
|
||||
}
|
||||
}
|
||||
// swiftlint:enable type_body_length
|
||||
|
||||
@@ -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
|
||||
@@ -62,7 +63,9 @@ class Prebuild {
|
||||
do {
|
||||
let metaData = try networkClient.fetch(.meta(commit: commit))
|
||||
let meta = try metaReader.read(data: metaData)
|
||||
let localDependencies = remapper.replace(genericPaths: meta.dependencies).map(URL.init(fileURLWithPath:))
|
||||
let localDependencies = try remapper.replace(
|
||||
genericPaths: meta.dependencies
|
||||
).map(URL.init(fileURLWithPath:))
|
||||
let localFingerprint = try generateFingerprint(for: localDependencies)
|
||||
if localFingerprint.raw != meta.rawFingerprint {
|
||||
if context.forceCached {
|
||||
|
||||
@@ -43,6 +43,9 @@ public struct PrebuildContext {
|
||||
let targetName: String
|
||||
/// List of all targets to downloaded from the thinning aggregation target
|
||||
var thinnedTargets: [String]?
|
||||
/// location of the json file that define virtual files system overlay
|
||||
/// (mappings of the virtual location file -> local file path)
|
||||
let overlayHeadersPath: URL
|
||||
}
|
||||
|
||||
extension PrebuildContext {
|
||||
@@ -64,5 +67,7 @@ extension PrebuildContext {
|
||||
self.targetName = targetName
|
||||
let thinFocusedTargetsString: String? = env.readEnv(key: "SPT_XCREMOTE_CACHE_THINNED_TARGETS")
|
||||
thinnedTargets = thinFocusedTargetsString?.split(separator: ",").map(String.init)
|
||||
/// Note: The file has yaml extension, even it is in the json format
|
||||
overlayHeadersPath = targetTempDir.appendingPathComponent("all-product-headers.yaml")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,8 +29,9 @@ public class XCPrebuild {
|
||||
let config: XCRemoteCacheConfig
|
||||
let context: PrebuildContext
|
||||
do {
|
||||
config = try XCRemoteCacheConfigReader(env: env, fileManager: fileManager).readConfiguration()
|
||||
config = try XCRemoteCacheConfigReader(env: env, fileReader: fileManager).readConfiguration()
|
||||
context = try PrebuildContext(config, env: env)
|
||||
updateProcessTag(context.targetName)
|
||||
} catch {
|
||||
// Fatal error:
|
||||
exit(1, "FATAL: Prebuild initialization failed with error: \(error)")
|
||||
@@ -77,6 +78,10 @@ public class XCPrebuild {
|
||||
exit(0)
|
||||
}
|
||||
|
||||
let compilationHistoryOrganizer = CompilationHistoryFileOrganizer(
|
||||
context.compilationHistoryFile,
|
||||
fileManager: fileManager
|
||||
)
|
||||
do {
|
||||
let envFingerprint = try EnvironmentFingerprintGenerator(
|
||||
configuration: config,
|
||||
@@ -95,6 +100,7 @@ public class XCPrebuild {
|
||||
awsV4Signature = AWSV4Signature(
|
||||
secretKey: config.AWSSecretKey,
|
||||
accessKey: config.AWSAccessKey,
|
||||
securityToken: config.AWSSecurityToken,
|
||||
region: config.AWSRegion,
|
||||
service: config.AWSService,
|
||||
date: Date(timeIntervalSinceNow: 0)
|
||||
@@ -103,6 +109,7 @@ public class XCPrebuild {
|
||||
let networkClient = NetworkClientImpl(
|
||||
session: sessionFactory.build(),
|
||||
retries: config.downloadRetries,
|
||||
retryDelay: config.retryDelay,
|
||||
fileManager: fileManager,
|
||||
awsV4Signature: awsV4Signature
|
||||
)
|
||||
@@ -115,10 +122,27 @@ 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 envsRemapper = try PathDependenciesRemapperFactory().build(
|
||||
orderKeys: DependenciesMapping.rewrittenEnvs + config.customRewriteEnvs,
|
||||
envs: env,
|
||||
customMappings: config.outOfBandMappings
|
||||
)
|
||||
var remappers: [DependenciesRemapper] = []
|
||||
if !config.disableVFSOverlay {
|
||||
// As PrebuildContext assumes file location and its filename (`all-product-headers.yaml`)
|
||||
// do not fail in case of a missing headers overlay file
|
||||
let overlayReader = JsonOverlayReader(
|
||||
context.overlayHeadersPath,
|
||||
mode: .bestEffort,
|
||||
fileReader: fileManager
|
||||
)
|
||||
let overlayRemapper = OverlayDependenciesRemapper(
|
||||
overlayReader: overlayReader
|
||||
)
|
||||
remappers.append(overlayRemapper)
|
||||
}
|
||||
remappers.append(envsRemapper)
|
||||
let pathRemapper = DependenciesRemapperComposite(remappers)
|
||||
let filesFingerprintGenerator = FingerprintAccumulatorImpl(
|
||||
algorithm: MD5Algorithm(),
|
||||
fileManager: fileManager
|
||||
@@ -129,7 +153,6 @@ public class XCPrebuild {
|
||||
algorithm: MD5Algorithm()
|
||||
)
|
||||
let organizer = ZipArtifactOrganizer(targetTempDir: context.targetTempDir, fileManager: fileManager)
|
||||
let compilationHistoryOrganizer = CompilationHistoryFileOrganizer(context.compilationHistoryFile, fileManager: fileManager)
|
||||
let metaReader = JsonMetaReader(fileAccessor: fileManager)
|
||||
var consumerPlugins: [ArtifactConsumerPrebuildPlugin] = []
|
||||
|
||||
@@ -167,7 +190,6 @@ public class XCPrebuild {
|
||||
case .compatible(localDependencies: let dependencies):
|
||||
// TODO: pass `allowedInputFiles` observed in the build time
|
||||
try modeController.enable(allowedInputFiles: dependencies, dependencies: dependencies)
|
||||
compilationHistoryOrganizer.reset()
|
||||
}
|
||||
} catch {
|
||||
disableRemoteCache(
|
||||
@@ -175,6 +197,7 @@ public class XCPrebuild {
|
||||
errorMessage: "Prebuild step failed with error: \(error)"
|
||||
)
|
||||
}
|
||||
compilationHistoryOrganizer.reset()
|
||||
}
|
||||
|
||||
private func disableRemoteCache(modeController: PhaseCacheModeController, errorMessage: String?) {
|
||||
|
||||
@@ -72,13 +72,24 @@ 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 """
|
||||
@@ -384,6 +395,8 @@ class TemplateBasedCCWrapperBuilder: CCWrapperBuilder {
|
||||
const char *dependency_arg_name = "-MF";
|
||||
const char *output_arg_name = "-o";
|
||||
const char *serialize_diagnostics_arg_name = "--serialize-diagnostics";
|
||||
const char *language_mode_arg_name = "-x";
|
||||
const char *precompile_header_arg_value = "objective-c-header";
|
||||
const char *clang_cmd = "\(clangCommand)";
|
||||
const char *markerFile = "\(markerFilename)";
|
||||
const char *compilationHistoryFile = "\(compilationHistoryFilename)";
|
||||
@@ -398,6 +411,7 @@ class TemplateBasedCCWrapperBuilder: CCWrapperBuilder {
|
||||
const char *output_file= NULL;
|
||||
const char *input_file = NULL;
|
||||
const char *diagnostics_file = NULL;
|
||||
const char *language_mode = NULL;
|
||||
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], dependency_arg_name) == 0 && i < (argc - 1) ) {
|
||||
@@ -418,6 +432,12 @@ class TemplateBasedCCWrapperBuilder: CCWrapperBuilder {
|
||||
i += 1;
|
||||
clang_args[i] = argv[i];
|
||||
diagnostics_file = argv[i];
|
||||
} if (strcmp(argv[i], language_mode_arg_name) == 0 && i < (argc - 1) ) {
|
||||
// called with "-x path" pattern and not the last argument
|
||||
clang_args[i] = argv[i];
|
||||
i += 1;
|
||||
clang_args[i] = argv[i];
|
||||
language_mode = argv[i];
|
||||
} else if (
|
||||
isSuffixed(argv[i],".m") ||
|
||||
isSuffixed(argv[i],".mm") ||
|
||||
@@ -425,10 +445,13 @@ class TemplateBasedCCWrapperBuilder: CCWrapperBuilder {
|
||||
isSuffixed(argv[i],".cc") ||
|
||||
isSuffixed(argv[i],".cpp") ||
|
||||
isSuffixed(argv[i],".c++") ||
|
||||
isSuffixed(argv[i],".cxx")
|
||||
isSuffixed(argv[i],".cxx") ||
|
||||
isSuffixed(argv[i],".S") ||
|
||||
isSuffixed(argv[i],".s")
|
||||
) {
|
||||
// a full list of extensions is taken from https://clang.llvm.org/docs/ClangFormatStyleOptions.html
|
||||
// support for .m,.mm,.c,.cc,.cpp,.c++,.cxx input files
|
||||
// .s and .S are assembly files
|
||||
clang_args[i] = argv[i];
|
||||
input_file = argv[i];
|
||||
} else {
|
||||
@@ -437,6 +460,14 @@ class TemplateBasedCCWrapperBuilder: CCWrapperBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
// null-terminating the args array needed for local compilation fallback
|
||||
clang_args[argc] = NULL;
|
||||
|
||||
// Verify mode. Even a target is cached, pch mode is not supported. Fallback to the local compilation
|
||||
if (language_mode != NULL && strcmp(language_mode, precompile_header_arg_value) == 0) {
|
||||
return execvp(clang_cmd, (char *const*) clang_args);
|
||||
}
|
||||
|
||||
// Verify all input arguments
|
||||
if (dependency_file == NULL) {
|
||||
fprintf(stderr, "error: missing %s input\\n", dependency_arg_name);
|
||||
@@ -507,8 +538,6 @@ class TemplateBasedCCWrapperBuilder: CCWrapperBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
// null-terminating the args array
|
||||
clang_args[argc] = NULL;
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wincompatible-pointer-types-discards-qualifiers"
|
||||
/// execvp takes $PATH to consideration
|
||||
@@ -516,5 +545,5 @@ class TemplateBasedCCWrapperBuilder: CCWrapperBuilder {
|
||||
#pragma GCC diagnostic pop
|
||||
}
|
||||
"""
|
||||
} // swiftlint:disable:next file_length
|
||||
}
|
||||
} // swiftlint:disable:next file_length line_length
|
||||
} // swiftlint:enable line_length
|
||||
|
||||
+9
-2
@@ -33,15 +33,18 @@ protocol BuildSettingsIntegrateAppender {
|
||||
class XcodeProjBuildSettingsIntegrateAppender: BuildSettingsIntegrateAppender {
|
||||
private let mode: Mode
|
||||
private let repoRoot: URL
|
||||
private let fakeSrcRoot: URL
|
||||
|
||||
init(mode: Mode, repoRoot: URL) {
|
||||
init(mode: Mode, repoRoot: URL, fakeSrcRoot: URL) {
|
||||
self.mode = mode
|
||||
self.repoRoot = repoRoot
|
||||
self.fakeSrcRoot = fakeSrcRoot
|
||||
}
|
||||
|
||||
func appendToBuildSettings(buildSettings: BuildSettings, wrappers: XCRCBinariesPaths) -> BuildSettings {
|
||||
var result = buildSettings
|
||||
result["SWIFT_EXEC"] = wrappers.swiftc.path
|
||||
result["SWIFT_USE_INTEGRATED_DRIVER"] = "NO"
|
||||
// When generating artifacts, no need to shell-out all compilation commands to our wrappers
|
||||
if case .consumer = mode {
|
||||
result["CC"] = wrappers.cc.path
|
||||
@@ -61,7 +64,11 @@ class XcodeProjBuildSettingsIntegrateAppender: BuildSettingsIntegrateAppender {
|
||||
result["OTHER_SWIFT_FLAGS"] = swiftFlags.settingValue
|
||||
result["OTHER_CFLAGS"] = clangFlags.settingValue
|
||||
|
||||
result["XCRC_FAKE_SRCROOT"] = "/\(String(repeating: "x", count: 10))"
|
||||
result["XCRC_FAKE_SRCROOT"] = "\(fakeSrcRoot.path)"
|
||||
result["XCRC_PLATFORM_PREFERRED_ARCH"] =
|
||||
"""
|
||||
$(LINK_FILE_LIST_$(CURRENT_VARIANT)_$(PLATFORM_PREFERRED_ARCH):dir:standardizepath:file:default=arm64)
|
||||
"""
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ struct IncludeExcludeOracle: IncludeOracle {
|
||||
|
||||
|
||||
func shouldInclude(identifier: OracleIdentifierType) -> Bool {
|
||||
// exclude array has precedence.
|
||||
// exclude array has precedence
|
||||
if excludes.contains(identifier) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ extension IntegrateContext {
|
||||
swiftc: binariesDir.appendingPathComponent("xcswiftc"),
|
||||
libtool: binariesDir.appendingPathComponent("xclibtool"),
|
||||
ld: binariesDir.appendingPathComponent("xcld"),
|
||||
ldplusplus: binariesDir.appendingPathComponent("xcldplusplus"),
|
||||
prebuild: binariesDir.appendingPathComponent("xcprebuild"),
|
||||
postbuild: binariesDir.appendingPathComponent("xcpostbuild")
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -73,7 +74,7 @@ public class XCIntegrate {
|
||||
let binariesDir = commandURL.deletingLastPathComponent()
|
||||
|
||||
let srcRoot: URL = URL(fileURLWithPath: projectPath).deletingLastPathComponent()
|
||||
let config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileManager: fileManager)
|
||||
let config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileReader: fileManager)
|
||||
.readConfiguration()
|
||||
|
||||
let context = try IntegrateContext(
|
||||
@@ -96,7 +97,8 @@ public class XCIntegrate {
|
||||
)
|
||||
let buildSettingsAppender = XcodeProjBuildSettingsIntegrateAppender(
|
||||
mode: context.mode,
|
||||
repoRoot: context.repoRoot
|
||||
repoRoot: context.repoRoot,
|
||||
fakeSrcRoot: context.fakeSrcRoot
|
||||
)
|
||||
let lldbPatcher: LLDBInitPatcher
|
||||
switch lldbMode {
|
||||
@@ -114,7 +116,7 @@ public class XCIntegrate {
|
||||
|
||||
let integrator = XcodeProjIntegrate(
|
||||
project: context.projectPath,
|
||||
mode:context.mode,
|
||||
mode: context.mode,
|
||||
binaries: context.binaries,
|
||||
configurationIncludeOracle: configurationOracle,
|
||||
targetIncludeOracle: targetOracle,
|
||||
|
||||
@@ -26,6 +26,7 @@ struct XCRCBinariesPaths {
|
||||
let swiftc: URL
|
||||
let libtool: URL
|
||||
let ld: URL
|
||||
let ldplusplus: URL
|
||||
let prebuild: URL
|
||||
let postbuild: URL
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
@@ -224,13 +227,15 @@ struct XcodeProjIntegrate: Integrate {
|
||||
let previousRCPhases = target.buildPhases.filter(isRCPhase)
|
||||
target.buildPhases.removeAll(where: previousRCPhases.contains)
|
||||
|
||||
if target.buildPhases.map(\.buildPhase).contains(.sources) {
|
||||
if let sourceIndex = target.buildPhases.map(\.buildPhase).firstIndex(of: .sources) {
|
||||
// add (pre|post)build phases only when a target has some compilation steps
|
||||
// otherwise they make no sense (nothing to store in an artifact)
|
||||
pbxproj.add(object: prebuildPhase)
|
||||
target.buildPhases.insert(prebuildPhase, at: 0)
|
||||
// add postbuild right after compilation as custom build steps may depend on files generated by
|
||||
// the xcpostbuild (e.g. to copy -Swift.h.md5)
|
||||
pbxproj.add(object: postbuildPhase)
|
||||
target.buildPhases.append(postbuildPhase)
|
||||
target.buildPhases.insert(postbuildPhase, at: sourceIndex + 1)
|
||||
pbxproj.add(object: prebuildPhase)
|
||||
target.buildPhases.insert(prebuildPhase, at: sourceIndex)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
// under the License.
|
||||
|
||||
import Foundation
|
||||
import Yams
|
||||
|
||||
/// Print current configuration to the console
|
||||
public class XCConfig {
|
||||
@@ -33,7 +32,7 @@ public class XCConfig {
|
||||
let fileManager = FileManager.default
|
||||
let config: XCRemoteCacheConfig
|
||||
do {
|
||||
config = try XCRemoteCacheConfigReader(env: env, fileManager: fileManager).readConfiguration()
|
||||
config = try XCRemoteCacheConfigReader(env: env, fileReader: fileManager).readConfiguration()
|
||||
} catch {
|
||||
exit(1, "FATAL: Prepare initialization failed with error: \(error)")
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
// under the License.
|
||||
|
||||
import Foundation
|
||||
import Yams
|
||||
|
||||
/// Switch between Online/Offline modes
|
||||
public enum XCPrepareMode {
|
||||
@@ -62,7 +61,7 @@ public class XCPrepare {
|
||||
var context: PrepareContext
|
||||
let xcodeVersion: String
|
||||
do {
|
||||
config = try XCRemoteCacheConfigReader(env: env, fileManager: fileManager).readConfiguration()
|
||||
config = try XCRemoteCacheConfigReader(env: env, fileReader: fileManager).readConfiguration()
|
||||
context = try PrepareContext(config, offline: offline)
|
||||
xcodeVersion = try customXcodeBuildNumber ?? XcodeProbeImpl(shell: shellGetStdout).read().buildVersion
|
||||
} catch {
|
||||
@@ -79,6 +78,7 @@ public class XCPrepare {
|
||||
awsV4Signature = AWSV4Signature(
|
||||
secretKey: config.AWSSecretKey,
|
||||
accessKey: config.AWSAccessKey,
|
||||
securityToken: config.AWSSecurityToken,
|
||||
region: config.AWSRegion,
|
||||
service: config.AWSService,
|
||||
date: Date(timeIntervalSinceNow: 0)
|
||||
@@ -87,6 +87,7 @@ public class XCPrepare {
|
||||
let networkClient = NetworkClientImpl(
|
||||
session: sessionFactory.build(),
|
||||
retries: config.downloadRetries,
|
||||
retryDelay: config.retryDelay,
|
||||
fileManager: fileManager,
|
||||
awsV4Signature: awsV4Signature
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -45,7 +46,7 @@ public class XCPrepareMark {
|
||||
let context: PrepareMarkContext
|
||||
let xcodeVersion: String
|
||||
do {
|
||||
config = try XCRemoteCacheConfigReader(env: env, fileManager: fileManager).readConfiguration()
|
||||
config = try XCRemoteCacheConfigReader(env: env, fileReader: fileManager).readConfiguration()
|
||||
context = try PrepareMarkContext(config)
|
||||
xcodeVersion = try xcode ?? XcodeProbeImpl(shell: shellGetStdout).read().buildVersion
|
||||
} catch {
|
||||
@@ -59,6 +60,7 @@ public class XCPrepareMark {
|
||||
awsV4Signature = AWSV4Signature(
|
||||
secretKey: config.AWSSecretKey,
|
||||
accessKey: config.AWSAccessKey,
|
||||
securityToken: config.AWSSecurityToken,
|
||||
region: config.AWSRegion,
|
||||
service: config.AWSService,
|
||||
date: Date(timeIntervalSinceNow: 0)
|
||||
@@ -67,6 +69,7 @@ public class XCPrepareMark {
|
||||
let networkClient = NetworkClientImpl(
|
||||
session: sessionFactory.build(),
|
||||
retries: config.uploadRetries,
|
||||
retryDelay: config.retryDelay,
|
||||
fileManager: fileManager,
|
||||
awsV4Signature: awsV4Signature
|
||||
)
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
// under the License.
|
||||
|
||||
import Foundation
|
||||
import Yams
|
||||
|
||||
/// Manages XCRemoteCache statistics: rests, print to the standard output etc
|
||||
public class XCStats {
|
||||
@@ -37,7 +36,7 @@ public class XCStats {
|
||||
let config: XCRemoteCacheConfig
|
||||
let context: XCStatsContext
|
||||
do {
|
||||
config = try XCRemoteCacheConfigReader(env: env, fileManager: fileManager).readConfiguration()
|
||||
config = try XCRemoteCacheConfigReader(env: env, fileReader: fileManager).readConfiguration()
|
||||
try context = XCStatsContext(config, fileManager: fileManager)
|
||||
} catch {
|
||||
exit(1, "FATAL: Prepare initialization failed with error: \(error)")
|
||||
|
||||
@@ -70,7 +70,7 @@ public class XCCreateBinary {
|
||||
let config: XCRemoteCacheConfig
|
||||
do {
|
||||
let srcRoot: URL = URL(fileURLWithPath: fileManager.currentDirectoryPath)
|
||||
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileManager: fileManager)
|
||||
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileReader: fileManager)
|
||||
.readConfiguration()
|
||||
} catch {
|
||||
errorLog("\(stepDescription) initialization failed with error: \(error). Fallbacking to \(fallbackCommand)")
|
||||
|
||||
@@ -120,9 +120,9 @@ class Swiftc: SwiftcProtocol {
|
||||
let prebuildDiscoveryURL = context.tempDir.appendingPathComponent(context.prebuildDependenciesPath)
|
||||
let prebuildDiscoverWriter = dependenciesWriterFactory(prebuildDiscoveryURL, fileManager)
|
||||
try prebuildDiscoverWriter.write(skipForSha: remoteCommit)
|
||||
case .consumer, .producer:
|
||||
// Never skips prebuild phase and fallbacks to the swiftc compilation for:
|
||||
// 1) Not enabled remote cache or 2) producer
|
||||
case .consumer, .producer, .producerFast:
|
||||
// Never skip prebuild phase and fallback to the swiftc compilation for:
|
||||
// 1) Not enabled remote cache, 2) producer(s)
|
||||
break
|
||||
}
|
||||
return .forceFallback
|
||||
|
||||
@@ -24,6 +24,8 @@ public struct SwiftcContext {
|
||||
case producer
|
||||
/// Commit sha of the commit to use during remote cache
|
||||
case consumer(commit: RemoteCommitInfo)
|
||||
/// Remote artifact exists and can be optimistically used in place of a local compilation
|
||||
case producerFast
|
||||
}
|
||||
|
||||
let objcHeaderOutput: URL
|
||||
@@ -74,6 +76,14 @@ public struct SwiftcContext {
|
||||
mode = .consumer(commit: remoteCommit)
|
||||
case .producer:
|
||||
mode = .producer
|
||||
case .producerFast:
|
||||
let remoteCommit = RemoteCommitInfo(try? String(contentsOf: remoteCommitLocation).trim())
|
||||
switch remoteCommit {
|
||||
case .unavailable:
|
||||
mode = .producer
|
||||
case .available:
|
||||
mode = .producerFast
|
||||
}
|
||||
}
|
||||
invocationHistoryFile = URL(fileURLWithPath: config.compilationHistoryFile, relativeTo: tempDir)
|
||||
}
|
||||
|
||||
@@ -110,12 +110,18 @@ class SwiftcOrchestrator {
|
||||
try invocationStorage.store(args: invocationArgs)
|
||||
}
|
||||
} catch {
|
||||
// The critical section is protected by a lock. Some other process already called compilation history.
|
||||
// We only need to call our current step then.
|
||||
// The critical section is protected by a lock. Some other process already called compilation history
|
||||
// We only need to call our current step then
|
||||
fallbackToDefault(command: swiftcCommand)
|
||||
}
|
||||
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
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
import Foundation
|
||||
|
||||
enum DiskSwiftcProductsGeneratorError: Error {
|
||||
/// When a generator was asked to generate unknown swiftmodule extension file.
|
||||
/// When a generator was asked to generate unknown swiftmodule extension file
|
||||
/// Probably a programmer error: asking to generate excessive extensions, not listed in
|
||||
/// `SwiftmoduleFileExtension.SwiftmoduleExtensions`
|
||||
case unknownSwiftmoduleFile
|
||||
|
||||
@@ -70,7 +70,7 @@ public class XCSwiftc {
|
||||
let context: SwiftcContext
|
||||
do {
|
||||
let srcRoot: URL = URL(fileURLWithPath: fileManager.currentDirectoryPath)
|
||||
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileManager: fileManager)
|
||||
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileReader: fileManager)
|
||||
.readConfiguration()
|
||||
context = try SwiftcContext(config: config, input: inputArgs)
|
||||
} catch {
|
||||
@@ -101,7 +101,10 @@ public class XCSwiftc {
|
||||
objcHeaderOutput: context.objcHeaderOutput,
|
||||
diskCopier: HardLinkDiskCopier(fileManager: fileManager)
|
||||
)
|
||||
let allInvocationsStorage = ExistingFileStorage(storageFile: context.invocationHistoryFile, command: swiftcCommand)
|
||||
let allInvocationsStorage = ExistingFileStorage(
|
||||
storageFile: context.invocationHistoryFile,
|
||||
command: swiftcCommand
|
||||
)
|
||||
// When fallbacking to local compilation do not call historical `swiftc` invocations
|
||||
// The current fallback invocation already compiles all files in a target
|
||||
let invocationStorage = FilteredInvocationStorage(
|
||||
|
||||
@@ -20,4 +20,5 @@
|
||||
public enum Mode: String, Codable, CaseIterable {
|
||||
case consumer
|
||||
case producer
|
||||
case producerFast = "producer-fast"
|
||||
}
|
||||
|
||||
@@ -83,6 +83,8 @@ public struct XCRemoteCacheConfig: Encodable {
|
||||
var downloadRetries: Int = 0
|
||||
/// Number of retries for upload requests
|
||||
var uploadRetries: Int = 3
|
||||
/// Delay between retries in seconds
|
||||
var retryDelay: Double = 10.0
|
||||
/// Extra headers appended to all remote HTTP(S) requests
|
||||
var requestCustomHeaders: [String: String] = [:]
|
||||
/// Filename (without an extension) of the compilation input file that is used
|
||||
@@ -94,10 +96,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,19 +114,43 @@ 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
|
||||
var AWSAccessKey: String = ""
|
||||
/// Temporary security token provided by the AWS Security Token Service
|
||||
var AWSSecurityToken: String?
|
||||
/// Region for AWS V4 Signature (e.g. `eu`)
|
||||
var AWSRegion: String = ""
|
||||
/// Service for AWS V4 Signature (e.g. `storage`)
|
||||
var AWSService: String = ""
|
||||
/// A dictionary of files path remapping that should be applied to make it absolute path agnostic on a list of
|
||||
/// dependencies. Useful if a project refers files out of repo root, either compilation files or precompiled
|
||||
/// dependencies. Keys represent generic replacement and values are substrings that should be replaced
|
||||
/// Example: for mapping `["COOL_LIBRARY": "/CoolLibrary"]`
|
||||
/// `/CoolLibrary/main.swift`will be represented as `$(COOL_LIBRARY)/main.swift`)
|
||||
/// Warning: remapping order is not-deterministic so avoid remappings with multiple matchings
|
||||
var outOfBandMappings: [String: String] = [:]
|
||||
/// If true, SSL certificate validation is disabled
|
||||
var disableCertificateVerification: Bool = false
|
||||
/// A feature flag to disable virtual file system overlay support (temporary)
|
||||
var disableVFSOverlay: Bool = false
|
||||
/// A list of extra ENVs that should be used as placeholders in the dependency list
|
||||
/// ENV rewrite process is optimistic - does nothing if an ENV is not defined in the pre/postbuild process
|
||||
var customRewriteEnvs: [String] = []
|
||||
/// Regexes of files that should not be included in a list of dependencies. Warning! Add entries here
|
||||
/// with caution - excluding dependencies that are relevant might lead to a target overcaching
|
||||
/// Note: The regex can match either partially or fully the filepath, e.g. `\\.modulemap$` will exclude
|
||||
/// all `.modulemap` files
|
||||
var irrelevantDependenciesPaths: [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
|
||||
@@ -150,6 +177,7 @@ extension XCRemoteCacheConfig {
|
||||
merge.statsDir = scheme.statsDir ?? statsDir
|
||||
merge.downloadRetries = scheme.downloadRetries ?? downloadRetries
|
||||
merge.uploadRetries = scheme.uploadRetries ?? uploadRetries
|
||||
merge.retryDelay = scheme.retryDelay ?? retryDelay
|
||||
merge.requestCustomHeaders = scheme.requestCustomHeaders ?? requestCustomHeaders
|
||||
merge.thinTargetMockFilename = scheme.thinTargetMockFilename ?? thinTargetMockFilename
|
||||
merge.focusedTargets = scheme.focusedTargets ?? focusedTargets
|
||||
@@ -163,10 +191,17 @@ 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.AWSSecurityToken = scheme.AWSSecurityToken ?? AWSSecurityToken
|
||||
merge.AWSRegion = scheme.AWSRegion ?? AWSRegion
|
||||
merge.AWSService = scheme.AWSService ?? AWSService
|
||||
merge.outOfBandMappings = scheme.outOfBandMappings ?? outOfBandMappings
|
||||
merge.disableCertificateVerification = scheme.disableCertificateVerification ?? disableCertificateVerification
|
||||
merge.disableVFSOverlay = scheme.disableVFSOverlay ?? disableVFSOverlay
|
||||
merge.customRewriteEnvs = scheme.customRewriteEnvs ?? customRewriteEnvs
|
||||
merge.irrelevantDependenciesPaths = scheme.irrelevantDependenciesPaths ?? irrelevantDependenciesPaths
|
||||
return merge
|
||||
}
|
||||
|
||||
@@ -211,6 +246,7 @@ struct ConfigFileScheme: Decodable {
|
||||
let statsDir: String?
|
||||
let downloadRetries: Int?
|
||||
let uploadRetries: Int?
|
||||
let retryDelay: Double?
|
||||
let requestCustomHeaders: [String: String]?
|
||||
let thinTargetMockFilename: String?
|
||||
let focusedTargets: [String]?
|
||||
@@ -221,10 +257,17 @@ struct ConfigFileScheme: Decodable {
|
||||
let productFilesExtensionsWithContentOverride: [String]?
|
||||
let thinningEnabled: Bool?
|
||||
let thinningTargetModuleName: String?
|
||||
let prettifyMetaFiles: Bool?
|
||||
let AWSSecretKey: String?
|
||||
let AWSAccessKey: String?
|
||||
let AWSSecurityToken: String?
|
||||
let AWSRegion: String?
|
||||
let AWSService: String?
|
||||
let outOfBandMappings: [String: String]?
|
||||
let disableCertificateVerification: Bool?
|
||||
let disableVFSOverlay: Bool?
|
||||
let customRewriteEnvs: [String]?
|
||||
let irrelevantDependenciesPaths: [String]?
|
||||
|
||||
// Yams library doesn't support encoding strategy, see https://github.com/jpsim/Yams/issues/84
|
||||
enum CodingKeys: String, CodingKey {
|
||||
@@ -252,6 +295,7 @@ struct ConfigFileScheme: Decodable {
|
||||
case statsDir = "stats_dir"
|
||||
case downloadRetries = "download_retries"
|
||||
case uploadRetries = "upload_retries"
|
||||
case retryDelay = "retry_delay"
|
||||
case requestCustomHeaders = "request_custom_headers"
|
||||
case thinTargetMockFilename = "thin_target_mock_filename"
|
||||
case focusedTargets = "focused_targets"
|
||||
@@ -262,10 +306,17 @@ 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 AWSSecurityToken = "aws_security_token"
|
||||
case AWSRegion = "aws_region"
|
||||
case AWSService = "aws_service"
|
||||
case outOfBandMappings = "out_of_band_mappings"
|
||||
case disableCertificateVerification = "disable_certificate_verification"
|
||||
case disableVFSOverlay = "disable_vfs_overlay"
|
||||
case customRewriteEnvs = "custom_rewrite_envs"
|
||||
case irrelevantDependenciesPaths = "irrelevant_dependencies_paths"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,31 +329,42 @@ class XCRemoteCacheConfigReader {
|
||||
/// Name of the configuration file, required in $(SRCROOT) location
|
||||
private static let configurationFile = ".rcinfo"
|
||||
private let srcRoot: String
|
||||
private let fileManager: FileManager
|
||||
private let fileReader: FileReader
|
||||
private lazy var yamlDecorer = YAMLDecoder(encoding: .utf8)
|
||||
|
||||
init(env: [String: String], fileManager: FileManager) throws {
|
||||
init(env: [String: String], fileReader: FileReader) throws {
|
||||
let explicitSrcRoot: String? = env.readEnv(key: "SRCROOT")
|
||||
srcRoot = explicitSrcRoot ?? fileManager.currentDirectoryPath
|
||||
self.fileManager = fileManager
|
||||
srcRoot = explicitSrcRoot ?? FileManager.default.currentDirectoryPath
|
||||
self.fileReader = fileReader
|
||||
}
|
||||
|
||||
init(srcRootPath srcRoot: String, fileManager: FileManager) {
|
||||
init(srcRootPath srcRoot: String, fileReader: FileReader) {
|
||||
self.srcRoot = srcRoot
|
||||
self.fileManager = fileManager
|
||||
self.fileReader = fileReader
|
||||
}
|
||||
|
||||
// Reads the final configuration by loading all extra configs
|
||||
// until reaching a config that doesn't override `extraConfigurationFile`
|
||||
func readConfiguration() throws -> XCRemoteCacheConfig {
|
||||
let rootURL = URL(fileURLWithPath: srcRoot)
|
||||
let configURL = URL(fileURLWithPath: Self.configurationFile, relativeTo: rootURL)
|
||||
let userConfigs = try readUserConfig(configURL)
|
||||
var config = XCRemoteCacheConfig(sourceRoot: srcRoot).merged(with: userConfigs)
|
||||
let extraConfURL = URL(fileURLWithPath: config.extraConfigurationFile, relativeTo: rootURL)
|
||||
do {
|
||||
let extraConfig = try readUserConfig(extraConfURL)
|
||||
config = config.merged(with: extraConfig)
|
||||
} catch {
|
||||
infoLog("Extra config override failed with \(error). Skipping extra configuration")
|
||||
var extraConfURL = URL(fileURLWithPath: config.extraConfigurationFile, relativeTo: rootURL)
|
||||
var visitedFiles = Set([configURL])
|
||||
while !visitedFiles.contains(extraConfURL) {
|
||||
do {
|
||||
let extraConfig = try readUserConfig(extraConfURL)
|
||||
debugLog("Reading extra configuration from \(extraConfURL)")
|
||||
config = config.merged(with: extraConfig)
|
||||
visitedFiles.insert(extraConfURL)
|
||||
// Advance extra configuration
|
||||
extraConfURL = URL(fileURLWithPath: config.extraConfigurationFile, relativeTo: rootURL)
|
||||
} catch {
|
||||
infoLog("Extra config override failed with \(error). Skipping extra configuration")
|
||||
// swiftlint:disable:next unneeded_break_in_switch
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return try config.verifyAndApplyDefaults()
|
||||
@@ -310,7 +372,7 @@ class XCRemoteCacheConfigReader {
|
||||
|
||||
/// Reads user configuration from a file
|
||||
private func readUserConfig(_ file: URL) throws -> ConfigFileScheme {
|
||||
let configurationContent = fileManager.contents(atPath: file.path)
|
||||
let configurationContent = try fileReader.contents(atPath: file.path)
|
||||
guard let configurationData = configurationContent else {
|
||||
throw XCRemoteCacheConfigReaderError.missingConfigurationFile(file)
|
||||
}
|
||||
|
||||
@@ -51,34 +51,22 @@ public class FileDependenciesReader: DependenciesReader {
|
||||
public func findDependencies() throws -> [String] {
|
||||
let yaml = try readRaw()
|
||||
|
||||
struct ParseState {
|
||||
var buffer: String = ""
|
||||
var prevChar: Character?
|
||||
var result: [String] = []
|
||||
func with(buffer: String? = nil, prevChar: Character? = nil, result: [String]? = nil) -> ParseState {
|
||||
var new = self
|
||||
new.buffer = buffer ?? new.buffer
|
||||
new.prevChar = prevChar ?? new.prevChar
|
||||
new.result = result ?? new.result
|
||||
return new
|
||||
}
|
||||
}
|
||||
|
||||
let dependencies = yaml.reduce(Set<String>()) { prev, arg1 -> Set<String> in
|
||||
let (key, value) = arg1
|
||||
switch key {
|
||||
case "dependencies":
|
||||
// 'clang' output formatting
|
||||
return Set(splitDependencyFileList(value))
|
||||
return Set(parseDependencyFileList(value))
|
||||
case let s where s.hasSuffix(".o") || s.hasSuffix(".bc"):
|
||||
// 'swiftc' output formatting
|
||||
// take dependencies from any .o or .bc file.
|
||||
// take dependencies from any .o or .bc file
|
||||
// Note: For WMO, all .{o|bc} files have the same dependencies
|
||||
return Set(splitDependencyFileList(value))
|
||||
return Set(parseDependencyFileList(value))
|
||||
default:
|
||||
return prev
|
||||
}
|
||||
}
|
||||
|
||||
return Array(dependencies)
|
||||
}
|
||||
|
||||
@@ -92,56 +80,92 @@ public class FileDependenciesReader: DependenciesReader {
|
||||
return yaml.mapValues { $0.components(separatedBy: .whitespaces) }
|
||||
}
|
||||
|
||||
private func readRaw() throws -> [String: String] {
|
||||
func readRaw() throws -> [String: String] {
|
||||
let fileData = try getFileData()
|
||||
let fileString = try getFileStringFromData(fileData: fileData)
|
||||
let yaml = try getYaml(fileString: fileString)
|
||||
return yaml
|
||||
}
|
||||
|
||||
func getFileData() throws -> Data {
|
||||
guard let fileData = fileManager.contents(atPath: file.path) else {
|
||||
throw DependenciesReaderError.readingError
|
||||
}
|
||||
return fileData
|
||||
}
|
||||
|
||||
func getFileStringFromData(fileData: Data) throws -> String {
|
||||
guard let fileString = String(data: fileData, encoding: .utf8) else {
|
||||
throw DependenciesReaderError.invalidFile
|
||||
}
|
||||
// .d matches the .yaml format
|
||||
return fileString
|
||||
}
|
||||
|
||||
func getYaml(fileString: String) throws -> [String: String] {
|
||||
guard let yaml = try Yams.load(yaml: fileString) as? [String: String] else {
|
||||
throw DependenciesReaderError.invalidFile
|
||||
}
|
||||
return yaml
|
||||
}
|
||||
|
||||
/// Splits space or new line separated files into a set of files
|
||||
/// Parses the String to get the list of files
|
||||
/// It iterates over the String using its UTF8View since it is more performant (String type operates
|
||||
/// in a higher abstraction level and supports features that have a negative impact in the performance)
|
||||
/// It supports escaping whitespace charaters, prefixed with "\\"
|
||||
/// - Parameter string: string of whitespace charaters separated file paths
|
||||
/// - Returns: Array of all file paths
|
||||
private func splitDependencyFileList(_ string: String) -> [String] {
|
||||
struct ParseState {
|
||||
var buffer: String = ""
|
||||
var prevChar: Character?
|
||||
var result: [String] = []
|
||||
func with(buffer: String? = nil, prevChar: Character? = nil, result: [String]? = nil) -> ParseState {
|
||||
var new = self
|
||||
new.buffer = buffer ?? new.buffer
|
||||
new.prevChar = prevChar ?? new.prevChar
|
||||
new.result = result ?? new.result
|
||||
return new
|
||||
}
|
||||
func parseDependencyFileList(_ string: String) -> [String] {
|
||||
var result: [String] = []
|
||||
var prevChar: UTF8.CodeUnit?
|
||||
|
||||
// These index are used to move over the UTF8View of the string
|
||||
// The goal is to optimize the memory used, since UTF8View uses
|
||||
// the same memory as the original String without copying it
|
||||
var startIndex = string.utf8.startIndex
|
||||
var endIndex = startIndex
|
||||
|
||||
// This buffer is only used to save the part of the path that has been already parsed when finding a backslash
|
||||
var buffer: String = ""
|
||||
|
||||
for c in string.utf8 {
|
||||
switch c {
|
||||
case UTF8.CodeUnit(ascii: "\n") where prevChar == UTF8.CodeUnit(ascii: "\\"):
|
||||
startIndex = string.utf8.index(after: startIndex)
|
||||
endIndex = startIndex
|
||||
case UTF8.CodeUnit(ascii: " ") where startIndex == endIndex && buffer.isEmpty:
|
||||
startIndex = string.utf8.index(after: startIndex)
|
||||
endIndex = startIndex
|
||||
case UTF8.CodeUnit(ascii: " ") where prevChar != UTF8.CodeUnit(ascii: "\\"):
|
||||
// If a space is found and it is not escaped, then that's the end of the file path
|
||||
buffer += String(Substring(string.utf8[startIndex ..< endIndex]))
|
||||
result.append(buffer)
|
||||
buffer = ""
|
||||
prevChar = nil
|
||||
startIndex = string.utf8.index(after: endIndex)
|
||||
endIndex = startIndex
|
||||
case UTF8.CodeUnit(ascii: "\\"):
|
||||
// If a backslash is found it is not included in the file path
|
||||
// The current parsed range of the UTF8View is saved in the buffer as a String
|
||||
buffer += String(Substring(string.utf8[startIndex ..< endIndex]))
|
||||
// The backslash is assigned as the previous char
|
||||
prevChar = c
|
||||
// The indexes are moved to the next char so we continue parsing the String
|
||||
startIndex = string.utf8.index(after: endIndex)
|
||||
endIndex = startIndex
|
||||
default:
|
||||
// As long as it is possible the indexes are used to track the range of the string that
|
||||
// will be included in the file path (until it ends or until a backslash is found)
|
||||
endIndex = string.utf8.index(after: endIndex)
|
||||
// The char is assigned as the previous char
|
||||
prevChar = c
|
||||
}
|
||||
}
|
||||
let parseResult = string.reduce(ParseState()) { total, char in
|
||||
switch char {
|
||||
case "\n" where total.prevChar == "\\":
|
||||
return total
|
||||
case " " where total.buffer.isEmpty:
|
||||
return total
|
||||
case " " where total.prevChar == "\\":
|
||||
return total.with(buffer: "\(total.buffer) ")
|
||||
case " ":
|
||||
return total.with(buffer: "", prevChar: nil, result: total.result + [total.buffer])
|
||||
case "\\":
|
||||
return total.with(prevChar: "\\")
|
||||
default:
|
||||
return total.with(buffer: "\(total.buffer)\(char)", prevChar: char, result: total.result)
|
||||
}
|
||||
|
||||
if startIndex != endIndex {
|
||||
buffer += String(Substring(string.utf8[startIndex ..< endIndex]))
|
||||
result.append(buffer)
|
||||
}
|
||||
if !parseResult.buffer.isEmpty {
|
||||
return parseResult.result + [parseResult.buffer]
|
||||
}
|
||||
return parseResult.result
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,9 @@ import Foundation
|
||||
/// Replaces paths formats between generic (placeholders-based) and local
|
||||
protocol DependenciesRemapper {
|
||||
/// Replaces all generic paths (with placeholders) to a local paths
|
||||
func replace(genericPaths: [String]) -> [String]
|
||||
func replace(genericPaths: [String]) throws -> [String]
|
||||
/// Replaces all local paths to the generic dependencies paths
|
||||
func replace(localPaths: [String]) -> [String]
|
||||
func replace(localPaths: [String]) throws -> [String]
|
||||
}
|
||||
|
||||
class DependenciesRemapperComposite: DependenciesRemapper {
|
||||
@@ -34,15 +34,15 @@ class DependenciesRemapperComposite: DependenciesRemapper {
|
||||
self.remappers = remappers
|
||||
}
|
||||
|
||||
func replace(genericPaths: [String]) -> [String] {
|
||||
remappers.reduce(genericPaths) { prev, mapper in
|
||||
mapper.replace(genericPaths: prev)
|
||||
func replace(genericPaths: [String]) throws -> [String] {
|
||||
try remappers.reversed().reduce(genericPaths) { prev, mapper in
|
||||
try mapper.replace(genericPaths: prev)
|
||||
}
|
||||
}
|
||||
|
||||
func replace(localPaths: [String]) -> [String] {
|
||||
remappers.reduce(localPaths) { prev, mapper in
|
||||
mapper.replace(localPaths: prev)
|
||||
func replace(localPaths: [String]) throws -> [String] {
|
||||
try remappers.reduce(localPaths) { prev, mapper in
|
||||
try mapper.replace(localPaths: prev)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,16 +59,16 @@ final class StringDependenciesRemapper: DependenciesRemapper {
|
||||
self.mappings = mappings
|
||||
}
|
||||
|
||||
func replace(genericPaths: [String]) -> [String] {
|
||||
func replace(genericPaths: [String]) throws -> [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
|
||||
}
|
||||
}
|
||||
|
||||
func replace(localPaths: [String]) -> [String] {
|
||||
func replace(localPaths: [String]) throws -> [String] {
|
||||
return localPaths.map { path in
|
||||
let result = mappings.reduce(path) { prevPath, mapping in
|
||||
prevPath.replacingOccurrences(of: mapping.local, with: mapping.generic)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ public class FileDependenciesWriter: DependenciesWriter {
|
||||
var content = ""
|
||||
for (file, deps) in dependencies {
|
||||
content.append(file + ": ")
|
||||
content.append(deps.joined(separator: " "))
|
||||
content.append(deps.map { $0.replacingOccurrences(of: " ", with: "\\ ") }.joined(separator: " "))
|
||||
content.append("\n")
|
||||
}
|
||||
try content.write(to: file, atomically: true, encoding: .utf8)
|
||||
|
||||
@@ -27,8 +27,11 @@ public struct Dependency: Equatable {
|
||||
case source
|
||||
case fingerprint
|
||||
case intermediate
|
||||
case derivedFile
|
||||
// Product of the target itself
|
||||
case ownProduct
|
||||
// User-excluded path
|
||||
case userExcluded
|
||||
case unknown
|
||||
}
|
||||
|
||||
@@ -55,14 +58,18 @@ class DependencyProcessorImpl: DependencyProcessor {
|
||||
private let productPath: String
|
||||
private let sourcePath: String
|
||||
private let intermediatePath: String
|
||||
private let derivedFilesPath: String
|
||||
private let bundlePath: String?
|
||||
private let skippedRegexes: [String]
|
||||
|
||||
init(xcode: URL, product: URL, source: URL, intermediate: URL, bundle: URL?) {
|
||||
xcodePath = xcode.path
|
||||
productPath = product.path
|
||||
sourcePath = source.path
|
||||
intermediatePath = intermediate.path
|
||||
bundlePath = bundle?.path
|
||||
init(xcode: URL, product: URL, source: URL, intermediate: URL, derivedFiles: URL, bundle: URL?, skippedRegexes: [String]) {
|
||||
xcodePath = xcode.path.dirPath()
|
||||
productPath = product.path.dirPath()
|
||||
sourcePath = source.path.dirPath()
|
||||
intermediatePath = intermediate.path.dirPath()
|
||||
derivedFilesPath = derivedFiles.path.dirPath()
|
||||
bundlePath = bundle?.path.dirPath()
|
||||
self.skippedRegexes = skippedRegexes
|
||||
}
|
||||
|
||||
func process(_ files: [URL]) -> [Dependency] {
|
||||
@@ -72,11 +79,15 @@ class DependencyProcessorImpl: DependencyProcessor {
|
||||
|
||||
private func classify(_ files: [URL]) -> [Dependency] {
|
||||
return files.map { file -> Dependency in
|
||||
let filePath = file.path
|
||||
if filePath.hasPrefix(xcodePath) {
|
||||
let filePath = file.resolvingSymlinksInPath().path
|
||||
if skippedRegexes.contains(where: { filePath.range(of: $0, options: .regularExpression) != nil }) {
|
||||
return Dependency(url: file, type: .userExcluded)
|
||||
} else if filePath.hasPrefix(xcodePath) {
|
||||
return Dependency(url: file, type: .xcode)
|
||||
} else if filePath.hasPrefix(intermediatePath) {
|
||||
return Dependency(url: file, type: .intermediate)
|
||||
} else if filePath.hasPrefix(derivedFilesPath) {
|
||||
return Dependency(url: file, type: .derivedFile)
|
||||
} else if let bundle = bundlePath, filePath.hasPrefix(bundle) {
|
||||
// If a target produces a bundle, explicitly classify all
|
||||
// of products to distinguish from other targets products
|
||||
@@ -107,7 +118,18 @@ class DependencyProcessorImpl: DependencyProcessor {
|
||||
// - All files in `*/Interemediates/*` - this file are created on-fly for a given target
|
||||
// - Some files may depend on its own product (e.g. .m may #include *-Swift.h) - we know products will match
|
||||
// because in case of a hit, these will be taken from the artifact
|
||||
let irrelevantDependenciesType: [Dependency.Kind] = [.xcode, .intermediate, .ownProduct]
|
||||
// - Customized DERIVED_FILE_DIR may change a directory of
|
||||
// derived files, which by default is under `*/Interemediates`
|
||||
// - User-specified (in .rcinfo) files to exclude
|
||||
let irrelevantDependenciesType: [Dependency.Kind] = [
|
||||
.xcode, .intermediate, .ownProduct, .derivedFile, .userExcluded,
|
||||
]
|
||||
return !irrelevantDependenciesType.contains(dependency.type)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension String {
|
||||
func dirPath() -> String {
|
||||
hasSuffix("/") ? self : appending("/")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// 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
|
||||
|
||||
/// File paths remapper according the virtual file system mappings
|
||||
/// - Warning: this class is not thread safe
|
||||
class OverlayDependenciesRemapper: DependenciesRemapper {
|
||||
private let overlayReader: OverlayReader
|
||||
private var mappings: [OverlayMapping]?
|
||||
|
||||
init(overlayReader: OverlayReader) {
|
||||
self.overlayReader = overlayReader
|
||||
}
|
||||
|
||||
/// Lazily Reads mappings from a file
|
||||
/// - Warning: this function is not thread safe
|
||||
private func getMappings() throws -> [OverlayMapping] {
|
||||
guard let mappings = mappings else {
|
||||
let mappings = try overlayReader.provideMappings()
|
||||
self.mappings = mappings
|
||||
return mappings
|
||||
}
|
||||
return mappings
|
||||
}
|
||||
|
||||
private func mapPath(
|
||||
_ path: String,
|
||||
source: KeyPath<OverlayMapping, URL>,
|
||||
destination: KeyPath<OverlayMapping, URL>
|
||||
) throws -> String {
|
||||
guard let mapping = try getMappings().first(where: { $0[keyPath: source].path == path }) else {
|
||||
// TODO: support partial mappings, where a directory path can be replaced with some other directory
|
||||
// no direct mapping found
|
||||
return path
|
||||
}
|
||||
return mapping[keyPath: destination].path
|
||||
}
|
||||
|
||||
func replace(genericPaths: [String]) throws -> [String] {
|
||||
try genericPaths.map {
|
||||
try mapPath($0, source: \.virtual, destination: \.local)
|
||||
}
|
||||
}
|
||||
|
||||
func replace(localPaths: [String]) throws -> [String] {
|
||||
try localPaths.map {
|
||||
try mapPath($0, source: \.local, destination: \.virtual)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
// 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
|
||||
|
||||
/// Maps overlay's virtual URL with an actual (local) location
|
||||
struct OverlayMapping: Hashable {
|
||||
let virtual: URL
|
||||
let local: URL
|
||||
}
|
||||
|
||||
enum JsonOverlayReaderError: Error {
|
||||
/// The source file is missing
|
||||
case missingSourceFile(URL)
|
||||
/// The file exists but its content is invalid
|
||||
case invalidSourceContent(URL)
|
||||
/// the overlay format is not supported - either contains a nested directory or a single file
|
||||
case unsupportedFormat
|
||||
}
|
||||
/// Provides virtual file system overlay mappings
|
||||
protocol OverlayReader {
|
||||
func provideMappings() throws -> [OverlayMapping]
|
||||
}
|
||||
|
||||
class JsonOverlayReader: OverlayReader {
|
||||
|
||||
enum Mode {
|
||||
/// Interrupts the operation if the representation file is missing
|
||||
case strict
|
||||
/// Assume empty overlay mapping if the file doesn't exist
|
||||
case bestEffort
|
||||
}
|
||||
|
||||
private struct Overlay: Decodable {
|
||||
enum OverlayType: String, Decodable {
|
||||
case file
|
||||
case directory
|
||||
}
|
||||
|
||||
struct Content: Decodable {
|
||||
let externalContents: String
|
||||
let name: String
|
||||
let type: OverlayType
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case externalContents = "external-contents"
|
||||
case name
|
||||
case type
|
||||
}
|
||||
}
|
||||
|
||||
struct RootContent: Decodable {
|
||||
let contents: [Content]
|
||||
let name: String
|
||||
let type: OverlayType
|
||||
}
|
||||
let roots: [RootContent]
|
||||
}
|
||||
|
||||
private lazy var jsonDecoder = JSONDecoder()
|
||||
private let json: URL
|
||||
private let mode: Mode
|
||||
private let fileReader: FileReader
|
||||
|
||||
|
||||
init(_ json: URL, mode: Mode, fileReader: FileReader) {
|
||||
self.json = json
|
||||
self.mode = mode
|
||||
self.fileReader = fileReader
|
||||
}
|
||||
|
||||
func provideMappings() throws -> [OverlayMapping] {
|
||||
guard let jsonContent = try fileReader.contents(atPath: json.path) else {
|
||||
switch mode {
|
||||
case .strict:
|
||||
throw JsonOverlayReaderError.missingSourceFile(json)
|
||||
case .bestEffort:
|
||||
debugLog("overlay mapping file \(json) doesn't exist. Skipping overlay for the best-effort mode.")
|
||||
return []
|
||||
}
|
||||
}
|
||||
do {
|
||||
let overlay: Overlay = try jsonDecoder.decode(Overlay.self, from: jsonContent)
|
||||
let mappings: [OverlayMapping] = try overlay.roots.reduce([]) { prev, root in
|
||||
switch root.type {
|
||||
case .directory:
|
||||
// iterate all contents
|
||||
let dir = URL(fileURLWithPath: root.name)
|
||||
let mappings: [OverlayMapping] = try root.contents.map { content in
|
||||
switch content.type {
|
||||
case .file:
|
||||
let virtual = dir.appendingPathComponent(content.name)
|
||||
let local = URL(fileURLWithPath: content.externalContents)
|
||||
return .init(virtual: virtual, local: local)
|
||||
case .directory:
|
||||
throw JsonOverlayReaderError.unsupportedFormat
|
||||
}
|
||||
|
||||
}
|
||||
return prev + mappings
|
||||
case .file:
|
||||
throw JsonOverlayReaderError.unsupportedFormat
|
||||
}
|
||||
}
|
||||
return mappings
|
||||
} catch {
|
||||
switch mode {
|
||||
case .strict:
|
||||
throw error
|
||||
case .bestEffort:
|
||||
errorLog("Overlay reader has failed with an error \(error). Best-effort mode - skipping an overlay.")
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// 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 PathDependenciesRemapperFactoryError: Error {
|
||||
/// Remapping keys are duplicated and can lead to undetermined results
|
||||
case mappingKeyDuplication
|
||||
}
|
||||
|
||||
class PathDependenciesRemapperFactory {
|
||||
func build(
|
||||
orderKeys: [String],
|
||||
envs: [String: String],
|
||||
customMappings: [String: String]
|
||||
) throws -> StringDependenciesRemapper {
|
||||
let mappingMap = try envs.merging(customMappings) { _, _ in
|
||||
throw PathDependenciesRemapperFactoryError.mappingKeyDuplication
|
||||
}
|
||||
let mappingOrderKeys = orderKeys + customMappings.keys
|
||||
let mappings: [StringDependenciesRemapper.Mapping] = mappingOrderKeys.compactMap { key in
|
||||
guard let localURL: URL = mappingMap.readEnv(key: key) else {
|
||||
debugLog("\(key) ENV to map a dependency is not defined")
|
||||
return nil
|
||||
}
|
||||
infoLog("Found url to remapp: \(localURL). Remapping: \(localURL.standardized.path)")
|
||||
return StringDependenciesRemapper.Mapping(generic: "$(\(key))", local: localURL.standardized.path)
|
||||
}
|
||||
return StringDependenciesRemapper(mappings: mappings)
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ class TargetDependenciesReader: DependenciesReader {
|
||||
let allURLs = try dirScanner.items(at: directory)
|
||||
let mergedDependencies = try allURLs.reduce(Set<String>()) { (prev: Set<String>, file) in
|
||||
// include only these .d files that either have corresponding .o file (incremental) or end
|
||||
// with '-master' (whole-module).
|
||||
// with '-master' (whole-module)
|
||||
// Otherwise .d is probably just a leftover from previous builds
|
||||
let correspondingOutputURL = file.deletingPathExtension().appendingPathExtension("o")
|
||||
let isDependencyFile = file.pathExtension == "d"
|
||||
|
||||
@@ -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]
|
||||
@@ -51,7 +52,9 @@ class EnvironmentFingerprintGenerator {
|
||||
}
|
||||
try fill(envKeys: Self.defaultEnvFingerprintKeys + customFingerprintEnvs)
|
||||
try generator.append(version)
|
||||
return try generator.generate()
|
||||
let result = try generator.generate()
|
||||
generatedFingerprint = result
|
||||
return result
|
||||
}
|
||||
|
||||
/// Creates a fingerprint of the environemtn, by hashing all ENVs specified in keys
|
||||
|
||||
@@ -22,34 +22,36 @@ import Foundation
|
||||
import os.log
|
||||
|
||||
|
||||
private var processTag: String = ""
|
||||
|
||||
public func exit(_ exitCode: Int32, _ message: String) -> Never {
|
||||
os_log("%{public}@", log: OSLog.default, type: .error, message)
|
||||
os_log("%{public}@%{public}@", log: OSLog.default, type: .error, processTag, message)
|
||||
printError(errorMessage: message)
|
||||
exit(exitCode)
|
||||
}
|
||||
|
||||
func defaultLog(_ message: String) {
|
||||
os_log("%{public}@", log: OSLog.default, type: .default, message)
|
||||
os_log("%{public}@%{public}@", log: OSLog.default, type: .default, processTag, message)
|
||||
}
|
||||
|
||||
func errorLog(_ message: String) {
|
||||
os_log("%{public}@", log: OSLog.default, type: .error, message)
|
||||
os_log("%{public}@%{public}@", log: OSLog.default, type: .error, processTag, message)
|
||||
}
|
||||
|
||||
func infoLog(_ message: String) {
|
||||
os_log("%{public}@", log: OSLog.default, type: .info, message)
|
||||
os_log("%{public}@%{public}@", log: OSLog.default, type: .info, processTag, message)
|
||||
}
|
||||
|
||||
func debugLog(_ message: String) {
|
||||
os_log("%{public}@", log: OSLog.default, type: .debug, message)
|
||||
os_log("%{public}@%{public}@", log: OSLog.default, type: .debug, processTag, message)
|
||||
}
|
||||
|
||||
func printError(errorMessage: String) {
|
||||
fputs("error: \(errorMessage)\n", stderr)
|
||||
fputs("error: \(processTag)\(errorMessage)\n", stderr)
|
||||
}
|
||||
|
||||
func printWarning(_ message: String) {
|
||||
print("warning: \(message)")
|
||||
print("warning: \(processTag)\(message)")
|
||||
}
|
||||
|
||||
/// Prints a message to the user. It shows in Xcode (if applies) or console output
|
||||
@@ -57,3 +59,7 @@ func printWarning(_ message: String) {
|
||||
func printToUser(_ message: String) {
|
||||
print("[RC] \(message)")
|
||||
}
|
||||
|
||||
func updateProcessTag(_ tag: String) {
|
||||
processTag = "(\(tag)) "
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -23,21 +23,38 @@ struct AWSV4Signature {
|
||||
|
||||
let secretKey: String
|
||||
let accessKey: String
|
||||
let securityToken: String?
|
||||
let region: String
|
||||
let service: String
|
||||
let date: Date
|
||||
|
||||
|
||||
func addSignatureHeaderTo(request: inout URLRequest) {
|
||||
|
||||
request.setValue(request.url?.host, forHTTPHeaderField: "host")
|
||||
request.setValue(StringToSign.ISO8601BasicFormatter.string(from: date), forHTTPHeaderField: "x-amz-date")
|
||||
request.setValue((request.httpBody ?? Data()).sha256(), forHTTPHeaderField: "x-amz-content-sha256")
|
||||
|
||||
if let securityToken = securityToken {
|
||||
request.setValue(securityToken, forHTTPHeaderField: "x-amz-security-token")
|
||||
}
|
||||
|
||||
let canonicalRequest = CanonicalRequest(request: request)
|
||||
let stringToSign = StringToSign(region: region, service: service, canonicalRequestHash: canonicalRequest.hash, date: date)
|
||||
let awsV4SigningKey = AWSV4SigningKey(secretAccessKey: secretKey, region: region, service: service, date: date)
|
||||
let signature = HMAC.calcHMAC(keyArray: awsV4SigningKey.value, value: stringToSign.value).map { String(format: "%02hhx", $0) }.joined()
|
||||
let stringToSign = StringToSign(
|
||||
region: region,
|
||||
service: service,
|
||||
canonicalRequestHash: canonicalRequest.hash,
|
||||
date: date
|
||||
)
|
||||
let awsV4SigningKey = AWSV4SigningKey(
|
||||
secretAccessKey: secretKey,
|
||||
region: region,
|
||||
service: service,
|
||||
date: date
|
||||
)
|
||||
let signature = HMAC.calcHMAC(
|
||||
keyArray: awsV4SigningKey.value,
|
||||
value: stringToSign.value
|
||||
).map { String(format: "%02hhx", $0) }.joined()
|
||||
|
||||
let authValue =
|
||||
"AWS4-HMAC-SHA256 " +
|
||||
|
||||
@@ -52,7 +52,14 @@ struct HMAC {
|
||||
|
||||
private static func calcHMAC(keyUnsafeBytes: UnsafeRawBufferPointer, value: String, out: UnsafeMutableRawPointer!) {
|
||||
value.data(using: .utf8)!.withUnsafeBytes { value in
|
||||
CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA256), keyUnsafeBytes.baseAddress, Int(keyUnsafeBytes.count), value.baseAddress, Int(value.count), out)
|
||||
CCHmac(
|
||||
CCHmacAlgorithm(kCCHmacAlgSHA256),
|
||||
keyUnsafeBytes.baseAddress,
|
||||
Int(keyUnsafeBytes.count),
|
||||
value.baseAddress,
|
||||
Int(value.count),
|
||||
out
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2021 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import Foundation
|
||||
|
||||
final class IgnoringCertificatesTrustManager: NSObject, URLSessionDelegate {
|
||||
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
||||
guard let serverTrust = challenge.protectionSpace.serverTrust else {
|
||||
completionHandler(.performDefaultHandling, nil)
|
||||
return
|
||||
}
|
||||
|
||||
let urlCredential = URLCredential(trust: serverTrust)
|
||||
completionHandler(.useCredential, urlCredential)
|
||||
}
|
||||
}
|
||||
@@ -29,12 +29,14 @@ class NetworkClientImpl: NetworkClient {
|
||||
private let session: URLSession
|
||||
private let fileManager: FileManager
|
||||
private let maxRetries: Int
|
||||
private let retryDelay: TimeInterval
|
||||
private let awsV4Signature: AWSV4Signature?
|
||||
|
||||
init(session: URLSession, retries: Int, fileManager: FileManager, awsV4Signature: AWSV4Signature?) {
|
||||
init(session: URLSession, retries: Int, retryDelay: TimeInterval, fileManager: FileManager, awsV4Signature: AWSV4Signature?) {
|
||||
self.session = session
|
||||
self.fileManager = fileManager
|
||||
maxRetries = retries
|
||||
self.maxRetries = retries
|
||||
self.retryDelay = retryDelay
|
||||
self.awsV4Signature = awsV4Signature
|
||||
}
|
||||
|
||||
@@ -173,7 +175,13 @@ class NetworkClientImpl: NetworkClient {
|
||||
if let error = responseError {
|
||||
if retries > 0 {
|
||||
infoLog("Upload request failed with \(error). Left retries: \(retries).")
|
||||
self.makeUploadRequest(request, input: input, retries: retries - 1, completion: completion)
|
||||
self.retryUpload(
|
||||
request,
|
||||
input: input,
|
||||
retries: retries,
|
||||
completion: completion,
|
||||
after: self.retryDelay
|
||||
)
|
||||
return
|
||||
}
|
||||
errorLog("Upload request failed: \(error)")
|
||||
@@ -184,6 +192,13 @@ class NetworkClientImpl: NetworkClient {
|
||||
}
|
||||
dataTask.resume()
|
||||
}
|
||||
|
||||
private func retryUpload(_ request: URLRequest, input: URL, retries: Int, completion: @escaping (Result<Void, NetworkClientError>) -> Void, after: TimeInterval) {
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + after) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.makeUploadRequest(request, input: input, retries: retries - 1, completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension NetworkClientError {
|
||||
|
||||
@@ -44,7 +44,7 @@ class RemoteNetworkClientAbstractFactory {
|
||||
return RemoteNetworkClientImpl(networkClient, downloadURLBuilder)
|
||||
}
|
||||
switch mode {
|
||||
case .producer:
|
||||
case .producer, .producerFast:
|
||||
let upstreamBuilders = try upstreamStreamURL.map(urlBuilderFactory)
|
||||
return ReplicatedRemotesNetworkClient(
|
||||
networkClient,
|
||||
|
||||
@@ -36,6 +36,17 @@ class DefaultURLSessionFactory: URLSessionFactory {
|
||||
let configuration = URLSessionConfiguration.default
|
||||
configuration.httpAdditionalHeaders = config.requestCustomHeaders
|
||||
configuration.timeoutIntervalForRequest = config.timeoutResponseDataChunksInterval
|
||||
return URLSession(configuration: configuration)
|
||||
configuration.urlCache?.memoryCapacity = 0
|
||||
configuration.urlCache?.diskCapacity = 0
|
||||
switch config.disableCertificateVerification {
|
||||
case true:
|
||||
return URLSession(
|
||||
configuration: configuration,
|
||||
delegate: IgnoringCertificatesTrustManager(),
|
||||
delegateQueue: nil
|
||||
)
|
||||
case false:
|
||||
return URLSession(configuration: configuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ protocol CacheHitLogger {
|
||||
/// Logs target hit or miss, based on an action of a build
|
||||
class ActionSpecificCacheHitLogger: CacheHitLogger {
|
||||
private let statsLogger: StatsLogger
|
||||
private let hitCounter: XCRemoteCacheStatistics.Counter
|
||||
private let missCounter: XCRemoteCacheStatistics.Counter
|
||||
private let hitCounter: XCRemoteCacheStatistics.Counter?
|
||||
private let missCounter: XCRemoteCacheStatistics.Counter?
|
||||
|
||||
init(action: BuildActionType, statsLogger: StatsLogger) {
|
||||
self.statsLogger = statsLogger
|
||||
@@ -37,19 +37,24 @@ class ActionSpecificCacheHitLogger: CacheHitLogger {
|
||||
case .index:
|
||||
hitCounter = .indexingTargetHitCount
|
||||
missCounter = .indexingTargetMissCount
|
||||
case .unknown:
|
||||
fallthrough
|
||||
case .build:
|
||||
hitCounter = .targetCacheHit
|
||||
missCounter = .targetCacheMiss
|
||||
case .unknown:
|
||||
hitCounter = nil
|
||||
missCounter = nil
|
||||
}
|
||||
}
|
||||
|
||||
func logHit() throws {
|
||||
try statsLogger.log(hitCounter)
|
||||
if let hitCounter = hitCounter {
|
||||
try statsLogger.log(hitCounter)
|
||||
}
|
||||
}
|
||||
|
||||
func logMiss() throws {
|
||||
try statsLogger.log(missCounter)
|
||||
if let missCounter = missCounter {
|
||||
try statsLogger.log(missCounter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,8 +76,8 @@ class ExclusiveFile: ExclusiveFileAccessor {
|
||||
guard flock(fd, LOCK_EX) == 0 else {
|
||||
throw FileAccessorError.lockingFailure
|
||||
}
|
||||
// While having a lock, make sure the file still exists.
|
||||
// It might delete it while we were waiting for a lock.
|
||||
// While having a lock, make sure the file still exists
|
||||
// It might delete it while we were waiting for a lock
|
||||
guard access(fileURL.path, F_OK) == 0 else {
|
||||
throw FileAccessorError.lockingFailure
|
||||
}
|
||||
|
||||
@@ -24,8 +24,15 @@ enum EnvironmentError: Error {
|
||||
}
|
||||
|
||||
extension Dictionary where Key == String, Value == String {
|
||||
func readEnv(key: String) throws -> URL {
|
||||
func readEnv(key: String) -> URL? {
|
||||
guard let value = self[key].map(URL.init(fileURLWithPath:)) else {
|
||||
return nil
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func readEnv(key: String) throws -> URL {
|
||||
guard let value: URL = readEnv(key: key) else {
|
||||
throw EnvironmentError.missingEnv(key)
|
||||
}
|
||||
return value
|
||||
|
||||
@@ -21,7 +21,7 @@ import Foundation
|
||||
import XCRemoteCache
|
||||
|
||||
/// Wrapper for a `LD` program that copies the dynamic executable from a cached-downloaded location
|
||||
/// Fallbacks to a standard `clang` when the Ramote cache is not applicable (e.g. modified sources)
|
||||
/// Fallbacks to a standard `clang` when the Remote cache is not applicable (e.g. modified sources)
|
||||
public class XCLDMain {
|
||||
public func main() {
|
||||
let args = ProcessInfo().arguments
|
||||
@@ -48,7 +48,16 @@ public class XCLDMain {
|
||||
i += 1
|
||||
}
|
||||
guard let outputInput = output, let filelistInput = filelist, let dependencyInfoInput = dependencyInfo else {
|
||||
exit(1, "Missing 'output' argument. Args: \(args)")
|
||||
let ldCommand = "clang"
|
||||
print("Fallbacking to compilation using \(ldCommand).")
|
||||
|
||||
let args = ProcessInfo().arguments
|
||||
let paramList = [ldCommand] + args.dropFirst()
|
||||
let cargs = paramList.map { strdup($0) } + [nil]
|
||||
execvp(ldCommand, cargs)
|
||||
|
||||
/// C-function `execv` returns only when the command fails
|
||||
exit(1)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// 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
|
||||
import XCRemoteCache
|
||||
|
||||
/// Wrapper for a `LDPLUSPLUS` program that copies the dynamic executable from a cached-downloaded location
|
||||
/// Fallbacks to a standard `clang++` when the Remote cache is not applicable (e.g. modified sources)
|
||||
public class XCLDPlusPlusMain {
|
||||
public func main() {
|
||||
let args = ProcessInfo().arguments
|
||||
var output: String?
|
||||
var filelist: String?
|
||||
var dependencyInfo: String?
|
||||
var i = 0
|
||||
while i < args.count {
|
||||
switch args[i] {
|
||||
case "-o":
|
||||
output = args[i + 1]
|
||||
i += 1
|
||||
case "-filelist":
|
||||
filelist = args[i + 1]
|
||||
i += 1
|
||||
case "-dependency_info":
|
||||
// Skip following `-Xlinker` argument. Sample call:
|
||||
// `clang -dynamiclib ... -Xlinker -dependency_info -Xlinker /path/Target_dependency_info.dat`
|
||||
dependencyInfo = args[i + 2]
|
||||
i += 2
|
||||
default:
|
||||
break
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
guard let outputInput = output, let filelistInput = filelist, let dependencyInfoInput = dependencyInfo else {
|
||||
let ldCommand = "clang++"
|
||||
print("Fallbacking to compilation using \(ldCommand).")
|
||||
|
||||
let args = ProcessInfo().arguments
|
||||
let paramList = [ldCommand] + args.dropFirst()
|
||||
let cargs = paramList.map { strdup($0) } + [nil]
|
||||
execvp(ldCommand, cargs)
|
||||
|
||||
/// C-function `execv` returns only when the command fails
|
||||
exit(1)
|
||||
}
|
||||
|
||||
|
||||
// TODO: consider using `clang_command` from .rcinfo
|
||||
/// concrete clang path should be taken from the current toolchain
|
||||
let fallbackCommand = "clang++"
|
||||
XCCreateBinary(
|
||||
output: outputInput,
|
||||
filelist: filelistInput,
|
||||
dependencyInfo: dependencyInfoInput,
|
||||
fallbackCommand: fallbackCommand,
|
||||
stepDescription: "xcldplusplus"
|
||||
).run()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// 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 XCRemoteCache
|
||||
|
||||
XCLDPlusPlusMain().main()
|
||||
@@ -60,8 +60,16 @@ public class XCSwiftcMain {
|
||||
let targetInputInput = target,
|
||||
let swiftFileListInput = swiftFileList
|
||||
else {
|
||||
print("Missing argument. Args: \(args)")
|
||||
exit(1)
|
||||
let swiftcCommand = "swiftc"
|
||||
print("Fallbacking to compilation using \(swiftcCommand).")
|
||||
|
||||
let args = ProcessInfo().arguments
|
||||
let paramList = [swiftcCommand] + args.dropFirst()
|
||||
let cargs = paramList.map { strdup($0) } + [nil]
|
||||
execvp(swiftcCommand, cargs)
|
||||
|
||||
/// C-function `execv` returns only when the command fails
|
||||
exit(1)
|
||||
}
|
||||
let swiftcArgsInput = SwiftcArgInput(
|
||||
objcHeaderOutput: objcHeaderOutputInput,
|
||||
|
||||
@@ -27,15 +27,18 @@ class ArtifactSwiftProductsBuilderImplTests: FileXCTestCase {
|
||||
private var swiftmoduleFile: URL!
|
||||
private var swiftmoduleDocFile: URL!
|
||||
private var swiftmoduleSourceInfoFile: URL!
|
||||
private var swiftmoduleInterfaceFile: URL!
|
||||
private var workingDir: URL!
|
||||
private var builder: ArtifactSwiftProductsBuilderImpl!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
let rootDir = try prepareTempDir()
|
||||
moduleDir = rootDir.appendingPathComponent("Products")
|
||||
swiftmoduleFile = moduleDir.appendingPathComponent("MyModule.swiftmodule")
|
||||
swiftmoduleDocFile = moduleDir.appendingPathComponent("MyModule.swiftdoc")
|
||||
swiftmoduleSourceInfoFile = moduleDir.appendingPathComponent("MyModule.swiftsourceinfo")
|
||||
swiftmoduleInterfaceFile = moduleDir.appendingPathComponent("MyModule.swiftinterface")
|
||||
workingDir = rootDir.appendingPathComponent("working")
|
||||
builder = ArtifactSwiftProductsBuilderImpl(
|
||||
workingDir: workingDir,
|
||||
@@ -47,24 +50,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 {
|
||||
func testIncludesAllBasicSwiftmoduleFiles() 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)
|
||||
|
||||
@@ -73,7 +93,38 @@ class ArtifactSwiftProductsBuilderImplTests: FileXCTestCase {
|
||||
XCTAssertTrue(fileManager.fileExists(atPath: expectedBuildedSwiftSourceInfoFile.path))
|
||||
}
|
||||
|
||||
func testIncludesAllEvolutionEnabledSwiftmoduleFiles() throws {
|
||||
try fileManager.spt_createEmptyFile(swiftmoduleFile)
|
||||
try fileManager.spt_createEmptyFile(swiftmoduleDocFile)
|
||||
try fileManager.spt_createEmptyFile(swiftmoduleSourceInfoFile)
|
||||
try fileManager.spt_createEmptyFile(swiftmoduleInterfaceFile)
|
||||
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 expectedBuildedSwiftInterfaceFile =
|
||||
builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftinterface")
|
||||
|
||||
try builder.includeModuleDefinitionsToTheArtifact(arch: "arm64", moduleURL: swiftmoduleFile)
|
||||
|
||||
XCTAssertTrue(fileManager.fileExists(atPath: expectedBuildedSwiftmoduleFile.path))
|
||||
XCTAssertTrue(fileManager.fileExists(atPath: expectedBuildedSwiftmoduledocFile.path))
|
||||
XCTAssertTrue(fileManager.fileExists(atPath: expectedBuildedSwiftSourceInfoFile.path))
|
||||
XCTAssertTrue(fileManager.fileExists(atPath: expectedBuildedSwiftInterfaceFile.path))
|
||||
}
|
||||
|
||||
func testFailsIncludingWhenMissingRequiredSwiftmoduleFiles() throws {
|
||||
XCTAssertThrowsError(try builder.includeModuleDefinitionsToTheArtifact(arch: "arm64", moduleURL: swiftmoduleFile))
|
||||
XCTAssertThrowsError(
|
||||
try builder.includeModuleDefinitionsToTheArtifact(
|
||||
arch: "arm64",
|
||||
moduleURL: swiftmoduleFile
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ class BuildArtifactCreatorTests: FileXCTestCase {
|
||||
private var swiftmoduleURL: URL!
|
||||
private var swiftdocURL: URL!
|
||||
private var swiftSourceInfoURL: URL!
|
||||
private var swiftInterfaceURL: URL!
|
||||
private var executablePath: String!
|
||||
private var executableURL: URL!
|
||||
private var creator: BuildArtifactCreator!
|
||||
@@ -50,6 +51,8 @@ class BuildArtifactCreatorTests: FileXCTestCase {
|
||||
.appendingPathComponent("Target.swiftdoc")
|
||||
swiftSourceInfoURL = workDirectory.appendingPathComponent("Objects-normal")
|
||||
.appendingPathComponent("Target.swiftsourceinfo")
|
||||
swiftInterfaceURL = workDirectory.appendingPathComponent("Objects-normal")
|
||||
.appendingPathComponent("Target.swiftinterface")
|
||||
executablePath = "libTarget.a"
|
||||
executableURL = buildDir.appendingPathComponent(executablePath)
|
||||
dSYM = executableURL.deletingPathExtension().appendingPathExtension(".dSYM")
|
||||
@@ -63,6 +66,7 @@ class BuildArtifactCreatorTests: FileXCTestCase {
|
||||
moduleName: "Target",
|
||||
modulesFolderPath: "",
|
||||
dSYMPath: dSYM,
|
||||
metaWriter: JsonMetaWriter(fileWriter: fileManager, pretty: false),
|
||||
fileManager: fileManager
|
||||
)
|
||||
}
|
||||
@@ -113,6 +117,28 @@ class BuildArtifactCreatorTests: FileXCTestCase {
|
||||
])
|
||||
}
|
||||
|
||||
func testPackagesEvolutionEnabledSwiftmoduleFiles() throws {
|
||||
try fileManager.spt_createEmptyFile(swiftmoduleURL)
|
||||
try fileManager.spt_createEmptyFile(swiftdocURL)
|
||||
try fileManager.spt_createEmptyFile(swiftSourceInfoURL)
|
||||
try fileManager.spt_createEmptyFile(swiftInterfaceURL)
|
||||
|
||||
try creator.includeModuleDefinitionsToTheArtifact(arch: "arch", moduleURL: swiftmoduleURL)
|
||||
let artifact = try creator.createArtifact(artifactKey: "key", meta: sampleMeta)
|
||||
|
||||
let unzippedURL = workDirectory.appendingPathComponent(UUID().uuidString)
|
||||
try Zip.unzipFile(artifact.package, destination: unzippedURL, overwrite: true, password: nil, progress: nil)
|
||||
let allFiles = try fileManager.spt_allFilesRecusively(unzippedURL)
|
||||
XCTAssertEqual(Set(allFiles), [
|
||||
unzippedURL.appendingPathComponent("libTarget.a"),
|
||||
unzippedURL.appendingPathComponent("fileKey.json"),
|
||||
unzippedURL.appendingPathComponent("swiftmodule/arch/Target.swiftmodule"),
|
||||
unzippedURL.appendingPathComponent("swiftmodule/arch/Target.swiftdoc"),
|
||||
unzippedURL.appendingPathComponent("swiftmodule/arch/Target.swiftsourceinfo"),
|
||||
unzippedURL.appendingPathComponent("swiftmodule/arch/Target.swiftinterface"),
|
||||
])
|
||||
}
|
||||
|
||||
func testFailsPackageWhenSwiftmoduleRelatedFilesAreMissing() throws {
|
||||
// Creating only `Target.swiftmodule`, without `.swiftdoc`
|
||||
try fileManager.spt_createEmptyFile(swiftmoduleURL)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+62
-1
@@ -33,7 +33,10 @@ class ThinningCreatorPluginTests: FileXCTestCase {
|
||||
targetTempDirRoot = workingDir.appendingPathComponent("Root")
|
||||
currentTargetTempDir = targetTempDirRoot.appendingPathComponent("Current.build")
|
||||
try fileManager.spt_createEmptyDir(currentTargetTempDir)
|
||||
plugin = ThinningCreatorPlugin(targetTempDir: currentTargetTempDir, dirScanner: FileManager.default)
|
||||
plugin = ThinningCreatorPlugin(
|
||||
targetTempDir: currentTargetTempDir,
|
||||
modeMarkerPath: "rc_marker.enabled",
|
||||
dirScanner: FileManager.default)
|
||||
}
|
||||
|
||||
func testReturnsEmptyExtraKeysForNoArtifacts() throws {
|
||||
@@ -73,4 +76,62 @@ class ThinningCreatorPluginTests: FileXCTestCase {
|
||||
|
||||
XCTAssertThrowsError(try plugin.extraMetaKeys(Self.sampleMeta))
|
||||
}
|
||||
|
||||
func testDefinesExtraMetaKeysForTargetsThatReusedArtifact() throws {
|
||||
let otherTargetTempDir = targetTempDirRoot.appendingPathComponent("Other.build")
|
||||
let marker = otherTargetTempDir.appendingPathComponent("rc_marker.enabled")
|
||||
let reusedArtifact = otherTargetTempDir
|
||||
.appendingPathComponent("xccache")
|
||||
.appendingPathComponent("123")
|
||||
.appendingPathExtension("zip")
|
||||
try fileManager.spt_createEmptyFile(marker)
|
||||
try fileManager.spt_createEmptyFile(reusedArtifact)
|
||||
|
||||
let extraKeys = try plugin.extraMetaKeys(Self.sampleMeta)
|
||||
|
||||
XCTAssertEqual(extraKeys, ["thinning_Other": "123"])
|
||||
}
|
||||
|
||||
func testFailsGeneratingExtraMetaKeysForTwoArtifactsInTargetTempDir() throws {
|
||||
let otherTargetTempDir = targetTempDirRoot.appendingPathComponent("Other.build")
|
||||
let marker = otherTargetTempDir.appendingPathComponent("rc_marker.enabled")
|
||||
let reusedArtifact1 = otherTargetTempDir
|
||||
.appendingPathComponent("xccache")
|
||||
.appendingPathComponent("001")
|
||||
.appendingPathExtension("zip")
|
||||
let reusedArtifact2 = otherTargetTempDir
|
||||
.appendingPathComponent("xccache")
|
||||
.appendingPathComponent("002")
|
||||
.appendingPathExtension("zip")
|
||||
try fileManager.spt_createEmptyFile(marker)
|
||||
try fileManager.spt_createEmptyFile(reusedArtifact1)
|
||||
try fileManager.spt_createEmptyFile(reusedArtifact2)
|
||||
|
||||
XCTAssertThrowsError(try plugin.extraMetaKeys(Self.sampleMeta))
|
||||
}
|
||||
|
||||
func testDefinesExtraMetaKeysForGeneratedAndReusedArtifact() throws {
|
||||
let otherTargetTempDir = targetTempDirRoot.appendingPathComponent("Generated.build")
|
||||
let generatedArtifact = otherTargetTempDir
|
||||
.appendingPathComponent("xccache")
|
||||
.appendingPathComponent("produced")
|
||||
.appendingPathComponent("000")
|
||||
.appendingPathExtension("zip")
|
||||
try fileManager.spt_createEmptyFile(generatedArtifact)
|
||||
let reusedTargetTempDir = targetTempDirRoot.appendingPathComponent("Reused.build")
|
||||
let marker = reusedTargetTempDir.appendingPathComponent("rc_marker.enabled")
|
||||
let reusedArtifact = reusedTargetTempDir
|
||||
.appendingPathComponent("xccache")
|
||||
.appendingPathComponent("999")
|
||||
.appendingPathExtension("zip")
|
||||
try fileManager.spt_createEmptyFile(marker)
|
||||
try fileManager.spt_createEmptyFile(reusedArtifact)
|
||||
|
||||
let extraKeys = try plugin.extraMetaKeys(Self.sampleMeta)
|
||||
|
||||
XCTAssertEqual(extraKeys, [
|
||||
"thinning_Generated": "000",
|
||||
"thinning_Reused": "999",
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,9 @@ 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" ,
|
||||
"DERIVED_FILE_DIR": "DERIVED_FILE_DIR",
|
||||
"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 +43,7 @@ class PostbuildContextTests: FileXCTestCase {
|
||||
"DWARF_DSYM_FILE_NAME": "DWARF_DSYM_FILE_NAME",
|
||||
"BUILT_PRODUCTS_DIR": "BUILT_PRODUCTS_DIR",
|
||||
"DERIVED_SOURCES_DIR": "DERIVED_SOURCES_DIR",
|
||||
"CURRENT_VARIANT": "normal",
|
||||
]
|
||||
|
||||
override func setUpWithError() throws {
|
||||
@@ -87,4 +89,45 @@ class PostbuildContextTests: FileXCTestCase {
|
||||
|
||||
XCTAssertEqual(context.action, .index)
|
||||
}
|
||||
|
||||
func testReadsSingleArch() throws {
|
||||
var envs = Self.SampleEnvs
|
||||
envs["ARCHS"] = "x86_64"
|
||||
let context = try PostbuildContext(config, env: envs)
|
||||
|
||||
XCTAssertEqual(context.arch, "x86_64")
|
||||
}
|
||||
|
||||
func testReadsFirstArch() throws {
|
||||
var envs = Self.SampleEnvs
|
||||
envs["ARCHS"] = "x86_64 arm64"
|
||||
let context = try PostbuildContext(config, env: envs)
|
||||
|
||||
XCTAssertEqual(context.arch, "x86_64")
|
||||
}
|
||||
|
||||
func testFailsForEmptyArch() throws {
|
||||
var envs = Self.SampleEnvs
|
||||
envs["ARCHS"] = ""
|
||||
|
||||
XCTAssertThrowsError(try PostbuildContext(config, env: envs))
|
||||
}
|
||||
|
||||
func testFailsForMissingArch() throws {
|
||||
var envs = Self.SampleEnvs
|
||||
envs["ARCHS"] = nil
|
||||
|
||||
XCTAssertThrowsError(try PostbuildContext(config, env: envs))
|
||||
}
|
||||
|
||||
func testFindTempDirForCustomVariant() throws {
|
||||
var envs = Self.SampleEnvs
|
||||
envs["ARCHS"] = "x86_64"
|
||||
envs["CURRENT_VARIANT"] = "custom"
|
||||
envs["OBJECT_FILE_DIR_custom"] = "/OBJECT_FILE_DIR_custom"
|
||||
|
||||
let context = try PostbuildContext(config, env: envs)
|
||||
|
||||
XCTAssertEqual(context.compilationTempDir, "/OBJECT_FILE_DIR_custom/x86_64")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,12 +20,14 @@
|
||||
@testable import XCRemoteCache
|
||||
import XCTest
|
||||
|
||||
// swiftlint:disable file_length
|
||||
// swiftlint:disable:next type_body_length
|
||||
class PostbuildTests: FileXCTestCase {
|
||||
private var postbuildContext = PostbuildContext(
|
||||
mode: .producer,
|
||||
targetName: "",
|
||||
targetTempDir: "",
|
||||
derivedFilesDir: "",
|
||||
compilationTempDir: "",
|
||||
configuration: "",
|
||||
platform: "",
|
||||
@@ -50,7 +52,10 @@ class PostbuildTests: FileXCTestCase {
|
||||
bundleDir: nil,
|
||||
derivedSourcesDir: "",
|
||||
thinnedTargets: [],
|
||||
action: .build
|
||||
action: .build,
|
||||
modeMarkerPath: "",
|
||||
overlayHeadersPath: "",
|
||||
irrelevantDependenciesPaths: []
|
||||
)
|
||||
private var network = RemoteNetworkClientImpl(
|
||||
NetworkClientFake(fileManager: .default),
|
||||
@@ -75,7 +80,9 @@ class PostbuildTests: FileXCTestCase {
|
||||
product: "/Product",
|
||||
source: "/Source",
|
||||
intermediate: "/Intermediate",
|
||||
bundle: nil
|
||||
derivedFiles: "/DerivedFiles",
|
||||
bundle: nil,
|
||||
skippedRegexes: []
|
||||
)
|
||||
private var overrideManager = FingerprintOverrideManagerImpl(
|
||||
overridingFileExtensions: ["swiftmodule"],
|
||||
@@ -84,6 +91,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
)
|
||||
private var modeController = CacheModeControllerFake()
|
||||
private var metaReader = JsonMetaReader(fileAccessor: FileManager.default)
|
||||
private var metaWriter = JsonMetaWriter(fileWriter: FileManager.default, pretty: false)
|
||||
private static let SampleMeta = MainArtifactSampleMeta.defaults
|
||||
private var sampleMetaFile: URL!
|
||||
|
||||
@@ -122,6 +130,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: []
|
||||
)
|
||||
@@ -151,6 +160,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: []
|
||||
)
|
||||
@@ -178,6 +188,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: dsymOrganizer,
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: []
|
||||
)
|
||||
@@ -205,6 +216,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: dsymOrganizer,
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: []
|
||||
)
|
||||
@@ -242,6 +254,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: dsymOrganizer,
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: []
|
||||
)
|
||||
@@ -282,6 +295,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: dsymOrganizer,
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: []
|
||||
)
|
||||
@@ -307,6 +321,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: fakeModeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: []
|
||||
)
|
||||
@@ -354,6 +369,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [plugin],
|
||||
consumerPlugins: []
|
||||
)
|
||||
@@ -385,6 +401,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [plugin],
|
||||
consumerPlugins: []
|
||||
)
|
||||
@@ -417,6 +434,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: [consumerPlugin]
|
||||
)
|
||||
@@ -454,6 +472,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: [consumerPlugin]
|
||||
)
|
||||
@@ -485,6 +504,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: [consumerPlugin]
|
||||
)
|
||||
@@ -518,6 +538,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: []
|
||||
)
|
||||
@@ -550,6 +571,7 @@ class PostbuildTests: FileXCTestCase {
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: []
|
||||
)
|
||||
@@ -558,5 +580,67 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,8 @@ class PrebuildTests: FileXCTestCase {
|
||||
forceCached: false,
|
||||
compilationHistoryFile: compilationHistory,
|
||||
turnOffRemoteCacheOnFirstTimeout: true,
|
||||
targetName: ""
|
||||
targetName: "",
|
||||
overlayHeadersPath: ""
|
||||
)
|
||||
contextCached = PrebuildContext(
|
||||
targetTempDir: sampleURL,
|
||||
@@ -74,7 +75,8 @@ class PrebuildTests: FileXCTestCase {
|
||||
forceCached: true,
|
||||
compilationHistoryFile: compilationHistory,
|
||||
turnOffRemoteCacheOnFirstTimeout: true,
|
||||
targetName: ""
|
||||
targetName: "",
|
||||
overlayHeadersPath: ""
|
||||
)
|
||||
organizer = ArtifactOrganizerFake(artifactRoot: artifactsRoot, unzippedExtension: "unzip")
|
||||
globalCacheSwitcher = InMemoryGlobalCacheSwitcher()
|
||||
@@ -238,7 +240,8 @@ class PrebuildTests: FileXCTestCase {
|
||||
forceCached: false,
|
||||
compilationHistoryFile: compilationHistory,
|
||||
turnOffRemoteCacheOnFirstTimeout: true,
|
||||
targetName: ""
|
||||
targetName: "",
|
||||
overlayHeadersPath: ""
|
||||
)
|
||||
|
||||
let prebuild = Prebuild(
|
||||
@@ -268,7 +271,8 @@ class PrebuildTests: FileXCTestCase {
|
||||
forceCached: false,
|
||||
compilationHistoryFile: compilationHistory,
|
||||
turnOffRemoteCacheOnFirstTimeout: true,
|
||||
targetName: ""
|
||||
targetName: "",
|
||||
overlayHeadersPath: ""
|
||||
)
|
||||
metaContent = try generateMeta(fingerprint: generator.generate(), filekey: "1")
|
||||
let downloadedArtifactPackage = artifactsRoot.appendingPathComponent("1")
|
||||
@@ -330,7 +334,8 @@ class PrebuildTests: FileXCTestCase {
|
||||
forceCached: false,
|
||||
compilationHistoryFile: compilationHistory,
|
||||
turnOffRemoteCacheOnFirstTimeout: false,
|
||||
targetName: ""
|
||||
targetName: "",
|
||||
overlayHeadersPath: ""
|
||||
)
|
||||
try globalCacheSwitcher.enable(sha: "1")
|
||||
let prebuild = Prebuild(
|
||||
|
||||
@@ -28,6 +28,7 @@ class FileLLDBInitPatcherTests: XCTestCase {
|
||||
private var patcher: FileLLDBInitPatcher!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
accessor = FileAccessorFake(mode: .normal)
|
||||
patcher = FileLLDBInitPatcher(
|
||||
file: lldbInitPath,
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
// 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 XcodeProjBuildSettingsIntegrateAppenderTests: XCTestCase {
|
||||
private let rootURL: URL = "/root"
|
||||
private let binariesDir: URL = "/binaries"
|
||||
private var buildSettings: BuildSettings!
|
||||
private var binaries: XCRCBinariesPaths!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
buildSettings = BuildSettings()
|
||||
binaries = XCRCBinariesPaths(
|
||||
prepare: binariesDir.appendingPathComponent("xcprepare"),
|
||||
cc: binariesDir.appendingPathComponent("xccc"),
|
||||
swiftc: binariesDir.appendingPathComponent("xcswiftc"),
|
||||
libtool: binariesDir.appendingPathComponent("xclibtool"),
|
||||
ld: binariesDir.appendingPathComponent("xcld"),
|
||||
ldplusplus: binariesDir.appendingPathComponent("xcldplusplus"),
|
||||
prebuild: binariesDir.appendingPathComponent("xcprebuild"),
|
||||
postbuild: binariesDir.appendingPathComponent("xcpostbuild")
|
||||
)
|
||||
}
|
||||
|
||||
func testProducerSettingFakeSrcRoot() throws {
|
||||
let mode: Mode = .producer
|
||||
let fakeRootURL: URL = "/xxxxxxxxxxP"
|
||||
let appender = XcodeProjBuildSettingsIntegrateAppender(mode: mode, repoRoot: rootURL, fakeSrcRoot: fakeRootURL)
|
||||
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
|
||||
let resultURL = try XCTUnwrap(result["XCRC_FAKE_SRCROOT"] as? String)
|
||||
|
||||
XCTAssertEqual(resultURL, fakeRootURL.path)
|
||||
}
|
||||
|
||||
func testConsumerSettingFakeSrcRoot() throws {
|
||||
let mode: Mode = .consumer
|
||||
let fakeRootURL: URL = "/xxxxxxxxxxC"
|
||||
let appender = XcodeProjBuildSettingsIntegrateAppender(mode: mode, repoRoot: rootURL, fakeSrcRoot: fakeRootURL)
|
||||
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
|
||||
let resultURL: String = try XCTUnwrap(result["XCRC_FAKE_SRCROOT"] as? String)
|
||||
|
||||
XCTAssertEqual(resultURL, fakeRootURL.path)
|
||||
}
|
||||
}
|
||||
@@ -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,12 @@ 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"
|
||||
)
|
||||
let artifactSwiftInterfaceInfo = URL(
|
||||
fileURLWithPath: "/cachedArtifact/swiftmodule/archTest/Target.swiftinterface"
|
||||
)
|
||||
|
||||
artifactOrganizer = ArtifactOrganizerFake(artifactRoot: artifactRoot)
|
||||
let swiftc = Swiftc(
|
||||
@@ -301,12 +306,14 @@ class SwiftcTests: FileXCTestCase {
|
||||
let swiftModuleURL = swiftModuleFiles.0[.swiftmodule]
|
||||
let swiftDocURL = swiftModuleFiles.0[.swiftdoc]
|
||||
let swiftSourceInfoURL = swiftModuleFiles.0[.swiftsourceinfo]
|
||||
let swiftInterfaceURL = swiftModuleFiles.0[.swiftinterface]
|
||||
let swiftHeaderURL = swiftModuleFiles.1
|
||||
|
||||
XCTAssertEqual(swiftModuleURL, artifactSwiftmodule)
|
||||
XCTAssertEqual(swiftDocURL, artifactSwiftdoc)
|
||||
XCTAssertEqual(swiftSourceInfoURL, artifactSwiftSourceInfo)
|
||||
XCTAssertEqual(swiftHeaderURL, artifactObjCHeader)
|
||||
XCTAssertEqual(swiftInterfaceURL, artifactSwiftInterfaceInfo)
|
||||
}
|
||||
|
||||
|
||||
@@ -457,5 +464,5 @@ class SwiftcTests: FileXCTestCase {
|
||||
)
|
||||
|
||||
XCTAssertNoThrow(try swiftc.mockCompilation())
|
||||
}
|
||||
} // swiftlint:disable:next file_length
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ class TemplateBasedCCWrapperBuilderTests: FileXCTestCase {
|
||||
private static let history = "history.compile"
|
||||
private static let prebuild = "prebuild.d"
|
||||
private static let commitSha = "321"
|
||||
private static let timeout = 5.0
|
||||
private static let timeout = 10.0
|
||||
|
||||
static let xccc: URL = {
|
||||
let fileManager = FileManager.default
|
||||
@@ -47,7 +47,8 @@ class TemplateBasedCCWrapperBuilderTests: FileXCTestCase {
|
||||
let app = appDir.appendingPathComponent("xccc")
|
||||
try? fileManager.removeItem(at: appDir)
|
||||
try? FileManager.default.createDirectory(at: appDir, withIntermediateDirectories: true, attributes: nil)
|
||||
try? builder.compile(to: app, commitSha: commitSha)
|
||||
// swiftlint:disable:next force_try
|
||||
try! builder.compile(to: app, commitSha: commitSha)
|
||||
return app
|
||||
}()
|
||||
|
||||
@@ -332,11 +333,31 @@ class TemplateBasedCCWrapperBuilderTests: FileXCTestCase {
|
||||
XCTAssertNotEqual(newFileOutputData, Data())
|
||||
}
|
||||
|
||||
func testPCHCompilationFallbacks() throws {
|
||||
// Marker is empty to mimic the new file scenario
|
||||
let pchFile = directory.appendingPathComponent("input.pch")
|
||||
createValidPCHFile(pchFile)
|
||||
arguments = ["-x", "objective-c-header", "-MF", dependencyFile.path, "-o", outputFile.path, pchFile.path]
|
||||
|
||||
try shellExec(Self.xccc.path, args: arguments, inDir: directory.path)
|
||||
|
||||
XCTAssertTrue(fileManager.fileExists(atPath: outputFile.path))
|
||||
}
|
||||
|
||||
/// Creates a simple C code in the location
|
||||
private func createValidCFile(_ location: URL) {
|
||||
fileManager.createFile(atPath: location.path, contents: "int main(){}".data(using: .utf8), attributes: nil)
|
||||
}
|
||||
|
||||
/// Creates a simple PCH code in the location
|
||||
private func createValidPCHFile(_ location: URL) {
|
||||
fileManager.createFile(
|
||||
atPath: location.path,
|
||||
contents: "#import <Availability.h>".data(using: .utf8),
|
||||
attributes: nil
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a C code that requires extra CUSTOM_STR clang macro to compile
|
||||
private func createInvalidCFile(_ location: URL) {
|
||||
fileManager.createFile(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright (c) 2022 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 XCRemoteCacheConfigReaderTests: XCTestCase {
|
||||
|
||||
private var fileReader: FileAccessorFake!
|
||||
private var reader: XCRemoteCacheConfigReader!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
fileReader = FileAccessorFake(mode: .normal)
|
||||
reader = XCRemoteCacheConfigReader(srcRootPath: "/", fileReader: fileReader)
|
||||
}
|
||||
|
||||
func testReadsFromExtraConfig() throws {
|
||||
try fileReader.write(toPath: "/.rcinfo", contents: "cache_addresses: [test]")
|
||||
|
||||
let config = try reader.readConfiguration()
|
||||
|
||||
XCTAssertEqual(config.cacheAddresses, ["test"])
|
||||
}
|
||||
|
||||
func testOverridesExtraConfigFromExtraFile() throws {
|
||||
try fileReader.write(toPath: "/.rcinfo", contents: "cache_addresses: [test]")
|
||||
try fileReader.write(toPath: "/user.rcinfo", contents: "cache_addresses: [user]")
|
||||
|
||||
let config = try reader.readConfiguration()
|
||||
|
||||
XCTAssertEqual(config.cacheAddresses, ["user"])
|
||||
}
|
||||
|
||||
func testReadsExtraConfigMultipleTimes() throws {
|
||||
try fileReader.write(toPath: "/.rcinfo", contents: "cache_addresses: [test]")
|
||||
try fileReader.write(toPath: "/user.rcinfo", contents: """
|
||||
cache_addresses: [user]
|
||||
extra_configuration_file: user2.rcinfo
|
||||
""")
|
||||
try fileReader.write(toPath: "/user2.rcinfo", contents: "cache_addresses: [user2]")
|
||||
|
||||
let config = try reader.readConfiguration()
|
||||
|
||||
XCTAssertEqual(config.cacheAddresses, ["user2"])
|
||||
}
|
||||
|
||||
func testBreaksImportingExtraConfigIfReachingALoop() throws {
|
||||
try fileReader.write(toPath: "/.rcinfo", contents: "cache_addresses: [test]")
|
||||
try fileReader.write(toPath: "/user.rcinfo", contents: """
|
||||
cache_addresses: [user]
|
||||
extra_configuration_file: .rcinfo
|
||||
""")
|
||||
|
||||
let config = try reader.readConfiguration()
|
||||
|
||||
XCTAssertEqual(config.cacheAddresses, ["user"])
|
||||
}
|
||||
|
||||
func testBreaksImportingExtraConfigIfFileDoesntExist() throws {
|
||||
try fileReader.write(toPath: "/.rcinfo", contents: "cache_addresses: [test]")
|
||||
try fileReader.write(toPath: "/user.rcinfo", contents: """
|
||||
cache_addresses: [user]
|
||||
extra_configuration_file: nonexisting.rcinfo
|
||||
""")
|
||||
|
||||
let config = try reader.readConfiguration()
|
||||
|
||||
XCTAssertEqual(config.cacheAddresses, ["user"])
|
||||
XCTAssertEqual(config.extraConfigurationFile, "nonexisting.rcinfo")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
// 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 DependenciesReaderPerformanceTest: XCTestCase {
|
||||
|
||||
private static let resourcesSubdirectory = "TestData/Dependencies/DependenciesReaderPerformanceTest"
|
||||
|
||||
private func pathForTestData(name: String) throws -> URL {
|
||||
return try XCTUnwrap(Bundle.module.url(
|
||||
forResource: name,
|
||||
withExtension: "d",
|
||||
subdirectory: DependenciesReaderPerformanceTest.resourcesSubdirectory
|
||||
))
|
||||
}
|
||||
|
||||
func testFindDependenciesPerformance() throws {
|
||||
let file = try pathForTestData(name: "dependencies")
|
||||
let reader = FileDependenciesReader(file, accessor: FileManager.default)
|
||||
|
||||
self.measure { // 0.005
|
||||
do {
|
||||
_ = try reader.findDependencies()
|
||||
} catch {
|
||||
print("Error reading dependencies")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testReadRawFilePerformance() throws {
|
||||
let file = try pathForTestData(name: "dependencies")
|
||||
let reader = FileDependenciesReader(file, accessor: FileManager.default)
|
||||
|
||||
self.measure { // 0.002
|
||||
do {
|
||||
_ = try reader.readRaw()
|
||||
} catch {
|
||||
print("Error reading dependencies")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testGetFileDataPerformance() throws {
|
||||
let file = try pathForTestData(name: "dependencies")
|
||||
let reader = FileDependenciesReader(file, accessor: FileManager.default)
|
||||
|
||||
self.measure { // 0.00008
|
||||
do {
|
||||
_ = try reader.getFileData()
|
||||
} catch {
|
||||
print("Error reading dependencies")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testGetFileStringPerformance() throws {
|
||||
let file = try pathForTestData(name: "dependencies")
|
||||
let reader = FileDependenciesReader(file, accessor: FileManager.default)
|
||||
let fileData = try reader.getFileData()
|
||||
|
||||
self.measure { // 0.00002
|
||||
do {
|
||||
_ = try reader.getFileStringFromData(fileData: fileData)
|
||||
} catch {
|
||||
print("Error reading dependencies")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testGetYamlPerformance() throws { // 0.222
|
||||
let file = try pathForTestData(name: "dependencies")
|
||||
let reader = FileDependenciesReader(file, accessor: FileManager.default)
|
||||
let fileData = try reader.getFileData()
|
||||
let fileString = try reader.getFileStringFromData(fileData: fileData)
|
||||
|
||||
self.measure { // 0.0022
|
||||
do {
|
||||
_ = try reader.getYaml(fileString: fileString)
|
||||
} catch {
|
||||
print("Error reading dependencies")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testParseDependencyFileListUsingUTF8View() throws {
|
||||
let file = try pathForTestData(name: "dependencies")
|
||||
let reader = FileDependenciesReader(file, accessor: FileManager.default)
|
||||
let fileData = try reader.getFileData()
|
||||
let fileString = try reader.getFileStringFromData(fileData: fileData)
|
||||
let yaml = try reader.getYaml(fileString: fileString)
|
||||
|
||||
guard let dependencies = yaml["dependencies"] else {
|
||||
XCTAssertTrue(false)
|
||||
return
|
||||
}
|
||||
|
||||
self.measure { // 0.004
|
||||
let deps = reader.parseDependencyFileList(dependencies)
|
||||
XCTAssertTrue(deps.count == 1000)
|
||||
}
|
||||
}
|
||||
|
||||
func testDeprecatedParseDependenciesFilesListOfAnObjectUsingUTF8View() throws {
|
||||
let file = try pathForTestData(name: "dependencies")
|
||||
let reader = FileDependenciesReader(file, accessor: FileManager.default)
|
||||
let fileData = try reader.getFileData()
|
||||
let fileString = try reader.getFileStringFromData(fileData: fileData)
|
||||
let yaml = try reader.getYaml(fileString: fileString)
|
||||
|
||||
guard let dependencies = yaml["/This/Is/A/Path/To/Some/Object/objectfile.o"] else {
|
||||
XCTAssertTrue(false)
|
||||
return
|
||||
}
|
||||
|
||||
self.measure { // 0.00048
|
||||
let deps = reader.parseDependencyFileList(dependencies)
|
||||
XCTAssertTrue(deps.count == 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,53 +29,120 @@ class DependenciesRemapperCompositeTests: XCTestCase {
|
||||
StringDependenciesRemapper.Mapping(generic: "$(PWD)", local: "/pwd"),
|
||||
]
|
||||
|
||||
func testNoRemappersIsTransparent() {
|
||||
func testNoRemappersIsTransparent() throws {
|
||||
let remapper = DependenciesRemapperComposite([])
|
||||
|
||||
let genericPath = remapper.replace(localPaths: ["/tmp/root/some.swift"])
|
||||
let genericPath = try remapper.replace(localPaths: ["/tmp/root/some.swift"])
|
||||
|
||||
XCTAssertEqual(genericPath, ["/tmp/root/some.swift"])
|
||||
}
|
||||
|
||||
func testOneRemapperReplacesLocalPaths() {
|
||||
func testOneRemapperReplacesLocalPaths() throws {
|
||||
let remapper = DependenciesRemapperComposite([
|
||||
StringDependenciesRemapper(mappings: mappings1),
|
||||
])
|
||||
|
||||
let genericPath = remapper.replace(localPaths: ["/tmp/root/some.swift"])
|
||||
let genericPath = try remapper.replace(localPaths: ["/tmp/root/some.swift"])
|
||||
|
||||
XCTAssertEqual(genericPath, ["$(SRC_ROOT)/some.swift"])
|
||||
}
|
||||
|
||||
func testOneRemapperReplacesGenericPaths() {
|
||||
func testOneRemapperReplacesGenericPaths() throws {
|
||||
let remapper = DependenciesRemapperComposite([
|
||||
StringDependenciesRemapper(mappings: mappings1),
|
||||
])
|
||||
|
||||
let localPath = remapper.replace(genericPaths: ["$(SRC_ROOT)/some.swift"])
|
||||
let localPath = try remapper.replace(genericPaths: ["$(SRC_ROOT)/some.swift"])
|
||||
|
||||
XCTAssertEqual(localPath, ["/tmp/root/some.swift"])
|
||||
}
|
||||
|
||||
func testTwoRemappersReplacesLocalPaths() {
|
||||
func testTwoRemappersReplacesLocalPaths() throws {
|
||||
let remapper = DependenciesRemapperComposite([
|
||||
StringDependenciesRemapper(mappings: mappings1),
|
||||
StringDependenciesRemapper(mappings: mappings2),
|
||||
])
|
||||
|
||||
let genericPath = remapper.replace(localPaths: ["/tmp/root/some.swift", "/pwd/other.swift"])
|
||||
let genericPath = try remapper.replace(localPaths: ["/tmp/root/some.swift", "/pwd/other.swift"])
|
||||
|
||||
XCTAssertEqual(genericPath, ["$(SRC_ROOT)/some.swift", "$(PWD)/other.swift"])
|
||||
}
|
||||
|
||||
func testOneRemappersReplacesGenericPaths() {
|
||||
func testOneRemappersReplacesGenericPaths() throws {
|
||||
let remapper = DependenciesRemapperComposite([
|
||||
StringDependenciesRemapper(mappings: mappings1),
|
||||
StringDependenciesRemapper(mappings: mappings2),
|
||||
])
|
||||
|
||||
let localPath = remapper.replace(genericPaths: ["$(SRC_ROOT)/some.swift", "$(PWD)/other.swift"])
|
||||
let localPath = try remapper.replace(genericPaths: ["$(SRC_ROOT)/some.swift", "$(PWD)/other.swift"])
|
||||
|
||||
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 = try 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 = try 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 = try remapper.replace(localPaths: localPaths)
|
||||
let remappedLocalPaths = try remapper.replace(genericPaths: genericPaths)
|
||||
|
||||
XCTAssertEqual(localPaths, remappedLocalPaths)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// 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 FileDependenciesWriterTests: XCTestCase {
|
||||
|
||||
private func generateTempFileURL(name: String = #function) throws -> URL {
|
||||
let directory = NSTemporaryDirectory()
|
||||
return try NSURL.fileURL(withPathComponents: [directory, name]).unwrap()
|
||||
}
|
||||
|
||||
func testWriteDependencyWithSpace() throws {
|
||||
let url = try generateTempFileURL()
|
||||
let writer = FileDependenciesWriter(url, accessor: FileManager.default)
|
||||
|
||||
try writer.writeGeneric(dependencies: [
|
||||
"/SomePath/Pods/Target Support Files/lottie-ios/lottie-ios-dummy.m",
|
||||
])
|
||||
|
||||
let expectedContent = """
|
||||
dependencies: /SomePath/Pods/Target\\ Support\\ Files/lottie-ios/lottie-ios-dummy.m
|
||||
|
||||
"""
|
||||
|
||||
let content = String(data: try Data(contentsOf: url), encoding: .utf8)
|
||||
|
||||
XCTAssertEqual(content, expectedContent)
|
||||
}
|
||||
}
|
||||
@@ -20,24 +20,28 @@
|
||||
@testable import XCRemoteCache
|
||||
import XCTest
|
||||
|
||||
class DependencyProcessorImplTests: XCTestCase {
|
||||
class DependencyProcessorImplTests: FileXCTestCase {
|
||||
|
||||
let processor = DependencyProcessorImpl(
|
||||
xcode: "/Xcode",
|
||||
product: "/Product",
|
||||
source: "/Source",
|
||||
intermediate: "/Intermediate",
|
||||
bundle: "/Bundle"
|
||||
derivedFiles: "/DerivedFiles",
|
||||
bundle: "/Bundle",
|
||||
skippedRegexes: []
|
||||
)
|
||||
|
||||
func testIntermediateFileIsSkippedForProductAndSourceSubdirectory() {
|
||||
func testIntermediateFileIsskippedRegexesForProductAndSourceSubdirectory() {
|
||||
let intermediateFile: URL = "/Intermediate/some"
|
||||
let processor = DependencyProcessorImpl(
|
||||
xcode: "/Xcode",
|
||||
product: "/",
|
||||
source: "/",
|
||||
intermediate: "/Intermediate",
|
||||
bundle: nil
|
||||
derivedFiles: "/DerivedFiles",
|
||||
bundle: nil,
|
||||
skippedRegexes: []
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
@@ -46,14 +50,16 @@ class DependencyProcessorImplTests: XCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
func testBundleFileIsSkippedForProductAndSourceSubdirectory() {
|
||||
func testBundleFileIsskippedRegexesForProductAndSourceSubdirectory() {
|
||||
let bundleFile: URL = "/Bundle/some"
|
||||
let processor = DependencyProcessorImpl(
|
||||
xcode: "/Xcode",
|
||||
product: "/",
|
||||
source: "/",
|
||||
intermediate: "/Intermediate",
|
||||
bundle: "/Bundle"
|
||||
derivedFiles: "/DerivedFiles",
|
||||
bundle: "/Bundle",
|
||||
skippedRegexes: []
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
@@ -62,6 +68,22 @@ class DependencyProcessorImplTests: XCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
func testFiltersOutGeneratedSwiftHeaders() throws {
|
||||
let dependencies = processor.process([
|
||||
"/DerivedFiles/ModuleName-Swift.h",
|
||||
])
|
||||
|
||||
XCTAssertEqual(dependencies, [])
|
||||
}
|
||||
|
||||
func testFiltersOutDerivedFile() throws {
|
||||
let dependencies = processor.process([
|
||||
"/DerivedFiles/output.h",
|
||||
])
|
||||
|
||||
XCTAssertEqual(dependencies, [])
|
||||
}
|
||||
|
||||
func testFiltersOutProductModulemap() throws {
|
||||
let dependencies = processor.process([
|
||||
"/Product/some.modulemap",
|
||||
@@ -70,6 +92,14 @@ class DependencyProcessorImplTests: XCTestCase {
|
||||
XCTAssertEqual(dependencies, [])
|
||||
}
|
||||
|
||||
func testDoesNotFilterOutOtherProductModulemap() throws {
|
||||
let dependencies = processor.process([
|
||||
"/ProductOther/some.modulemap",
|
||||
])
|
||||
|
||||
XCTAssertEqual(dependencies, [.init(url: "/ProductOther/some.modulemap", type: .unknown)])
|
||||
}
|
||||
|
||||
func testDoesNotFilterOutNonProductModulemap() throws {
|
||||
let dependencies = processor.process([
|
||||
"/Source/some.modulemap",
|
||||
@@ -109,4 +139,152 @@ class DependencyProcessorImplTests: XCTestCase {
|
||||
|
||||
XCTAssertEqual(dependencies, [.init(url: "/xxx/some", type: .unknown)])
|
||||
}
|
||||
|
||||
func testFiltersOutIntermediateBySymlink() throws {
|
||||
let sampleDir = try prepareTempDir()
|
||||
|
||||
let intermediateDirReal = sampleDir.appendingPathComponent("Intermediate")
|
||||
let symlink = sampleDir.appendingPathComponent("symlink")
|
||||
let someFilename = "some"
|
||||
|
||||
let processor = DependencyProcessorImpl(
|
||||
xcode: "/Xcode",
|
||||
product: "/Product",
|
||||
source: "/Source",
|
||||
intermediate: intermediateDirReal,
|
||||
derivedFiles: "/DerivedFiles",
|
||||
bundle: "/Bundle",
|
||||
skippedRegexes: []
|
||||
)
|
||||
|
||||
let intermediateFileSymlink = createSymlink(
|
||||
filename: someFilename,
|
||||
sourceDir: symlink,
|
||||
destinationDir: intermediateDirReal
|
||||
)
|
||||
|
||||
let dependencies = processor.process([
|
||||
intermediateFileSymlink,
|
||||
])
|
||||
|
||||
XCTAssertEqual(dependencies, [])
|
||||
}
|
||||
|
||||
func testDoesNotFilterOutSourceBySymlink() throws {
|
||||
let sampleDir = try prepareTempDir()
|
||||
|
||||
let sourceDirReal = sampleDir.appendingPathComponent("Source")
|
||||
let symlink = sampleDir.appendingPathComponent("symlink")
|
||||
let someFilename = "some"
|
||||
|
||||
let processor = DependencyProcessorImpl(
|
||||
xcode: "/Xcode",
|
||||
product: "/Product",
|
||||
source: sourceDirReal,
|
||||
intermediate: "/Intermediate",
|
||||
derivedFiles: "/DerivedFiles",
|
||||
bundle: "/Bundle",
|
||||
skippedRegexes: []
|
||||
)
|
||||
|
||||
let sourceFileSymlink = createSymlink(
|
||||
filename: someFilename,
|
||||
sourceDir: symlink,
|
||||
destinationDir: sourceDirReal
|
||||
)
|
||||
|
||||
let dependencies = processor.process([
|
||||
sourceFileSymlink,
|
||||
])
|
||||
|
||||
XCTAssertEqual(dependencies, [.init(url: sourceFileSymlink, type: .source)])
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates Symlink from sourceDir to destinationDir and creates empty file inside it
|
||||
* return URL with symbolic link from sourceDir to destinationDir
|
||||
*/
|
||||
fileprivate func createSymlink(filename: String, sourceDir: URL, destinationDir: URL) -> URL {
|
||||
let fileMng = FileManager.default
|
||||
|
||||
XCTAssertNoThrow(try fileMng.spt_forceSymbolicLink(
|
||||
at: sourceDir,
|
||||
withDestinationURL: destinationDir
|
||||
))
|
||||
XCTAssertNoThrow(try fileMng.spt_createEmptyFile(destinationDir.appendingPathComponent(filename)))
|
||||
|
||||
return sourceDir.appendingPathComponent(filename)
|
||||
}
|
||||
|
||||
func testSkipsCustomizedDerivedDirFileUnderSources() {
|
||||
let derivedFile: URL = "/DerivedFiles/Module-Swift.h"
|
||||
let processor = DependencyProcessorImpl(
|
||||
xcode: "/Xcode",
|
||||
product: "/",
|
||||
source: "/",
|
||||
intermediate: "/Intermediate",
|
||||
derivedFiles: "/DerivedFiles",
|
||||
bundle: nil,
|
||||
skippedRegexes: []
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
processor.process([derivedFile]),
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
||||
func testSkippsFilesWithFullMatch() {
|
||||
let source: URL = "/someFile.m"
|
||||
let processor = DependencyProcessorImpl(
|
||||
xcode: "/Xcode",
|
||||
product: "/",
|
||||
source: "/",
|
||||
intermediate: "/Intermediate",
|
||||
derivedFiles: "/DerivedFiles",
|
||||
bundle: nil,
|
||||
skippedRegexes: ["/someFile\\.m"]
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
processor.process([source]),
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
||||
func testSkippsFilesWithPartialMatch() {
|
||||
let derivedModulemap: URL = "/module.modulemap"
|
||||
let processor = DependencyProcessorImpl(
|
||||
xcode: "/Xcode",
|
||||
product: "/product",
|
||||
source: "/",
|
||||
intermediate: "/Intermediate",
|
||||
derivedFiles: "/DerivedFiles",
|
||||
bundle: nil,
|
||||
skippedRegexes: ["\\.modulemap$"]
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
processor.process([derivedModulemap]),
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
||||
func testDoesntSkipFileIfInvalidRegex() {
|
||||
let source: URL = "/someFile.m"
|
||||
let processor = DependencyProcessorImpl(
|
||||
xcode: "/Xcode",
|
||||
product: "/product",
|
||||
source: "/",
|
||||
intermediate: "/Intermediate",
|
||||
derivedFiles: "/DerivedFiles",
|
||||
bundle: nil,
|
||||
skippedRegexes: ["\\"]
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
processor.process([source]),
|
||||
[.init(url: source, type: .source)]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,70 @@
|
||||
// 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 OverlayDependenciesRemapperTests: XCTestCase {
|
||||
private let overlayReader = OverlayReaderFake(
|
||||
mappings: [.init(virtual: "/file.h", local: "/Intermediate/Some/file.h")]
|
||||
)
|
||||
|
||||
func testMappingFromLocalToGeneric() throws {
|
||||
let remapper = OverlayDependenciesRemapper(overlayReader: overlayReader)
|
||||
|
||||
let dependencies = try remapper.replace(localPaths: ["/Intermediate/Some/file.h"])
|
||||
XCTAssertEqual(dependencies, ["/file.h"])
|
||||
}
|
||||
|
||||
func testMappingFromGenericToLocal() throws {
|
||||
let remapper = OverlayDependenciesRemapper(overlayReader: overlayReader)
|
||||
|
||||
|
||||
let dependencies = try remapper.replace(genericPaths: ["/file.h"])
|
||||
XCTAssertEqual(dependencies, ["/Intermediate/Some/file.h"])
|
||||
}
|
||||
|
||||
func testGenericDependenciesAreNotMerged() throws {
|
||||
let remapper = OverlayDependenciesRemapper(overlayReader: overlayReader)
|
||||
|
||||
|
||||
let dependencies = try remapper.replace(localPaths: ["/Intermediate/Some/file.h", "/file.h"])
|
||||
XCTAssertEqual(dependencies, ["/file.h", "/file.h"])
|
||||
}
|
||||
|
||||
func testLocalDependenciesAreNotMerged() throws {
|
||||
let remapper = OverlayDependenciesRemapper(overlayReader: overlayReader)
|
||||
|
||||
|
||||
let dependencies = try remapper.replace(genericPaths: ["/Intermediate/Some/file.h", "/file.h"])
|
||||
XCTAssertEqual(dependencies, ["/Intermediate/Some/file.h", "/Intermediate/Some/file.h"])
|
||||
}
|
||||
|
||||
func testMappingsAreReadOnce() throws {
|
||||
let remapper = OverlayDependenciesRemapper(overlayReader: overlayReader)
|
||||
|
||||
let firstDependencies = try remapper.replace(localPaths: ["/Intermediate/Some/file.h"])
|
||||
// Update mappings in-fly to verify the previous value is cached in a remapper
|
||||
overlayReader.mappings = []
|
||||
let secondDependencies = try remapper.replace(localPaths: ["/Intermediate/Some/file.h"])
|
||||
|
||||
XCTAssertEqual(firstDependencies, ["/file.h"])
|
||||
XCTAssertEqual(secondDependencies, ["/file.h"])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// 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 JsonOverlayReaderTests: FileXCTestCase {
|
||||
private static let resourcesSubdirectory = "TestData/Dependencies/JsonOverlayReaderTests"
|
||||
|
||||
func testParsingWithSuccess() throws {
|
||||
let file = try pathForTestData(name: "overlayReaderDefault")
|
||||
let reader = JsonOverlayReader(file, mode: .strict, fileReader: FileManager.default)
|
||||
let mappings = try reader.provideMappings()
|
||||
|
||||
let expectedMappings = [
|
||||
OverlayMapping(
|
||||
virtual: "/DerivedDataProducts/Target1.framework/Headers/Target1.h",
|
||||
local: "/Path/Target1/Target1.h"
|
||||
),
|
||||
OverlayMapping(
|
||||
virtual: "/DerivedDataProducts/Target2.framework/Modules/module.modulemap",
|
||||
local: "/DerivedDataIntermediate/Target2.build/module.modulemap"
|
||||
),
|
||||
]
|
||||
XCTAssertEqual(Set(mappings), Set(expectedMappings))
|
||||
}
|
||||
|
||||
func testFailsWithMissingFileForStrictMode() throws {
|
||||
let file: URL = "nonExiting"
|
||||
let reader = JsonOverlayReader(file, mode: .strict, fileReader: FileManager.default)
|
||||
|
||||
XCTAssertThrowsError(try reader.provideMappings())
|
||||
}
|
||||
|
||||
func testReturnsEmpptyMappingForMissingFileForBestEffortMode() throws {
|
||||
let file: URL = "nonExiting"
|
||||
let reader = JsonOverlayReader(file, mode: .bestEffort, fileReader: FileManager.default)
|
||||
|
||||
let mappings = try reader.provideMappings()
|
||||
|
||||
XCTAssertEqual(mappings, [])
|
||||
}
|
||||
|
||||
func testParsingEmptyOverlay() throws {
|
||||
let file = try pathForTestData(name: "overlayReaderEmpty")
|
||||
let reader = JsonOverlayReader(file, mode: .strict, fileReader: FileManager.default)
|
||||
let mappings = try reader.provideMappings()
|
||||
|
||||
XCTAssertEqual(mappings, [])
|
||||
}
|
||||
|
||||
func testInvalidJsonDoesntThrowForBestEffortMode() throws {
|
||||
let workingDir = try prepareTempDir()
|
||||
let file = workingDir.appendingPathExtension("overlay.json")
|
||||
try fileManager.spt_createEmptyFile(file)
|
||||
let reader = JsonOverlayReader(file, mode: .bestEffort, fileReader: fileManager)
|
||||
let mappings = try reader.provideMappings()
|
||||
|
||||
XCTAssertEqual(mappings, [])
|
||||
}
|
||||
|
||||
func testInvalidJsonThrowsForStrictMode() throws {
|
||||
let workingDir = try prepareTempDir()
|
||||
let file = workingDir.appendingPathExtension("overlay.json")
|
||||
try fileManager.spt_createEmptyFile(file)
|
||||
let reader = JsonOverlayReader(file, mode: .strict, fileReader: fileManager)
|
||||
|
||||
XCTAssertThrowsError(try reader.provideMappings())
|
||||
}
|
||||
|
||||
private func pathForTestData(name: String) throws -> URL {
|
||||
return try XCTUnwrap(Bundle.module.url(
|
||||
forResource: name,
|
||||
withExtension: "json",
|
||||
subdirectory: JsonOverlayReaderTests.resourcesSubdirectory
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// 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 PathDependenciesRemapperFactoryTests: XCTestCase {
|
||||
private var factory: PathDependenciesRemapperFactory!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
factory = PathDependenciesRemapperFactory()
|
||||
}
|
||||
|
||||
func testMappingsFromEnvMaps() throws {
|
||||
let remapper = try factory.build(
|
||||
orderKeys: ["SRC_ROOT"],
|
||||
envs: ["SRC_ROOT": "/tmp/root"],
|
||||
customMappings: [:]
|
||||
)
|
||||
|
||||
let localPaths = try remapper.replace(genericPaths: ["$(SRC_ROOT)/some.swift"])
|
||||
XCTAssertEqual(localPaths, ["/tmp/root/some.swift"])
|
||||
}
|
||||
|
||||
func testMappingsGenericWhenMappingHasParentDir() throws {
|
||||
let remapper = try factory.build(
|
||||
orderKeys: ["SRC_ROOT"],
|
||||
envs: ["SRC_ROOT": "/tmp/root/extra/.."],
|
||||
customMappings: [:]
|
||||
)
|
||||
|
||||
let localPaths = try remapper.replace(genericPaths: ["$(SRC_ROOT)/some.swift"])
|
||||
XCTAssertEqual(localPaths, ["/tmp/root/some.swift"])
|
||||
}
|
||||
|
||||
func testMappingsLocalWhenMappingHasParentDir() throws {
|
||||
let remapper = try factory.build(
|
||||
orderKeys: ["SRC_ROOT"],
|
||||
envs: ["SRC_ROOT": "/tmp/root/excessive/.."],
|
||||
customMappings: [:]
|
||||
)
|
||||
|
||||
let localPaths = try remapper.replace(localPaths: ["/tmp/root/some.swift"])
|
||||
XCTAssertEqual(localPaths, ["$(SRC_ROOT)/some.swift"])
|
||||
}
|
||||
|
||||
func testMissingEnvIsSkipped() throws {
|
||||
XCTAssertNoThrow(
|
||||
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 = try 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"]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -30,34 +30,34 @@ class StringDependenciesRemapperTests: XCTestCase {
|
||||
remapper = StringDependenciesRemapper(mappings: mappings)
|
||||
}
|
||||
|
||||
func testMappingSingleGenericPathReplacesWithLocalPath() {
|
||||
let localPaths = remapper.replace(genericPaths: ["$(SRC_ROOT)/some.swift"])
|
||||
func testMappingSingleGenericPathReplacesWithLocalPath() throws {
|
||||
let localPaths = try remapper.replace(genericPaths: ["$(SRC_ROOT)/some.swift"])
|
||||
|
||||
XCTAssertEqual(localPaths, ["/tmp/root/some.swift"])
|
||||
}
|
||||
|
||||
func testRewritingSingleLocalPathReplacesWithGenericPath() {
|
||||
let genericPaths = remapper.replace(localPaths: ["/tmp/root/some.swift"])
|
||||
func testRewritingSingleLocalPathReplacesWithGenericPath() throws {
|
||||
let genericPaths = try remapper.replace(localPaths: ["/tmp/root/some.swift"])
|
||||
|
||||
XCTAssertEqual(genericPaths, ["$(SRC_ROOT)/some.swift"])
|
||||
}
|
||||
|
||||
func testRewritingLocalToGenericAndLocalIsIdentical() {
|
||||
func testRewritingLocalToGenericAndLocalIsIdentical() throws {
|
||||
let inputLocalPaths = ["/tmp/root/some.swift"]
|
||||
|
||||
let genericPaths = remapper.replace(localPaths: inputLocalPaths)
|
||||
let localPaths = remapper.replace(genericPaths: genericPaths)
|
||||
let genericPaths = try remapper.replace(localPaths: inputLocalPaths)
|
||||
let localPaths = try remapper.replace(genericPaths: genericPaths)
|
||||
|
||||
XCTAssertEqual(localPaths, inputLocalPaths)
|
||||
}
|
||||
|
||||
func testRewritingUnrelatedDirReturnsInputPath() {
|
||||
let genericPaths = remapper.replace(localPaths: ["/other/some.swift"])
|
||||
func testRewritingUnrelatedDirReturnsInputPath() throws {
|
||||
let genericPaths = try remapper.replace(localPaths: ["/other/some.swift"])
|
||||
|
||||
XCTAssertEqual(genericPaths, ["/other/some.swift"])
|
||||
}
|
||||
|
||||
func testMultipleMatchesTakeTheFirstMapping() {
|
||||
func testMultipleMatchesTakeTheFirstMapping() throws {
|
||||
let mappings: [StringDependenciesRemapper.Mapping] = [
|
||||
.init(generic: "$(SRC_ROOT)", local: "/tmp/root"),
|
||||
.init(generic: "$(PWD)", local: "/tmp"),
|
||||
@@ -65,22 +65,34 @@ class StringDependenciesRemapperTests: XCTestCase {
|
||||
remapper = StringDependenciesRemapper(mappings: mappings)
|
||||
|
||||
|
||||
let genericPaths = remapper.replace(localPaths: ["/tmp/root/some.swift", "/tmp/extra.swift"])
|
||||
let genericPaths = try remapper.replace(localPaths: ["/tmp/root/some.swift", "/tmp/extra.swift"])
|
||||
|
||||
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() throws {
|
||||
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 = try remapper.replace(localPaths: ["/tmp/root/some.swift"])
|
||||
|
||||
XCTAssertEqual(genericPaths, ["$(ROOT)/some.swift"])
|
||||
}
|
||||
|
||||
func testMappingsGenericPathsIsDoneInReversedOrder() throws {
|
||||
let mappings: [StringDependenciesRemapper.Mapping] = [
|
||||
.init(generic: "$(TMP)", local: "/tmp"),
|
||||
.init(generic: "$(ROOT)", local: "$(TMP)/root"),
|
||||
]
|
||||
remapper = StringDependenciesRemapper(mappings: mappings)
|
||||
|
||||
|
||||
let localPaths = try 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,18 +33,23 @@ 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"
|
||||
|
||||
private var config: XCRemoteCacheConfig!
|
||||
private var generator: FingerprintAccumulator!
|
||||
private var generator: FingerprintAccumulator! {
|
||||
return generatorFake
|
||||
}
|
||||
|
||||
private var generatorFake: FingerprintAccumulatorFake!
|
||||
private var fingerprintGenerator: EnvironmentFingerprintGenerator!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
config = XCRemoteCacheConfig(sourceRoot: "")
|
||||
generator = FingerprintAccumulatorFake()
|
||||
generatorFake = FingerprintAccumulatorFake()
|
||||
fingerprintGenerator = EnvironmentFingerprintGenerator(
|
||||
configuration: config,
|
||||
env: Self.defaultENV,
|
||||
@@ -55,7 +60,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 +78,7 @@ class EnvironmentFingerprintGeneratorTests: XCTestCase {
|
||||
|
||||
let fingerprint = try fingerprintGenerator.generateFingerprint()
|
||||
|
||||
XCTAssertEqual(fingerprint, ",,,,,,,,,,\(Self.currentVersion)")
|
||||
XCTAssertEqual(fingerprint, ",,,,,,,,,,,\(Self.currentVersion)")
|
||||
}
|
||||
|
||||
func testConsidersCustomEnvs() throws {
|
||||
@@ -89,6 +94,13 @@ 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)")
|
||||
}
|
||||
|
||||
func testFingerprintIsGeneratedOnce() throws {
|
||||
let fingerprint1 = try fingerprintGenerator.generateFingerprint()
|
||||
let fingerprint2 = try fingerprintGenerator.generateFingerprint()
|
||||
XCTAssertEqual(fingerprint1, fingerprint2)
|
||||
XCTAssertEqual(generatorFake.generateCallsCount, 1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user