Compare commits

..

104 Commits

Author SHA1 Message Date
Bartosz Polaczyk c052ed8ed6 Merge pull request #105 from cezarsignori/csignori/xcrc_fake_src_root
Set provided fakeSrcRoot on Xcode proj build settings as XCRC_FAKE_SRCROOT
2022-03-22 17:00:59 +01:00
Cezar Signori d4f9486b92 Set provided fakeSrcRoot on Xcode proj build settings as XCRC_FAKE_SRCROOT 2022-03-22 16:26:16 +01:00
Bartosz Polaczyk 8241914543 Merge pull request #106 from alecgarza96/master
Fixes issue #49
2022-03-21 20:08:19 +01:00
alecgarza96 b5ff16484f removed trailing whitespace 2022-03-21 11:31:09 -05:00
alecgarza96 2b9dde9aec added support for both ".S" and ".s" extensions 2022-03-21 11:16:40 -05:00
alecgarza96 136e7a99ff added .s file extension for assembly support 2022-03-10 14:40:58 -06:00
Bartosz Polaczyk c626d51f97 Merge pull request #99 from polac24/20220304-document-debugging
[Docs] Local development steps
2022-03-07 17:29:49 +01:00
Bartosz Polaczyk e8ddc9297d [Docs] Local development steps 2022-03-04 18:47:29 +01:00
Bartosz Polaczyk 3b614c6172 Merge pull request #97 from polac24/20220228-fix-swiftlint
Fix swiftlint errors
2022-03-02 08:47:44 +01:00
Bartosz Polaczyk 87a214104e Add headers linting 2022-03-01 22:58:00 +01:00
Bartosz Polaczyk 599e1e229d Fix linter errors 2022-03-01 22:44:21 +01:00
Bartosz Polaczyk 423da7cc4a Enable trailing_dot_in_comments 2022-03-01 22:44:03 +01:00
Bartosz Polaczyk 4b082e9dd2 Run CI linting in strict mode 2022-03-01 22:22:45 +01:00
Vadim Smal 36803d6b5d Merge pull request #96 from CognitiveDisson/update-dependencies
Update dependencies
2022-03-01 08:54:36 +00:00
Bartosz Polaczyk 5809bc963c Fix serious errors 2022-02-28 23:14:52 +01:00
Bartosz Polaczyk cb6626cfbc Add exclude to E2E Pods 2022-02-28 22:57:45 +01:00
Bartosz Polaczyk 3e18711e09 Apply autocorrect 2022-02-28 22:56:34 +01:00
Vadim Smal 90d784cc8d Update dependencies 2022-02-28 16:05:59 +00:00
Bartosz Polaczyk f839b4064b Merge pull request #93 from polac24/20220223-add-troubleshootings
[Docs] Add troubleshooting tips
2022-02-24 20:48:29 +01:00
Bartosz Polaczyk e71837b8b2 Merge pull request #94 from polac24/20220223-missing-overlay-log
Print overlay error messages to os_log only
2022-02-24 20:48:12 +01:00
Bartosz Polaczyk b45792646b Apply suggestions from code review
Co-authored-by: Aleksander Grzyb <aleksander.grzyb@gmail.com>
2022-02-24 08:46:57 +01:00
Bartosz Polaczyk 1966562eef Print overlay error messages to logs only 2022-02-23 21:58:03 +01:00
Bartosz Polaczyk 883b207c5b Fix formatting 2022-02-23 21:43:40 +01:00
Bartosz Polaczyk ca137d0ce4 Add troubleshooting steps 2022-02-23 21:24:07 +01:00
Bartosz Polaczyk b9a633c86f Merge pull request #91 from polac24/20220222-disable-urlcache
Disable local URLSession cache
2022-02-22 17:33:59 +01:00
Bartosz Polaczyk 55a87eb4e9 Disable local URLSession cache 2022-02-22 14:37:06 +01:00
Bartosz Polaczyk e6b56024b9 Merge pull request #90 from polac24/bump-cocoapods-007
Bump CocoaPods plugin version
2022-02-21 10:25:45 +01:00
Bartosz Polaczyk 8c34a31110 Bump CocoaPods plugin version 2022-02-19 08:54:22 +01:00
Bartosz Polaczyk f7c32d6e80 Merge pull request #87 from polac24/20220217-custom-mappings-envs
Customize rewritting dependency paths
2022-02-18 11:30:51 +01:00
Bartosz Polaczyk b3a16ae5d0 Merge pull request #88 from polac24/20220217-decorate-logs
Decorate logs with a target name
2022-02-18 11:30:35 +01:00
Bartosz Polaczyk d013fe4c81 Decorate logs with a target name 2022-02-17 20:35:13 +01:00
Bartosz Polaczyk 4aefee078e Customize rewritting paths 2022-02-17 20:13:25 +01:00
Bartosz Polaczyk 9363e68d51 Merge pull request #85 from polac24/20220214-remapper-stable
Do not change dependencies order when remapping overlay
2022-02-15 09:06:50 +01:00
Bartosz Polaczyk 4af8156da5 Do not change dependencies order when remapping overlay 2022-02-14 18:46:51 +01:00
Bartosz Polaczyk f086feb005 Merge pull request #83 from polac24/issue-template
Add issue templates
2022-02-14 09:01:51 +01:00
Bartosz Polaczyk d98d4cd0a3 Apply suggestions from code review 2022-02-14 08:46:25 +01:00
Bartosz Polaczyk a38d445ae9 Merge pull request #82 from polac24/20220210-best-effort-overlay
Do not fail prebuild/postbuild for invalid vfs overlay
2022-02-14 08:45:22 +01:00
Bartosz Polaczyk 0436f6ae27 Separate templates 2022-02-13 21:55:56 +01:00
Bartosz Polaczyk be78959437 Add Issue template 2022-02-11 18:10:00 +01:00
Bartosz Polaczyk cccae0d9f6 Add disable_vfs_overlay feature flag 2022-02-10 21:03:53 +01:00
Bartosz Polaczyk e7d1e905cf Do not throw overlay for the best-effort mode 2022-02-10 20:57:05 +01:00
Bartosz Polaczyk 20d53a1c71 Merge pull request #75 from polac24/20220209-prebuild-index
[Integrate] Place xcprebuild script right before compilation
2022-02-10 20:17:16 +01:00
Bartosz Polaczyk f4ba03d581 Merge pull request #72 from polac24/20220207-non-throwing-init
Make DependenciesRemapper throwable
2022-02-09 22:36:36 +01:00
Bartosz Polaczyk 0e48d39818 [Integrate] Move prebuild script right before compilation 2022-02-09 17:52:31 +01:00
Bartosz Polaczyk 3b30939f99 Merge pull request #74 from vasvf/master
Rename prebuild and postbuild phases, adjust prebuild position
2022-02-09 17:40:41 +01:00
Vasily Fedorov e263cb6e25 fix comment 2022-02-09 17:01:35 +03:00
Vasily Fedorov b805bf4a99 revert some changes 2022-02-09 16:55:13 +03:00
Vasily Fyodorov fd7b68a344 Merge branch 'spotify:master' into master 2022-02-08 19:18:39 +03:00
Vasily Fedorov a63488a043 Move build configs to xcconfigs 2022-02-08 19:02:38 +03:00
Bartosz Polaczyk 48bcdd8ce9 Apply suggestions from code review
Co-authored-by: PatrikBillgren <PatrikBillgren@users.noreply.github.com>
2022-02-08 16:49:57 +01:00
Bartosz Polaczyk 41dd1cae03 Change to throwing remapping 2022-02-07 20:40:44 +01:00
Bartosz Polaczyk 1e86cac3ec Merge pull request #71 from polac24/20220204-overlay-mapper
Enable virtual file system overlay replacements
2022-02-07 20:39:53 +01:00
Bartosz Polaczyk 22faa5dbdb Fix comments 2022-02-07 19:39:22 +01:00
Bartosz Polaczyk 522900748d Change DerivedData's path for consumer 2022-02-07 19:35:55 +01:00
Bartosz Polaczyk 4998cc4f87 Merge remote-tracking branch 'upstream/master' into 20220204-overlay-mapper 2022-02-07 19:31:10 +01:00
Bartosz Polaczyk 9221f9d2b5 Merge pull request #70 from polac24/20220204-overlay-reader
Parse vfs overlay file
2022-02-07 19:28:04 +01:00
Bartosz Polaczyk 1127257ad5 Merge pull request #67 from vasvf/resolve_symlinks
Resolve symlinks and dirpaths in dependency processor
2022-02-07 19:26:56 +01:00
Bartosz Polaczyk 599e5fe561 Merge pull request #64 from polac24/20220127-automate-e2e
Add E2E automation
2022-02-07 17:52:39 +01:00
Bartosz Polaczyk 478649a1d7 Add docs 2022-02-05 21:58:00 +01:00
Bartosz Polaczyk 1c5aa569dd Cleanup xcodeproj 2022-02-05 21:37:36 +01:00
Bartosz Polaczyk c6b31d3086 Disable exclusive DD paths 2022-02-05 21:37:06 +01:00
Bartosz Polaczyk f0a4d361b1 Use separate DerivedData path for consumser 2022-02-05 21:25:34 +01:00
Bartosz Polaczyk 0778639ae2 Extract to a separate file 2022-02-05 21:06:54 +01:00
Bartosz Polaczyk c44b793a19 Fix comments 2022-02-04 22:07:30 +01:00
Bartosz Polaczyk 2b383f046e Add scenario for empty overlay 2022-02-04 21:55:04 +01:00
Bartosz Polaczyk cf0b27d03c Add integration
This reverts commit b2d47760cf.
2022-02-04 21:48:43 +01:00
Bartosz Polaczyk b2d47760cf Revert integration 2022-02-04 19:20:21 +01:00
Bartosz Polaczyk 714c9ef35b Add overlay and related integration 2022-02-04 19:16:08 +01:00
Bartosz Polaczyk b872a8d7f9 Post test cleanup 2022-02-04 19:01:05 +01:00
Bartosz Polaczyk 56b6722dbe Reuse DerivedData for producer and consumer 2022-02-03 21:44:55 +01:00
Bartosz Polaczyk 92875bcdee Change DerivedData path 2022-02-03 20:35:07 +01:00
Bartosz Polaczyk 46a99bbe32 Cleanup 2022-02-03 20:32:45 +01:00
Bartosz Polaczyk 408698f1e8 Switch to nginx 2022-02-03 20:10:51 +01:00
Bartosz Polaczyk e54ce770e7 Start docker 2022-02-03 19:39:06 +01:00
Bartosz Polaczyk 49be11184e Fix dash in docker installer 2022-02-03 19:29:41 +01:00
Bartosz Polaczyk d8850b555a Install docker 2022-02-03 19:11:05 +01:00
Bartosz Polaczyk 93a60b2b40 Fix branch typo 2022-02-03 18:52:40 +01:00
Bartosz Polaczyk cde81d852f add a branch 2022-02-03 18:24:05 +01:00
Bartosz Polaczyk 97328144fd Log remotes 2022-02-03 17:40:42 +01:00
Bartosz Polaczyk 3f8333c07b print log times 2022-02-03 17:02:52 +01:00
Bartosz Polaczyk 2160645f2c do not build again for e2e 2022-02-02 23:45:44 +01:00
Bartosz Polaczyk d9c2213f50 Log xcodebuild to stdout 2022-02-02 23:42:47 +01:00
Bartosz Polaczyk 327d282e23 Merge pull request #66 from vasvf/master
Deintegrate XCRemoteCache from all projects on errors
2022-02-02 20:43:04 +01:00
Vasily Fedorov 06781763aa Resolve symlinks in dependency processor 2022-02-01 20:40:41 +03:00
Vasily Fedorov 60c7a586c7 Also remove unneeded space in CFlags 2022-01-31 15:36:35 +03:00
Vasily Fedorov 2ac3da9035 Deintegrate XCRemoteCache from all projects on errors 2022-01-30 15:43:44 +03:00
Bartosz Polaczyk 4dbc5c9b19 Merge pull request #63 from vasvf/master
support producer-fast mode for plugin
2022-01-30 10:06:19 +01:00
Vasily Fedorov 6d10ac8c7d Fix after code review 2022-01-29 13:22:32 +03:00
Vasily Fedorov 8b8c2d627c Remove CC in plugin also 2022-01-28 12:08:55 +03:00
Vasily Fyodorov b1026b16da Merge branch 'spotify:master' into master 2022-01-28 12:06:23 +03:00
Bartosz Polaczyk e1cc629c55 Merge remote-tracking branch 'upstream/master' into 20220127-automate-e2e 2022-01-28 08:05:10 +01:00
Bartosz Polaczyk f75c77efa2 Merge pull request #61 from polac24/20220126-cocoapods-skip-aggregation
Limit integrating CocoaPods to native targets
2022-01-28 08:04:19 +01:00
Bartosz Polaczyk 3f8ec5b453 WIP 2022-01-27 22:43:49 +01:00
Vasily Fedorov 94b57475e8 support producer-fast mode for plugin, and also remove "cc" config when in producer mode. 2022-01-27 20:23:26 +03:00
Bartosz Polaczyk a30d56ac16 Cleanup native targets limitation 2022-01-27 17:08:30 +01:00
Bartosz Polaczyk 81077edfa4 Align with external Pods 2022-01-26 22:33:57 +01:00
Bartosz Polaczyk da3a5d59fa Merge remote-tracking branch 'upstream/master' into 20220126-cocoapods-skip-aggregation 2022-01-26 22:32:56 +01:00
Bartosz Polaczyk 94490532f7 Merge pull request #57 from vasvf/master
Add support for generate_multiple_pod_projects
2022-01-26 22:32:23 +01:00
Bartosz Polaczyk 326cac7668 Limit integrating CocoaPods to native targets 2022-01-26 21:40:25 +01:00
Vasily Fedorov 44a09befa0 Add support for generate_multiple_pod_projects 2022-01-23 19:43:05 +03:00
Bartosz Polaczyk cdddc5bf19 Merge pull request #56 from vasvf/master
Add support for swift modules with @objc interfaces
2022-01-23 17:31:49 +01:00
Vasily Fedorov 93b8dcd0c3 Also added swiftinterface file check to swiftc tests 2022-01-23 18:21:52 +03:00
Vasily Fedorov da0d1c20d2 Separated testing for including swiftinterface file in artifacts builders 2022-01-23 18:13:42 +03:00
Vasily Fedorov 8155e042b5 Add support for swift modules with @objc interfaces 2022-01-23 03:59:49 +03:00
88 changed files with 2112 additions and 167 deletions
+65
View File
@@ -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). -->
+20
View File
@@ -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
+4
View File
@@ -9,6 +9,8 @@ jobs:
- uses: actions/checkout@v1
- name: SwiftLint
uses: norio-nomura/action-swiftlint@3.1.0
with:
args: --strict
macOS:
runs-on: macOS-latest
@@ -23,3 +25,5 @@ jobs:
run: rake build[release]
- name: Test
run: rake test
- name: E2ETests
run: rake e2e_only
+5 -1
View File
@@ -1,9 +1,13 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
*.xcodeproj/
*.xcworkspace/
DerivedData
/.swiftpm/
releases
tmp/
.idea/
xcuserdata
*.gem
Pods/
+21 -2
View File
@@ -5,7 +5,6 @@ disabled_rules:
- superfluous_disable_command # Disabled since we disable some rules pre-emptively to avoid issues in the future
- todo # Temporarily disabled. We have too many right now hiding real issues :(
- nesting # Does not make sense anymore since Swift 4 uses nested `CodingKeys` enums for example
- trailing_dot_in_comments # Triggers warnings for generated file headers
opt_in_rules:
- anyobject_protocol
@@ -64,6 +63,7 @@ excluded:
- docs/
- fastlane/
- DerivedData/
- e2eTests/XCRemoteCacheSample/Pods
attributes:
always_on_same_line:
@@ -88,6 +88,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
@@ -124,6 +143,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
+10 -10
View File
@@ -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",
@@ -42,8 +42,8 @@
"repositoryURL": "https://github.com/tuist/XcodeProj.git",
"state": {
"branch": null,
"revision": "0b18c3e7a10c241323397a80cb445051f4494971",
"version": "8.0.0"
"revision": "c75c3acc25460195cfd203a04dde165395bf00e0",
"version": "8.7.1"
}
},
{
@@ -51,8 +51,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 +60,8 @@
"repositoryURL": "https://github.com/marmelroy/Zip.git",
"state": {
"branch": null,
"revision": "80b1c3005ee25b4c7ce46c4029ac3347e8d5e37e",
"version": "2.0.0"
"revision": "67fa55813b9e7b3b9acee9c0ae501def28746d76",
"version": "2.1.2"
}
}
]
+7 -5
View File
@@ -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,10 @@ 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"),
],
targets: [
.target(
@@ -56,7 +57,8 @@ let package = Package(
),
.testTarget(
name: "XCRemoteCacheTests",
dependencies: ["XCRemoteCache"]
dependencies: ["XCRemoteCache"],
resources: [.copy("TestData")]
),
]
)
+4
View File
@@ -312,6 +312,8 @@ _Note that for the `producer` mode, the prebuild build phase and `xccc`, `xcld`,
| `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. | `[]` | ⬜️ |
## Backend cache server
@@ -402,12 +404,14 @@ Note: This setup is not recommended and may not be supported in future XCRemoteC
* 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) 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.
* Using "Precompiled prefix headers" for Objective-C targets is not yet supported. [PR is welcome]
## FAQ
+7
View File
@@ -1,4 +1,5 @@
# encoding: utf-8
require_relative 'tasks/e2e'
################################
# Rake configuration
@@ -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
################################
@@ -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,
]
}
@@ -38,7 +38,7 @@ class ThinningCreatorPlugin: ArtifactCreatorPlugin {
/// 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 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, modeMarkerPath: String, dirScanner: DirScanner) {
self.targetTempDir = targetTempDir
@@ -94,7 +94,7 @@ class ThinningCreatorPlugin: ArtifactCreatorPlugin {
// 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.
// `$(TARGET_TEMP_DIR)/xccache/{{fileKey}}.zip` location
let targetEnabledMarker = tempDir.appendingPathComponent(modeMarkerPath)
let targetReusedArtifactRootDir = tempDir.appendingPathComponent("xccache")
@@ -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 \
@@ -153,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,
@@ -76,12 +76,16 @@ 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
}
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")
@@ -117,7 +121,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
@@ -127,5 +131,7 @@ extension PostbuildContext {
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")
}
}
@@ -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
@@ -35,6 +36,7 @@ public class XCPostbuild {
do {
config = try XCRemoteCacheConfigReader(env: env, fileManager: 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,8 +68,8 @@ 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 StringDependenciesRemapperFactory().build(
orderKeys: DependenciesMapping.rewrittenEnvs,
let envsRemapper = try PathDependenciesRemapperFactory().build(
orderKeys: DependenciesMapping.rewrittenEnvs + config.customRewriteEnvs,
envs: env,
customMappings: config.outOfBandMappings
)
@@ -145,6 +147,24 @@ 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,
@@ -301,3 +321,4 @@ public class XCPostbuild {
}
}
}
// swiftlint:enable type_body_length
@@ -63,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")
}
}
@@ -31,6 +31,7 @@ public class XCPrebuild {
do {
config = try XCRemoteCacheConfigReader(env: env, fileManager: fileManager).readConfiguration()
context = try PrebuildContext(config, env: env)
updateProcessTag(context.targetName)
} catch {
// Fatal error:
exit(1, "FATAL: Prebuild initialization failed with error: \(error)")
@@ -115,11 +116,27 @@ public class XCPrebuild {
)
let client: NetworkClient = config.disableHttpCache ? networkClient : cacheNetworkClient
let remoteNetworkClient = RemoteNetworkClientImpl(client, urlBuilder)
let pathRemapper = try StringDependenciesRemapperFactory().build(
orderKeys: DependenciesMapping.rewrittenEnvs,
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
@@ -72,7 +72,17 @@ class TemplateBasedCCWrapperBuilder: CCWrapperBuilder {
)
infoLog("ClangWrapperBuilder compiles file at \(compilationFile).")
// -O3: optimize for faster execution
let args = [clangCommand, "-arch", "arm64", "-arch", "x86_64", "-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)")
}
@@ -426,7 +436,9 @@ 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
@@ -33,10 +33,12 @@ 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 {
@@ -61,8 +63,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_PLATFORM_PREFERRED_ARCH"] = "$(LINK_FILE_LIST_$(CURRENT_VARIANT)_$(PLATFORM_PREFERRED_ARCH):dir:standardizepath:file:default=arm64)"
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
}
@@ -97,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 {
@@ -227,11 +227,11 @@ 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)
target.buildPhases.insert(prebuildPhase, at: sourceIndex)
pbxproj.add(object: postbuildPhase)
target.buildPhases.append(postbuildPhase)
}
@@ -18,7 +18,6 @@
// under the License.
import Foundation
import Yams
/// Print current configuration to the console
public class XCConfig {
@@ -18,7 +18,6 @@
// under the License.
import Foundation
import Yams
/// Switch between Online/Offline modes
public enum XCPrepareMode {
@@ -18,7 +18,6 @@
// under the License.
import Foundation
import Yams
/// Manages XCRemoteCache statistics: rests, print to the standard output etc
public class XCStats {
@@ -110,8 +110,8 @@ 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:
@@ -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
@@ -95,10 +95,10 @@ public struct XCRemoteCacheConfig: Encodable {
/// 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 e
/// xecuted if a target switches to local compilation.
/// 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.
/// 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
@@ -122,15 +122,20 @@ public struct XCRemoteCacheConfig: Encodable {
var AWSRegion: String = ""
/// Service for AWS V4 Signature (e.g. `storage`)
var AWSService: String = ""
/// A dictionary of files path remapping that should be applied to make it absolute path agnostic on a list of dependencies.
/// Useful if a project refers files out of repo root, either compilation files or precompiled dependencies.
/// Keys represent generic replacement and values are substrings that should be replaced.
/// 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.
/// `/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] = []
}
extension XCRemoteCacheConfig {
@@ -183,6 +188,8 @@ extension XCRemoteCacheConfig {
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
return merge
}
@@ -244,6 +251,8 @@ struct ConfigFileScheme: Decodable {
let AWSService: String?
let outOfBandMappings: [String: String]?
let disableCertificateVerification: Bool?
let disableVFSOverlay: Bool?
let customRewriteEnvs: [String]?
// Yams library doesn't support encoding strategy, see https://github.com/jpsim/Yams/issues/84
enum CodingKeys: String, CodingKey {
@@ -288,6 +297,8 @@ struct ConfigFileScheme: Decodable {
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"
}
}
@@ -72,7 +72,7 @@ public class FileDependenciesReader: DependenciesReader {
return Set(splitDependencyFileList(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))
default:
@@ -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.reversed().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,7 +59,7 @@ 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.reversed().reduce(path) { prevPath, mapping in
prevPath.replacingOccurrences(of: mapping.generic, with: mapping.local)
@@ -68,7 +68,7 @@ final class StringDependenciesRemapper: DependenciesRemapper {
}
}
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)
@@ -58,11 +58,11 @@ class DependencyProcessorImpl: DependencyProcessor {
private let bundlePath: 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
xcodePath = xcode.path.dirPath()
productPath = product.path.dirPath()
sourcePath = source.path.dirPath()
intermediatePath = intermediate.path.dirPath()
bundlePath = bundle?.path.dirPath()
}
func process(_ files: [URL]) -> [Dependency] {
@@ -72,7 +72,7 @@ class DependencyProcessorImpl: DependencyProcessor {
private func classify(_ files: [URL]) -> [Dependency] {
return files.map { file -> Dependency in
let filePath = file.path
let filePath = file.resolvingSymlinksInPath().path
if filePath.hasPrefix(xcodePath) {
return Dependency(url: file, type: .xcode)
} else if filePath.hasPrefix(intermediatePath) {
@@ -111,3 +111,9 @@ class DependencyProcessorImpl: DependencyProcessor {
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 []
}
}
}
}
@@ -19,24 +19,28 @@
import Foundation
enum StringDependenciesRemapperFactoryError: Error {
enum PathDependenciesRemapperFactoryError: Error {
/// Remapping keys are duplicated and can lead to undetermined results
case mappingKeyDuplication
}
class StringDependenciesRemapperFactory {
class PathDependenciesRemapperFactory {
func build(
orderKeys: [String],
envs: [String: String],
customMappings: [String: String]
) throws -> StringDependenciesRemapper {
let mappingMap = try envs.merging(customMappings) { envValue, outOfBandMapping in
throw StringDependenciesRemapperFactoryError.mappingKeyDuplication
let mappingMap = try envs.merging(customMappings) { _, _ in
throw PathDependenciesRemapperFactoryError.mappingKeyDuplication
}
let mappingOrderKeys = orderKeys + customMappings.keys
let mappings: [StringDependenciesRemapper.Mapping] = try mappingOrderKeys.map { key in
let localValue: String = try mappingMap.readEnv(key: key)
return StringDependenciesRemapper.Mapping(generic: "$(\(key))", local: localValue)
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"
@@ -31,7 +31,7 @@ class EnvironmentFingerprintGenerator {
"DYLIB_COMPATIBILITY_VERSION",
"DYLIB_CURRENT_VERSION",
"PRODUCT_MODULE_NAME",
"ARCHS"
"ARCHS",
]
private let version: String
private let customFingerprintEnvs: [String]
+13 -7
View File
@@ -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)) "
}
@@ -20,7 +20,7 @@
import Foundation
protocol MetaWriter {
func write<T>(_ meta: T, locationDir : URL) throws -> URL where T : Meta
func write<T>(_ meta: T, locationDir: URL) throws -> URL where T: Meta
}
class JsonMetaWriter: MetaWriter {
@@ -36,7 +36,7 @@ class JsonMetaWriter: MetaWriter {
self.metaEncoder = encoder
}
func write<T>(_ meta: T, locationDir : URL) throws -> URL where T : Meta {
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)
@@ -25,7 +25,7 @@ final class IgnoringCertificatesTrustManager: NSObject, URLSessionDelegate {
completionHandler(.performDefaultHandling, nil)
return
}
let urlCredential = URLCredential(trust: serverTrust)
completionHandler(.useCredential, urlCredential)
}
@@ -36,6 +36,8 @@ class DefaultURLSessionFactory: URLSessionFactory {
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = config.requestCustomHeaders
configuration.timeoutIntervalForRequest = config.timeoutResponseDataChunksInterval
configuration.urlCache?.memoryCapacity = 0
configuration.urlCache?.diskCapacity = 0
switch config.disableCertificateVerification {
case true:
return URLSession(
@@ -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
}
+8 -1
View File
@@ -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
@@ -27,6 +27,7 @@ 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!
@@ -37,6 +38,7 @@ class ArtifactSwiftProductsBuilderImplTests: FileXCTestCase {
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,
@@ -69,7 +71,7 @@ class ArtifactSwiftProductsBuilderImplTests: FileXCTestCase {
)
}
func testIncludesAllSwiftmoduleFiles() throws {
func testIncludesAllBasicSwiftmoduleFiles() throws {
try fileManager.spt_createEmptyFile(swiftmoduleFile)
try fileManager.spt_createEmptyFile(swiftmoduleDocFile)
try fileManager.spt_createEmptyFile(swiftmoduleSourceInfoFile)
@@ -91,6 +93,32 @@ 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(
@@ -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")
@@ -114,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)
@@ -131,7 +131,7 @@ class ThinningCreatorPluginTests: FileXCTestCase {
XCTAssertEqual(extraKeys, [
"thinning_Generated": "000",
"thinning_Reused": "999"
"thinning_Reused": "999",
])
}
}
@@ -42,7 +42,7 @@ class PostbuildContextTests: FileXCTestCase {
"DWARF_DSYM_FILE_NAME": "DWARF_DSYM_FILE_NAME",
"BUILT_PRODUCTS_DIR": "BUILT_PRODUCTS_DIR",
"DERIVED_SOURCES_DIR": "DERIVED_SOURCES_DIR",
"CURRENT_VARIANT": "normal"
"CURRENT_VARIANT": "normal",
]
override func setUpWithError() throws {
@@ -20,6 +20,7 @@
@testable import XCRemoteCache
import XCTest
// swiftlint:disable file_length
// swiftlint:disable:next type_body_length
class PostbuildTests: FileXCTestCase {
private var postbuildContext = PostbuildContext(
@@ -51,7 +52,8 @@ class PostbuildTests: FileXCTestCase {
derivedSourcesDir: "",
thinnedTargets: [],
action: .build,
modeMarkerPath: ""
modeMarkerPath: "",
overlayHeadersPath: ""
)
private var network = RemoteNetworkClientImpl(
NetworkClientFake(fileManager: .default),
@@ -638,4 +640,3 @@ class PostbuildTests: FileXCTestCase {
XCTAssertEqual(downloadedMeta, expectedMeta)
}
}
// swiftlint:disable:next file_length
@@ -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(
@@ -0,0 +1,62 @@
// 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"),
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)
}
}
@@ -280,6 +280,9 @@ class SwiftcTests: FileXCTestCase {
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(
@@ -303,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)
}
@@ -29,89 +29,119 @@ 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")])
StringDependenciesRemapper(mappings: [
StringDependenciesRemapper.Mapping(
generic: "$(ROOT)",
local: "/root"
),
]),
StringDependenciesRemapper(mappings: [
StringDependenciesRemapper.Mapping(
generic: "$(SPECIFIC)",
local: "$(ROOT)/specific"
),
]),
])
let localPaths = ["/root/specific/file"]
let genericPaths = remapper.replace(localPaths: localPaths)
let 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")])
StringDependenciesRemapper(mappings: [
StringDependenciesRemapper.Mapping(
generic: "$(ROOT)",
local: "/root"
),
]),
StringDependenciesRemapper(mappings: [
StringDependenciesRemapper.Mapping(
generic: "$(SPECIFIC)",
local: "$(ROOT)/specific"
),
]),
])
let genericPaths = ["$(SPECIFIC)/file"]
let localPaths = remapper.replace(genericPaths: genericPaths)
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")])
StringDependenciesRemapper(mappings: [
StringDependenciesRemapper.Mapping(
generic: "$(ROOT)",
local: "/root"
),
]),
StringDependenciesRemapper(mappings: [
StringDependenciesRemapper.Mapping(
generic: "$(SPECIFIC)",
local: "$(ROOT)/specific"
),
]),
])
let localPaths = ["/root/specific/file"]
let genericPaths = remapper.replace(localPaths: localPaths)
let remappedLocalPaths = remapper.replace(genericPaths: genericPaths)
let genericPaths = try remapper.replace(localPaths: localPaths)
let remappedLocalPaths = try remapper.replace(genericPaths: genericPaths)
XCTAssertEqual(localPaths, remappedLocalPaths)
}
@@ -20,7 +20,7 @@
@testable import XCRemoteCache
import XCTest
class DependencyProcessorImplTests: XCTestCase {
class DependencyProcessorImplTests: FileXCTestCase {
let processor = DependencyProcessorImpl(
xcode: "/Xcode",
@@ -70,6 +70,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 +117,76 @@ 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,
bundle: "/Bundle"
)
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",
bundle: "/Bundle"
)
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)
}
}
@@ -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
))
}
}
@@ -20,11 +20,12 @@
@testable import XCRemoteCache
import XCTest
class StringDependenciesRemapperFactoryTests: XCTestCase {
private var factory: StringDependenciesRemapperFactory!
class PathDependenciesRemapperFactoryTests: XCTestCase {
private var factory: PathDependenciesRemapperFactory!
override func setUp() {
factory = StringDependenciesRemapperFactory()
super.setUp()
factory = PathDependenciesRemapperFactory()
}
func testMappingsFromEnvMaps() throws {
@@ -34,12 +35,34 @@ class StringDependenciesRemapperFactoryTests: XCTestCase {
customMappings: [:]
)
let localPaths = remapper.replace(genericPaths: ["$(SRC_ROOT)/some.swift"])
let localPaths = try remapper.replace(genericPaths: ["$(SRC_ROOT)/some.swift"])
XCTAssertEqual(localPaths, ["/tmp/root/some.swift"])
}
func testInvalidMappingsFromEnvFails() throws {
XCTAssertThrowsError(
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": ""],
@@ -55,7 +78,7 @@ class StringDependenciesRemapperFactoryTests: XCTestCase {
customMappings: ["TMP": "/tmp"]
)
let genericPaths = remapper.replace(localPaths: ["/some/repoFile.swift", "/tmp/externalFile.swift"])
let genericPaths = try remapper.replace(localPaths: ["/some/repoFile.swift", "/tmp/externalFile.swift"])
XCTAssertEqual(genericPaths, ["$(PWD)/repoFile.swift", "$(TMP)/externalFile.swift"])
}
@@ -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,12 +65,12 @@ 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 testMappingsLocalPathsIsDoneInOrder() {
func testMappingsLocalPathsIsDoneInOrder() throws {
let mappings: [StringDependenciesRemapper.Mapping] = [
.init(generic: "$(TMP)", local: "/tmp"),
.init(generic: "$(ROOT)", local: "$(TMP)/root"),
@@ -78,12 +78,12 @@ class StringDependenciesRemapperTests: XCTestCase {
remapper = StringDependenciesRemapper(mappings: mappings)
let genericPaths = remapper.replace(localPaths: ["/tmp/root/some.swift"])
let genericPaths = try remapper.replace(localPaths: ["/tmp/root/some.swift"])
XCTAssertEqual(genericPaths, ["$(ROOT)/some.swift"])
}
func testMappingsGenericPathsIsDoneInReversedOrder() {
func testMappingsGenericPathsIsDoneInReversedOrder() throws {
let mappings: [StringDependenciesRemapper.Mapping] = [
.init(generic: "$(TMP)", local: "/tmp"),
.init(generic: "$(ROOT)", local: "$(TMP)/root"),
@@ -91,7 +91,7 @@ class StringDependenciesRemapperTests: XCTestCase {
remapper = StringDependenciesRemapper(mappings: mappings)
let localPaths = remapper.replace(genericPaths: ["$(ROOT)/some.swift"])
let localPaths = try remapper.replace(genericPaths: ["$(ROOT)/some.swift"])
XCTAssertEqual(localPaths, ["/tmp/root/some.swift"])
}
@@ -33,7 +33,7 @@ class EnvironmentFingerprintGeneratorTests: XCTestCase {
"DYLIB_COMPATIBILITY_VERSION": "2",
"DYLIB_CURRENT_VERSION": "3",
"PRODUCT_MODULE_NAME": "4",
"ARCHS": "AR"
"ARCHS": "AR",
]
/// Corresponds to EnvironmentFingerprintGenerator.version
private static let currentVersion = "5"
@@ -0,0 +1,31 @@
{
"case-sensitive": "false",
"roots":
[
{
"contents":
[
{
"external-contents": "/Path/Target1/Target1.h",
"name": "Target1.h",
"type": "file",
},
],
"name": "/DerivedDataProducts/Target1.framework/Headers",
"type": "directory",
},
{
"contents":
[
{
"external-contents": "/DerivedDataIntermediate/Target2.build/module.modulemap",
"name": "module.modulemap",
"type": "file",
},
],
"name": "/DerivedDataProducts/Target2.framework/Modules",
"type": "directory",
},
],
"version": 0,
}
@@ -0,0 +1 @@
{"case-sensitive":"false","roots":[],"version":0}
@@ -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.
import Foundation
@testable import XCRemoteCache
class OverlayReaderFake: OverlayReader {
var mappings: [OverlayMapping]
init(mappings: [OverlayMapping]) {
self.mappings = mappings
}
func provideMappings() throws -> [OverlayMapping] {
return mappings
}
}
@@ -82,8 +82,8 @@ module CocoapodsXCRemoteCacheModifier
end
mode = @@configuration['mode']
unless mode == 'consumer' || mode == 'producer'
throw "Incorrect 'mode' value. Allowed values are ['consumer', 'producer'], but you provided '#{mode}'. A typo?"
unless mode == 'consumer' || mode == 'producer' || mode == 'producer-fast'
throw "Incorrect 'mode' value. Allowed values are ['consumer', 'producer', 'producer-fast'], but you provided '#{mode}'. A typo?"
end
unless mode == 'consumer' || @@configuration.key?('final_target')
@@ -103,7 +103,7 @@ module CocoapodsXCRemoteCacheModifier
# @param repo_distance [Integer] distance from the git repo root to the target's $SRCROOT
# @param xc_location [String] path to the dir with all XCRemoteCache binaries, relative to the repo root
# @param xc_cc_path [String] path to the XCRemoteCache clang wrapper, relative to the repo root
# @param mode [String] mode name ('consumer', 'producer' etc.)
# @param mode [String] mode name ('consumer', 'producer', 'producer-fast' etc.)
# @param exclude_build_configurations [String[]] list of targets that should have disabled remote cache
# @param final_target [String] name of target that should trigger marking
def self.enable_xcremotecache(target, repo_distance, xc_location, xc_cc_path, mode, exclude_build_configurations, final_target)
@@ -114,6 +114,8 @@ module CocoapodsXCRemoteCacheModifier
next if exclude_build_configurations.include?(config.name)
if mode == 'consumer'
config.build_settings['CC'] = ["$SRCROOT/#{parent_dir(xc_cc_path, repo_distance)}"]
elsif mode == 'producer' || mode == 'producer-fast'
config.build_settings.delete('CC') if config.build_settings.key?('CC')
end
config.build_settings['SWIFT_EXEC'] = ["$SRCROOT/#{srcroot_relative_xc_location}/xcswiftc"]
config.build_settings['LIBTOOL'] = ["$SRCROOT/#{srcroot_relative_xc_location}/xclibtool"]
@@ -133,7 +135,8 @@ module CocoapodsXCRemoteCacheModifier
phase.name != nil && phase.name.start_with?("[XCRC] Prebuild")
end
end
prebuild_script = existing_prebuild_script || target.new_shell_script_build_phase("[XCRC] Prebuild")
prebuild_script = existing_prebuild_script || target.new_shell_script_build_phase("[XCRC] Prebuild #{target.name}")
prebuild_script.shell_script = "\"$SCRIPT_INPUT_FILE_0\""
prebuild_script.input_paths = ["$SRCROOT/#{srcroot_relative_xc_location}/xcprebuild"]
prebuild_script.output_paths = [
@@ -142,9 +145,10 @@ module CocoapodsXCRemoteCacheModifier
]
prebuild_script.dependency_file = "$(TARGET_TEMP_DIR)/prebuild.d"
# Move prebuild (last element) to the first position (to make it real 'prebuild')
target.build_phases.rotate!(-1) if existing_prebuild_script.nil?
elsif mode == 'producer'
# Move prebuild (last element) to the position before compile sources phase (to make it real 'prebuild')
compile_phase_index = target.build_phases.index(target.source_build_phase)
target.build_phases.insert(compile_phase_index, target.build_phases.delete(prebuild_script))
elsif mode == 'producer' || mode == 'producer-fast'
# Delete existing prebuild build phase (to support switching between modes)
target.build_phases.delete_if do |phase|
if phase.respond_to?(:name)
@@ -159,7 +163,7 @@ module CocoapodsXCRemoteCacheModifier
phase.name != nil && phase.name.start_with?("[XCRC] Postbuild")
end
end
postbuild_script = existing_postbuild_script || target.new_shell_script_build_phase("[XCRC] Postbuild")
postbuild_script = existing_postbuild_script || target.new_shell_script_build_phase("[XCRC] Postbuild #{target.name}")
postbuild_script.shell_script = "\"$SCRIPT_INPUT_FILE_0\""
postbuild_script.input_paths = ["$SRCROOT/#{srcroot_relative_xc_location}/xcpostbuild"]
postbuild_script.output_paths = [
@@ -169,7 +173,7 @@ module CocoapodsXCRemoteCacheModifier
postbuild_script.dependency_file = "$(TARGET_TEMP_DIR)/postbuild.d"
# Mark a sha as ready for a given platform and configuration when building the final_target
if mode == 'producer' && target.name == final_target
if (mode == 'producer' || mode == 'producer-fast') && target.name == final_target
existing_mark_script = target.build_phases.detect do |phase|
if phase.respond_to?(:name)
phase.name != nil && phase.name.start_with?("[XCRC] Mark")
@@ -194,8 +198,9 @@ module CocoapodsXCRemoteCacheModifier
config.build_settings.delete('SWIFT_EXEC') if config.build_settings.key?('SWIFT_EXEC')
config.build_settings.delete('LIBTOOL') if config.build_settings.key?('LIBTOOL')
config.build_settings.delete('LD') if config.build_settings.key?('LD')
# Add Fake src root for ObjC & Swift
# Remove Fake src root for ObjC & Swift
config.build_settings.delete('XCREMOTE_CACHE_FAKE_SRCROOT')
config.build_settings.delete('XCRC_PLATFORM_PREFERRED_ARCH')
remove_cflags!(config.build_settings, '-fdebug-prefix-map')
remove_swiftflags!(config.build_settings, '-debug-prefix-map')
end
@@ -257,14 +262,14 @@ module CocoapodsXCRemoteCacheModifier
end
def self.add_cflags!(options, key, value)
return if options.fetch('OTHER_CFLAGS',[]).include?(' ' + value)
options['OTHER_CFLAGS'] = remove_cflags!(options, key) << " #{key}=#{value}"
return if options.fetch('OTHER_CFLAGS',[]).include?(value)
options['OTHER_CFLAGS'] = remove_cflags!(options, key) << "#{key}=#{value}"
end
def self.remove_cflags!(options, key)
cflags_arr = options.fetch('OTHER_CFLAGS', ['$(inherited)'])
cflags_arr = [cflags_arr] if cflags_arr.kind_of? String
options['OTHER_CFLAGS'] = cflags_arr.delete_if {|flag| flag.start_with?(" #{key}=") }
options['OTHER_CFLAGS'] = cflags_arr.delete_if {|flag| flag.include?("#{key}=") }
options['OTHER_CFLAGS']
end
@@ -279,12 +284,27 @@ module CocoapodsXCRemoteCacheModifier
end
# Uninstall the XCRemoteCache
def self.disable_xcremotecache(user_project)
def self.disable_xcremotecache(user_project, pods_project = nil)
user_project.targets.each do |target|
disable_xcremotecache_for_target(target)
end
user_project.save()
unless pods_project.nil?
pods_project.native_targets.each do |target|
disable_xcremotecache_for_target(target)
end
pods_proj_directory = pods_project.project_dir
pods_project.root_object.project_references.each do |subproj_ref|
generated_project = Xcodeproj::Project.open("#{pods_proj_directory}/#{subproj_ref[:project_ref].path}")
generated_project.native_targets.each do |target|
disable_xcremotecache_for_target(target)
end
generated_project.save()
end
pods_project.save()
end
# Remove .lldbinit rewrite
save_lldbinit_rewrite(nil) unless !@@configuration['modify_lldb_init']
end
@@ -375,7 +395,9 @@ module CocoapodsXCRemoteCacheModifier
# Always integrate XCRemoteCache to all Pods, in case it will be needed later
unless installer_context.pods_project.nil?
# Attach XCRemoteCache to Pods targets
installer_context.pods_project.targets.each do |target|
# Enable only for native targets which can have compilation steps
installer_context.pods_project.native_targets.each do |target|
next if target.source_build_phase.files_references.empty?
next if target.name.start_with?("Pods-")
next if target.name.end_with?("Tests")
next if exclude_targets.include?(target.name)
@@ -385,6 +407,18 @@ module CocoapodsXCRemoteCacheModifier
# Create .rcinfo into `Pods` directory as that .xcodeproj reads configuration from .xcodeproj location
pods_proj_directory = installer_context.sandbox_root
# Attach XCRemoteCache to Generated Pods projects
installer_context.pods_project.root_object.project_references.each do |subproj_ref|
generated_project = Xcodeproj::Project.open("#{pods_proj_directory}/#{subproj_ref[:project_ref].path}")
generated_project.native_targets.each do |target|
next if target.source_build_phase.files_references.empty?
next if target.name.end_with?("Tests")
next if exclude_targets.include?(target.name)
enable_xcremotecache(target, 1, xcrc_location, xccc_location, mode, exclude_build_configurations, final_target)
end
generated_project.save()
end
# Manual Pods/.rcinfo generation
# all paths in .rcinfo are relative to the root so paths used in Pods.xcodeproj need to be aligned
@@ -406,12 +440,12 @@ module CocoapodsXCRemoteCacheModifier
prepare_result = YAML.load`#{xcrc_location_absolute}/xcprepare --configuration #{check_build_configuration} --platform #{check_platform}`
unless prepare_result['result'] || mode != 'consumer'
# Uninstall the XCRemoteCache for the consumer mode
disable_xcremotecache(user_project)
disable_xcremotecache(user_project, installer_context.pods_project)
Pod::UI.puts "[XCRC] XCRemoteCache disabled - no artifacts available"
next
end
rescue => error
disable_xcremotecache(user_project)
disable_xcremotecache(user_project, installer_context.pods_project)
Pod::UI.puts "[XCRC] XCRemoteCache failed with an error: #{error}."
next
end
@@ -436,7 +470,7 @@ module CocoapodsXCRemoteCacheModifier
rescue Exception => e
Pod::UI.puts "[XCRC] XCRemoteCache disabled with error: #{e}"
puts e.full_message(highlight: true, order: :top)
disable_xcremotecache(user_project)
disable_xcremotecache(user_project, installer_context.pods_project)
end
end
end
@@ -13,5 +13,5 @@
# limitations under the License.
module CocoapodsXcremotecache
VERSION = "0.0.5"
VERSION = "0.0.7"
end
+40 -1
View File
@@ -23,9 +23,48 @@ If you prefer to edit in Xcode, run `swift package generate-xcodeproj`. Do **not
The generated Xcode project contains schemes for each output application (like `xcswiftc`, `xcprebuild` etc.) so to build a single app, just select the appropriate scheme and build (⌘+B). If you want to build all applications at once, select `Aggregator` scheme that automatically builds all apps. `Aggregator` target in `Package.swift` is defined only for development convenience, it shouldn't be ever used as a dependency.
#### Debugging the app in a real project
Debugging XCRemoteCache in a real project is simple:
* Open XCRemoteCache's `Package.swift` in Xcode and select a scheme that corresponds to the process you want to debug, e.g. `xcpostbuild`
![Select scheme](./img/debug-scheme.png)
* Build the app with Product->Build (or ⌘+B)
* Open the scheme's settings (⌘+⇧+<) and for the "Run" action, select "Wait for the executable to be launched"
![Configure scheme](./img/debug-scheme-wait.png)
* Find the produced binary in your DerivedData. The default location would be `~/Library/Developer/Xcode/DerivedData/XCRemoteCache-{hash}/Build/Products/Debug/xcpostbuild`
* In the project you want to debug, pick the target you want to inspect and expand the build phase that calls the process
* Replace the Input Files path so it points the locally built binary placed in DerivedData
![Update phase](./img/debug-phase-update.png)
* Now, you can run the **XCRemoteCache** project (not the project you want to debug). Because Xcode will not initiate a new process, it will just wait until someone else triggers it.
![LLDB waiting](./img/debug-wait.png)
* Finally, build the project to want to debug and expect your XCRemoteCache breakpoints to hit
![Breakpoint hit](./img/debug-breakpoint.png)
> Tip: If your breakpoints don't hit, try adding a dummy `sleep(1)` in the process entry point (e.g. at the top of `XCPostbuild.main` function)
#### Running tests in Xcode
All unit tests are placed in `XCRemoteCacheTests`. To run them from Xcode, just pick any application scheme and run tests (⌘+U).
All unit tests are placed in `XCRemoteCacheTests`. To run them from Xcode, just pick any application scheme and run tests (⌘+U).
#### Running E2E tests
E2E tests build a CocoaPods plugin, locally build both `producer` and `consumer` modes and verify 100% hit rate. All `Podfile` templates are placed in [e2eTests/tests](../e2eTests/tests). As a backend server, nginx server with a sample [nginx.conf](../e2eTests/nginx/nginx.conf) configuration is used. The sample server exposes local location `/tmp/cache` under http://localhost:8080.
To run tests locally, install `nginx` (e.g. `brew install nginx`) and call:
```bash
rake 'build[release]'
rake e2e_only
```
## Project organization
+15
View File
@@ -50,3 +50,18 @@ log show --predicate 'sender == "xcprepare"' --style compact --info --debug -las
log show --predicate 'sender BEGINSWITH "xc"' --style compact --info --debug -last 10m
```
</details>
### Troubleshooting cache misses
Here is a non-exhaustive list of steps that may help with troubleshooting poor cache hit rate.
1. ***Producer&Consumer:*** Review XCRemoteCache [Requirements](../#Requirements) and [Limitations](../#limitations)
1. ***Producer&Consumer:*** Make sure a producer build uses the same architecture(s) as a consumer. You can inspect `ARCHS` Build Setting in Xcode's Script Phase output logs. Navigate to the report navigator (⌘+9) and expand XCRemoteCache's `prebuild` step output using the "collapsed menu icon" (aka hamburger menu)
1. ***Producer:*** Verify that all Xcode targets have a Build Phase called `postbuild`
1. ***Producer:*** If you are using optional XCRemoteCache auto-marking feature (`--final-producer-target` or `final_target`) verify an extra Build Phase called `mark` is added to the specified target
1. ***Producer:*** After a full build, review logs according to [docs](#how-can-i-find-xcremotecache-logs)
1. ***Consumer:*** Verify that all Xcode targets have extra XCRemoteCache Build Phase called `prebuild` and `postbuild`
1. ***Consumer:*** After a full build, review according to [docs](#how-can-i-find-xcremotecache-logs). Find a ***first:*** target that reports a cache miss with a message like `Prebuild step failed with error: ...`. If a target reports faces a cache miss, it may have a knock-on effect where a lot of its consumers (dependant targets) need to be built locally too
1. ***Consumer:*** ***After a full build, review all meta files placed in `~/Library/Caches/XCRemoteCache/{your_host_path}/meta/*.json` and make sure no absolute paths are used in its `dependencies`. All paths should start a placeholder, like `$(SRCROOT)` or `$(BUILD_DIR)`***
1. ***Consumer:*** If you are integrating XCRemoteCache and rebuild artifacts for the same sha, previously downloaded artifacts placed in a local cache may still be used on a consumer side. You can either manually delete your local cache at `~/Library/Caches/XCRemoteCache/` before any consumer build or disable a local cache with `artifact_maximum_age: 0` property in `.rcinfo`
1. ***Consumer:*** To find an actual cache hit, before building in Xcode reset statistics with `xcprepare stats --reset` and once it is done, call `xcprepare stats` to find a cache hit rate
Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

@@ -0,0 +1,363 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 55;
objects = {
/* Begin PBXBuildFile section */
3695D9CC27A3218C007F3792 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3695D9CB27A3218C007F3792 /* AppDelegate.swift */; };
3695D9CE27A3218C007F3792 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3695D9CD27A3218C007F3792 /* SceneDelegate.swift */; };
3695D9D027A3218C007F3792 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3695D9CF27A3218C007F3792 /* ViewController.swift */; };
3695D9D327A3218C007F3792 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3695D9D127A3218C007F3792 /* Main.storyboard */; };
3695D9D527A3218D007F3792 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3695D9D427A3218D007F3792 /* Assets.xcassets */; };
3695D9D827A3218D007F3792 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3695D9D627A3218D007F3792 /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
3695D9C827A3218C007F3792 /* XCRemoteCacheSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = XCRemoteCacheSample.app; sourceTree = BUILT_PRODUCTS_DIR; };
3695D9CB27A3218C007F3792 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
3695D9CD27A3218C007F3792 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
3695D9CF27A3218C007F3792 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
3695D9D227A3218C007F3792 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
3695D9D427A3218D007F3792 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
3695D9D727A3218D007F3792 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
3695D9D927A3218D007F3792 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
3695D9C527A3218C007F3792 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
3695D9BF27A3218C007F3792 = {
isa = PBXGroup;
children = (
3695D9CA27A3218C007F3792 /* XCRemoteCacheSample */,
3695D9C927A3218C007F3792 /* Products */,
);
sourceTree = "<group>";
};
3695D9C927A3218C007F3792 /* Products */ = {
isa = PBXGroup;
children = (
3695D9C827A3218C007F3792 /* XCRemoteCacheSample.app */,
);
name = Products;
sourceTree = "<group>";
};
3695D9CA27A3218C007F3792 /* XCRemoteCacheSample */ = {
isa = PBXGroup;
children = (
3695D9CB27A3218C007F3792 /* AppDelegate.swift */,
3695D9CD27A3218C007F3792 /* SceneDelegate.swift */,
3695D9CF27A3218C007F3792 /* ViewController.swift */,
3695D9D127A3218C007F3792 /* Main.storyboard */,
3695D9D427A3218D007F3792 /* Assets.xcassets */,
3695D9D627A3218D007F3792 /* LaunchScreen.storyboard */,
3695D9D927A3218D007F3792 /* Info.plist */,
);
path = XCRemoteCacheSample;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
3695D9C727A3218C007F3792 /* XCRemoteCacheSample */ = {
isa = PBXNativeTarget;
buildConfigurationList = 3695D9DC27A3218D007F3792 /* Build configuration list for PBXNativeTarget "XCRemoteCacheSample" */;
buildPhases = (
3695D9C427A3218C007F3792 /* Sources */,
3695D9C527A3218C007F3792 /* Frameworks */,
3695D9C627A3218C007F3792 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = XCRemoteCacheSample;
productName = XCRemoteCacheSample;
productReference = 3695D9C827A3218C007F3792 /* XCRemoteCacheSample.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
3695D9C027A3218C007F3792 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1320;
LastUpgradeCheck = 1320;
TargetAttributes = {
3695D9C727A3218C007F3792 = {
CreatedOnToolsVersion = 13.2.1;
};
};
};
buildConfigurationList = 3695D9C327A3218C007F3792 /* Build configuration list for PBXProject "XCRemoteCacheSample" */;
compatibilityVersion = "Xcode 13.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 3695D9BF27A3218C007F3792;
productRefGroup = 3695D9C927A3218C007F3792 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
3695D9C727A3218C007F3792 /* XCRemoteCacheSample */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
3695D9C627A3218C007F3792 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
3695D9D827A3218D007F3792 /* LaunchScreen.storyboard in Resources */,
3695D9D527A3218D007F3792 /* Assets.xcassets in Resources */,
3695D9D327A3218C007F3792 /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
3695D9C427A3218C007F3792 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
3695D9D027A3218C007F3792 /* ViewController.swift in Sources */,
3695D9CC27A3218C007F3792 /* AppDelegate.swift in Sources */,
3695D9CE27A3218C007F3792 /* SceneDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
3695D9D127A3218C007F3792 /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
3695D9D227A3218C007F3792 /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
3695D9D627A3218D007F3792 /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
3695D9D727A3218D007F3792 /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
3695D9DA27A3218D007F3792 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.2;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
3695D9DB27A3218D007F3792 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.2;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
3695D9DD27A3218D007F3792 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = XCRemoteCacheSample/Info.plist;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UIMainStoryboardFile = Main;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.xcremotecache.XCRemoteCacheSample;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
3695D9DE27A3218D007F3792 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = XCRemoteCacheSample/Info.plist;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UIMainStoryboardFile = Main;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.xcremotecache.XCRemoteCacheSample;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
3695D9C327A3218C007F3792 /* Build configuration list for PBXProject "XCRemoteCacheSample" */ = {
isa = XCConfigurationList;
buildConfigurations = (
3695D9DA27A3218D007F3792 /* Debug */,
3695D9DB27A3218D007F3792 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
3695D9DC27A3218D007F3792 /* Build configuration list for PBXNativeTarget "XCRemoteCacheSample" */ = {
isa = XCConfigurationList;
buildConfigurations = (
3695D9DD27A3218D007F3792 /* Debug */,
3695D9DE27A3218D007F3792 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 3695D9C027A3218C007F3792 /* Project object */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,39 @@
// 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 UIKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
return true
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
}
}
@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,98 @@
{
"images" : [
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
</dict>
</plist>
@@ -0,0 +1,43 @@
// Copyright (c) 2021 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
}
func sceneDidDisconnect(_ scene: UIScene) {
}
func sceneDidBecomeActive(_ scene: UIScene) {
}
func sceneWillResignActive(_ scene: UIScene) {
}
func sceneWillEnterForeground(_ scene: UIScene) {
}
func sceneDidEnterBackground(_ scene: UIScene) {
}
}
@@ -0,0 +1,29 @@
// 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 UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
}
+43
View File
@@ -0,0 +1,43 @@
# Nginx configuration used for E2E tests cache server
# Listens HTTP on port 8080
worker_processes 1;
events {
worker_connections 1024;
}
http {
default_type application/octet-stream;
server {
listen 8080;
server_name localhost;
location / {
root html;
index index.html index.htm;
}
location /cache/ {
# The path to the directory where nginx should store the cache contents.
root /tmp/cache;
# Allow PUT
dav_methods PUT;
create_full_put_path on;
# The maximum size of a single file.
client_max_body_size 1G;
allow all;
}
sendfile on;
keepalive_timeout 65;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
+8
View File
@@ -0,0 +1,8 @@
plugin 'cocoapods-xcremotecache'
target 'XCRemoteCacheSample' do
use_frameworks!
pod 'Firebase/Analytics'
pod 'ReactiveSwift'
end
+11
View File
@@ -0,0 +1,11 @@
plugin 'cocoapods-xcremotecache'
install! 'cocoapods', :generate_multiple_pod_projects => true
target 'XCRemoteCacheSample' do
use_frameworks!
pod 'Firebase/Analytics'
pod 'ReactiveSwift'
end
+186
View File
@@ -0,0 +1,186 @@
require 'json'
desc 'Support for E2E tests: building XCRemoteCache-enabled xcodeproj using xcodebuild'
namespace :e2e do
COCOAPODS_DIR = 'cocoapods-plugin'
COCOAPODS_GEMSPEC_FILENAME = "cocoapods-xcremotecache.gemspec"
E2E_COCOAPODS_SAMPLE_DIR = 'e2eTests/XCRemoteCacheSample'
GIT_REMOTE_NAME = 'self'
# Location of the remote address that points to itself
GIT_REMOTE_ADDRESS = '.'
GIT_BRANCH = 'e2e-test-branch'
LOG_NAME = "xcodebuild.log"
DERIVED_DATA_PATH = './DerivedData'
NGINX_ROOT_DIR = '/tmp/cache'
XCRC_BINARIES = 'XCRC'
SHARED_COCOAPODS_CONFIG = {
'cache_addresses' => ['http://localhost:8080/cache/pods'],
'primary_repo' => GIT_REMOTE_ADDRESS,
'primary_branch' => GIT_BRANCH,
'mode' => 'consumer',
'final_target' => 'XCRemoteCacheSample',
'artifact_maximum_age' => 0
}
Stats = Struct.new(:hits, :misses, :hit_rate)
# run E2E tests
task :run => [:run_cocoapods]
# run E2E tests for CocoaPods-powered projects
task :run_cocoapods do
install_cocoapods_plugin
start_nginx
configure_git
# Run scenarios for all Podfile scenarios
for podfile_path in Dir.glob('e2eTests/**/*.Podfile')
run_cocoapods_scenario(podfile_path)
end
# Revert all side effects
clean
end
# Build and install a plugin
def self.install_cocoapods_plugin
Dir.chdir(COCOAPODS_DIR) do
gemfile_path = "cocoapods-xcremotecache.gem"
system("gem build #{COCOAPODS_GEMSPEC_FILENAME} -o #{gemfile_path}")
system("gem install #{gemfile_path}")
end
end
def self.start_nginx
# Start nginx server
system('nginx -c $PWD/e2eTests/nginx/nginx.conf')
puts('starting nginx')
# Call cleanup on exit
at_exit { puts('resetting ngingx'); system('nginx -s stop') }
end
# Create a new branch out of a current commit and
# add remote that points to itself
def self.configure_git
system("git checkout -B #{GIT_BRANCH}")
system("git remote add #{GIT_REMOTE_NAME} #{GIT_REMOTE_ADDRESS} && git fetch -q #{GIT_REMOTE_NAME}")
# Revert new remote on exit
at_exit { system("git remote remove #{GIT_REMOTE_NAME}")}
end
def self.pre_producer_setup
clean_git
clean_server
# Link prebuilt binaries to the Project
system("ln -s $(pwd)/releases #{E2E_COCOAPODS_SAMPLE_DIR}/#{XCRC_BINARIES}")
end
def self.pre_consumer_setup
clean_git
# Link prebuilt binaries to the Project
system("ln -s $(pwd)/releases #{E2E_COCOAPODS_SAMPLE_DIR}/#{XCRC_BINARIES}")
end
def self.clean_server
system("rm -rf #{NGINX_ROOT_DIR}")
end
# Revert any local changes in the test project
def self.clean_git
system("git clean -xdf #{E2E_COCOAPODS_SAMPLE_DIR}")
end
# Cleans all extra locations that a test creates
def self.clean
clean_git
clean_server
end
# xcremotecache configuration to add to Podfile
def self.cocoapods_configuration_string(extra_configs = {})
configuration_lines = ['xcremotecache({']
all_properties = SHARED_COCOAPODS_CONFIG.merge(extra_configs)
config_lines = all_properties.map {|key, value| " \"#{key}\" => #{value.inspect},"}
configuration_lines.push(*config_lines)
configuration_lines << '})'
configuration_lines.join("\n")
end
def self.dump_podfile(config, source)
# Create producer Podfile
File.open("#{E2E_COCOAPODS_SAMPLE_DIR}/Podfile", 'w') do |f|
# Copy podfile template
File.foreach(source) { |line| f.puts line }
f.write(config)
end
end
def self.build_project(extra_args = {})
system('pod install')
xcodebuild_args = {
'workspace' => 'XCRemoteCacheSample.xcworkspace',
'scheme' => 'XCRemoteCacheSample',
'configuration' => 'Debug',
'sdk' => 'iphonesimulator',
'destination' => 'generic/platform=iOS Simulator',
'derivedDataPath' => DERIVED_DATA_PATH,
}.merge(extra_args)
xcodebuild_vars = {
'EXCLUDED_ARCHS' => 'arm64 i386'
}
args = ['xcodebuild']
args.push(*xcodebuild_args.map {|k,v| "-#{k} '#{v}'"})
args.push(*xcodebuild_vars.map {|k,v| "#{k}='#{v}'"})
args.push('clean build')
args.push("> #{LOG_NAME}")
puts 'Building a project with xcodebuild...'
system(args.join(' '))
unless $?.success?
system("tail #{LOG_NAME}")
raise "xcodebuild failed."
end
end
def self.read_stats
stats_json_string = JSON.parse(`#{XCRC_BINARIES}/xcprepare stats --format json`)
misses = stats_json_string.fetch('miss_count', 0)
hits = stats_json_string.fetch('hit_count', 0)
all_targets = misses + hits
raise "Failure: No XCRemoteCache targets invoked" if all_targets == 0
hit_rate = hits * 100 / all_targets
Stats.new(hits, misses, hit_rate)
end
# validate 100% hit rate
def self.valide_hit_rate
status = read_stats()
raise "Failure: Hit rate is only #{status.hit_rate}% (#{status.hits}/#{status.all_targets})" if status.misses > 0
all_targets = status.misses + status.hits
puts("Hit rate: #{status.hit_rate}% (#{status.hits}/#{all_targets})")
end
def self.run_cocoapods_scenario(template_path)
producer_configuration = cocoapods_configuration_string({'mode' => 'producer'})
consumer_configuration = cocoapods_configuration_string()
puts("****** Scenario: #{template_path}")
# Run producer build
pre_producer_setup
dump_podfile(producer_configuration, template_path)
puts('Building producer ...')
Dir.chdir(E2E_COCOAPODS_SAMPLE_DIR) do
build_project
# reset XCRemoteCache stats
system("#{XCRC_BINARIES}/xcprepare stats --reset --format json")
end
# Run consumer build
pre_consumer_setup
dump_podfile(consumer_configuration, template_path)
puts('Building consumer ...')
Dir.chdir(E2E_COCOAPODS_SAMPLE_DIR) do
build_project({'derivedDataPath' => "#{DERIVED_DATA_PATH}_consumer"})
valide_hit_rate
end
end
end