Compare commits
164 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 11eabdab3d | |||
| de24e609ef | |||
| 850983cbde | |||
| 0064335cc7 | |||
| 1ddadcb361 | |||
| 2f10c6a3a0 | |||
| c7cd649aab | |||
| 8201f7778b | |||
| 504393c3e3 | |||
| 82334dda04 | |||
| 1072979479 | |||
| d741b3f6df | |||
| a0f20b4da3 | |||
| f325b74796 | |||
| a50eae615c | |||
| 3c8f062e95 | |||
| 1cf685e197 | |||
| d2ba874079 | |||
| b439674378 | |||
| de066f2b1c | |||
| 73d7a13246 | |||
| 75fdd27a5f | |||
| 2fa1f4e927 | |||
| 0a64893489 | |||
| 56850cf2b0 | |||
| cef92d2f0b | |||
| b1507b6e60 | |||
| c76c8a7672 | |||
| 816a9c07c3 | |||
| 1e741bc859 | |||
| 398b9b11e4 | |||
| cb76934ca2 | |||
| aa92805e14 | |||
| d6355074b2 | |||
| 070e671ddb | |||
| 4efdbabf3e | |||
| f33819da60 | |||
| f16e6b06f3 | |||
| 25ff5a790b | |||
| f446f6061d | |||
| 4262620c57 | |||
| b46d1e2ca1 | |||
| 88d0666da9 | |||
| 08bf5c21bf | |||
| be97a6a247 | |||
| dff71f8070 | |||
| 36fc5ae1e4 | |||
| 2d7c881b3b | |||
| aed4a124cd | |||
| cda5fc1188 | |||
| 2a5c6edfce | |||
| 7a1703e70f | |||
| 15586755d2 | |||
| e978eb182c | |||
| 7e98cebdd9 | |||
| fbc2982aa3 | |||
| cbcc028cad | |||
| 2acf97eca1 | |||
| 40803cf747 | |||
| 97496ed7b8 | |||
| 45222f8e33 | |||
| 91b5b5e590 | |||
| 8222478817 | |||
| 7a1b5267bf | |||
| 2bd50e1c19 | |||
| f2a7880c24 | |||
| 00cb8cc23d | |||
| 6d0fd51c8e | |||
| 65a16d0964 | |||
| a152d9b159 | |||
| a3cd6bea07 | |||
| 86c762b070 | |||
| bd21156695 | |||
| 3a82ad91b2 | |||
| 5a5bf35c4a | |||
| 1f766ad4a4 | |||
| 3c3cd84d81 | |||
| 7728733aef | |||
| 50580bf9fd | |||
| ceaff318d6 | |||
| 4cc932a592 | |||
| 75bef0baf6 | |||
| 03671e71b7 | |||
| 91505a59d2 | |||
| 59c1d999b1 | |||
| ba58c1c21e | |||
| 96ce18bc31 | |||
| 30e49ef7bc | |||
| d46f09c6dd | |||
| 2fdf6c39e2 | |||
| 064b22a2d1 | |||
| ddeffe75d6 | |||
| 181997e3d2 | |||
| 1d6ac2c171 | |||
| 3dfd6e296f | |||
| c39b286222 | |||
| a55a4547ac | |||
| 51c2007c5b | |||
| fc092fc4e3 | |||
| 3f9596f6e6 | |||
| 15f4ee7eab | |||
| a6325db0a5 | |||
| cfa4fbd070 | |||
| c573ced4f4 | |||
| 0c2afc15fc | |||
| 586e8df56e | |||
| ef3a88c6ec | |||
| 95f95be29e | |||
| 4d12800096 | |||
| fbb456b0e0 | |||
| a321ea3362 | |||
| 963e6858ee | |||
| f9999a402f | |||
| dbfe1dd8d4 | |||
| def12fab6f | |||
| 6fee69f081 | |||
| b74bafa5d7 | |||
| 33de361317 | |||
| 0aeb5aee36 | |||
| 8682731f30 | |||
| 62b637bdaa | |||
| 0bca5e5bc4 | |||
| eb0d4b17b7 | |||
| d9ea5bfd49 | |||
| ee8bf69814 | |||
| 2c0f78056f | |||
| b6b17bdc8a | |||
| a387359930 | |||
| 456233b90e | |||
| d6f6e2e9bc | |||
| a35bd0b394 | |||
| def131f2a0 | |||
| c052ed8ed6 | |||
| d4f9486b92 | |||
| 8241914543 | |||
| b5ff16484f | |||
| 2b9dde9aec | |||
| 136e7a99ff | |||
| c626d51f97 | |||
| e8ddc9297d | |||
| 3b614c6172 | |||
| 87a214104e | |||
| 599e1e229d | |||
| 423da7cc4a | |||
| 4b082e9dd2 | |||
| 36803d6b5d | |||
| 5809bc963c | |||
| cb6626cfbc | |||
| 3e18711e09 | |||
| ccda424791 | |||
| 90d784cc8d | |||
| 6715824195 | |||
| c4e08d4288 | |||
| a2067f8dfe | |||
| 253e9597bd | |||
| 4a93f944fd | |||
| f839b4064b | |||
| e71837b8b2 | |||
| b45792646b | |||
| 1966562eef | |||
| 883b207c5b | |||
| ca137d0ce4 | |||
| b9a633c86f | |||
| 55a87eb4e9 |
@@ -9,11 +9,13 @@ jobs:
|
||||
- uses: actions/checkout@v1
|
||||
- name: SwiftLint
|
||||
uses: norio-nomura/action-swiftlint@3.1.0
|
||||
with:
|
||||
args: --strict
|
||||
|
||||
macOS:
|
||||
runs-on: macOS-latest
|
||||
runs-on: macos-12
|
||||
env:
|
||||
XCODE_VERSION: ${{ '13.1' }}
|
||||
XCODE_VERSION: ${{ '14.2' }}
|
||||
steps:
|
||||
- name: Select Xcode
|
||||
run: "sudo xcode-select -s /Applications/Xcode_$XCODE_VERSION.app"
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
name: Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
docs:
|
||||
runs-on: macos-12
|
||||
env:
|
||||
XCODE_VERSION: ${{ '14.2' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Select Xcode
|
||||
run: "sudo xcode-select -s /Applications/Xcode_$XCODE_VERSION.app"
|
||||
- name: "Generate documentation"
|
||||
run: "swift package --allow-writing-to-directory ./docs generate-documentation --target XCRemoteCache --disable-indexing --transform-for-static-hosting --output-path ./docs --hosting-base-path XCRemoteCache/"
|
||||
- name: Deploy GH-pages
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./docs
|
||||
keep_files: false
|
||||
@@ -6,9 +6,9 @@ on:
|
||||
jobs:
|
||||
macOS:
|
||||
name: Add macOS binaries to release
|
||||
runs-on: macOS-latest
|
||||
runs-on: macos-12
|
||||
env:
|
||||
XCODE_VERSION: ${{ '13.1' }}
|
||||
XCODE_VERSION: ${{ '14.2' }}
|
||||
steps:
|
||||
- name: Select Xcode
|
||||
run: "sudo xcode-select -s /Applications/Xcode_$XCODE_VERSION.app"
|
||||
|
||||
+22
-2
@@ -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,8 @@ excluded:
|
||||
- docs/
|
||||
- fastlane/
|
||||
- DerivedData/
|
||||
- e2eTests/XCRemoteCacheSample/Pods
|
||||
- e2eTests/StandaloneSampleApp
|
||||
|
||||
attributes:
|
||||
always_on_same_line:
|
||||
@@ -88,6 +89,25 @@ file_header:
|
||||
\/\/ Created by .*? on .*\.
|
||||
\/\/ Copyright © \d{4} .*\. All rights reserved\.
|
||||
\/\/
|
||||
required_pattern: |
|
||||
\/\/ Copyright \(c\) \d{4} Spotify AB\.
|
||||
\/\/
|
||||
\/\/ Licensed to the Apache Software Foundation \(ASF\) under one
|
||||
\/\/ or more contributor license agreements\. See the NOTICE file
|
||||
\/\/ distributed with this work for additional information
|
||||
\/\/ regarding copyright ownership\. The ASF licenses this file
|
||||
\/\/ to you under the Apache License, Version 2.0 \(the
|
||||
\/\/ "License"\); you may not use this file except in compliance
|
||||
\/\/ with the License\. You may obtain a copy of the License at
|
||||
\/\/
|
||||
\/\/ http:\/\/www.apache.org\/licenses\/LICENSE-2\.0
|
||||
\/\/
|
||||
\/\/ Unless required by applicable law or agreed to in writing,
|
||||
\/\/ software distributed under the License is distributed on an
|
||||
\/\/ \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
\/\/ KIND, either express or implied\. See the License for the
|
||||
\/\/ specific language governing permissions and limitations
|
||||
\/\/ under the License\.
|
||||
force_cast: warning
|
||||
force_try: warning
|
||||
implicit_getter: warning
|
||||
@@ -124,6 +144,6 @@ custom_rules:
|
||||
severity: warning
|
||||
trailing_dot_in_comments:
|
||||
name: "Trailing dot in comments"
|
||||
regex: '^[ ]*///?[^\n]*\.\n'
|
||||
regex: '^(?!\/\/\ Copyright\ \(c\)\ \d{4}\ Spotify AB\.|\/\/\ under\ the\ License\.)[ ]*///?[^\n]*\.\n'
|
||||
message: "There shouldn't be trailing dot in comments"
|
||||
severity: warning
|
||||
|
||||
+19
-10
@@ -3,16 +3,16 @@
|
||||
"pins": [
|
||||
{
|
||||
"package": "AEXML",
|
||||
"repositoryURL": "https://github.com/tadija/AEXML",
|
||||
"repositoryURL": "https://github.com/tadija/AEXML.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "8623e73b193386909566a9ca20203e33a09af142",
|
||||
"version": "4.5.0"
|
||||
"revision": "38f7d00b23ecd891e1ee656fa6aeebd6ba04ecc3",
|
||||
"version": "4.6.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "PathKit",
|
||||
"repositoryURL": "https://github.com/kylef/PathKit",
|
||||
"repositoryURL": "https://github.com/kylef/PathKit.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "73f8e9dca9b7a3078cb79128217dc8f2e585a511",
|
||||
@@ -37,13 +37,22 @@
|
||||
"version": "0.0.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "SwiftDocCPlugin",
|
||||
"repositoryURL": "https://github.com/apple/swift-docc-plugin",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "3303b164430d9a7055ba484c8ead67a52f7b74f6",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "XcodeProj",
|
||||
"repositoryURL": "https://github.com/tuist/XcodeProj.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "0b18c3e7a10c241323397a80cb445051f4494971",
|
||||
"version": "8.0.0"
|
||||
"revision": "fae27b48bc14ff3fd9b02902e48c4665ce5a0793",
|
||||
"version": "8.9.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -51,8 +60,8 @@
|
||||
"repositoryURL": "https://github.com/jpsim/Yams.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "53741ba55ecca5c7149d8c9f810913ec80845c69",
|
||||
"version": "3.0.0"
|
||||
"revision": "00c403debcd0a007b854bb35e598466207a2d58c",
|
||||
"version": "5.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -60,8 +69,8 @@
|
||||
"repositoryURL": "https://github.com/marmelroy/Zip.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "80b1c3005ee25b4c7ce46c4029ac3347e8d5e37e",
|
||||
"version": "2.0.0"
|
||||
"revision": "67fa55813b9e7b3b9acee9c0ae501def28746d76",
|
||||
"version": "2.1.2"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
+31
-4
@@ -1,4 +1,5 @@
|
||||
// swift-tools-version:5.3
|
||||
// swiftlint:disable:previous file_header
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package
|
||||
|
||||
import PackageDescription
|
||||
@@ -12,10 +13,11 @@ let package = Package(
|
||||
.executable(name: "xcprebuild", targets: ["xcprebuild"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/marmelroy/Zip.git", from: "2.0.0"),
|
||||
.package(url: "https://github.com/jpsim/Yams.git", from: "3.0.0"),
|
||||
.package(url: "https://github.com/marmelroy/Zip.git", from: "2.1.2"),
|
||||
.package(url: "https://github.com/jpsim/Yams.git", from: "5.0.0"),
|
||||
.package(url: "https://github.com/apple/swift-argument-parser", from: "0.0.1"),
|
||||
.package(url: "https://github.com/tuist/XcodeProj.git", from: "8.0.0"),
|
||||
.package(url: "https://github.com/tuist/XcodeProj.git", from: "8.9.0"),
|
||||
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
@@ -30,8 +32,16 @@ let package = Package(
|
||||
name: "xcswiftc",
|
||||
dependencies: ["XCRemoteCache"]
|
||||
),
|
||||
.target(
|
||||
name: "xclibtoolSupport",
|
||||
dependencies: ["XCRemoteCache"]
|
||||
),
|
||||
.target(
|
||||
name: "xclibtool",
|
||||
dependencies: ["XCRemoteCache", "xclibtoolSupport"]
|
||||
),
|
||||
.target(
|
||||
name: "xclipo",
|
||||
dependencies: ["XCRemoteCache"]
|
||||
),
|
||||
.target(
|
||||
@@ -49,15 +59,32 @@ let package = Package(
|
||||
name: "xcld",
|
||||
dependencies: ["XCRemoteCache"]
|
||||
),
|
||||
.target(
|
||||
name: "xcldplusplus",
|
||||
dependencies: ["XCRemoteCache"]
|
||||
),
|
||||
.target(
|
||||
// Wrapper target that builds all binaries but does nothing in runtime
|
||||
name: "Aggregator",
|
||||
dependencies: ["xcprebuild", "xcswiftc", "xclibtool", "xcpostbuild", "xcprepare", "xcld"]
|
||||
dependencies: [
|
||||
"xcprebuild",
|
||||
"xcswiftc",
|
||||
"xclibtool",
|
||||
"xcpostbuild",
|
||||
"xcprepare",
|
||||
"xcld",
|
||||
"xcldplusplus",
|
||||
"xclipo",
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "XCRemoteCacheTests",
|
||||
dependencies: ["XCRemoteCache"],
|
||||
resources: [.copy("TestData")]
|
||||
),
|
||||
.testTarget(
|
||||
name: "xclibtoolSupportTests",
|
||||
dependencies: ["xclibtoolSupport"]
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -8,6 +8,7 @@ _XCRemoteCache is a remote cache tool for Xcode projects. It reuses target artif
|
||||
[](https://github.com/spotify/XCRemoteCache/workflows/CI/badge.svg)
|
||||
[](LICENSE)
|
||||
[](https://slackin.spotify.com)
|
||||
[](https://spotify.github.io/XCRemoteCache/documentation/xcremotecache/)
|
||||
|
||||
- [How and Why?](#how-and-why)
|
||||
* [Accurate target input files](#accurate-target-input-files)
|
||||
@@ -194,8 +195,11 @@ Configure Xcode targets that **should use** XCRemoteCache:
|
||||
* `CC` - `xccc_file` from your `.rcinfo` configuration (e.g. `xcremotecache/xccc`)
|
||||
* `SWIFT_EXEC` - location of `xcprepare` (e.g. `xcremotecache/xcswiftc`)
|
||||
* `LIBTOOL` - location of `xclibtool` (e.g. `xcremotecache/xclibtool`)
|
||||
* `LIPO` - location of `xclipo` (e.g. `xcremotecache/xclipo`)
|
||||
* `LD` - location of `xcld` (e.g. `xcremotecache/xcld`)
|
||||
* `LDPLUSPLUS` - location of `xcldplusplus` (e.g. `xcremotecache/xcldplusplus`)
|
||||
* `XCRC_PLATFORM_PREFERRED_ARCH` - `$(LINK_FILE_LIST_$(CURRENT_VARIANT)_$(PLATFORM_PREFERRED_ARCH):dir:standardizepath:file:default=arm64)`
|
||||
* `SWIFT_USE_INTEGRATED_DRIVER` - `NO` (required in Xcode 14.0+)
|
||||
|
||||
<details>
|
||||
<summary>Screenshot</summary>
|
||||
@@ -264,7 +268,42 @@ $ xcremotecache/xcprepare mark --configuration Debug --platform iphonesimulator
|
||||
|
||||
That command creates an empty file on a remote server which informs that for given sha, configuration, platform, Xcode versions etc. all artifacts are available.
|
||||
|
||||
_Note that for the `producer` mode, the prebuild build phase and `xccc`, `xcld`, `xclibtool` wrappers become no-op, so it is recommended to not add them for the `producer` mode._
|
||||
_Note that for the `producer` mode, the prebuild build phase and `xccc`, `xcld`, `xcldplusplus`, `xclibtool`, `xclipo` wrappers become no-op, so it is recommended to not add them for the `producer` mode._
|
||||
|
||||
##### 7. Generalize `-Swift.h` (Optional only if using static library with a bridging header with public `NS_ENUM` exposed from ObjC)
|
||||
|
||||
If a static library target contains a mixed target with a bridging header exposing an enum from ObjC in a public Swift API, your custom script that moves `*-Swift.h` to the shared location, it should also move `*-Swift.h.md5` next to it.
|
||||
|
||||
Example:
|
||||
|
||||
##### Existing script (Before):
|
||||
|
||||
```shell
|
||||
ditto "${SCRIPT_INPUT_FILE_0}" "${SCRIPT_OUTPUT_FILE_0}"
|
||||
```
|
||||
|
||||
where
|
||||
* `SCRIPT_INPUT_FILE_0="$(DERIVED_SOURCES_DIR)/$(SWIFT_OBJC_INTERFACE_HEADER_NAME)"`
|
||||
* `SCRIPT_OUTPUT_FILE_0="$(BUILT_PRODUCTS_DIR)/include/$(PRODUCT_MODULE_NAME)/$(SWIFT_OBJC_INTERFACE_HEADER_NAME)"`
|
||||
|
||||
##### Correct script (After):
|
||||
|
||||
```shell
|
||||
ditto "${SCRIPT_INPUT_FILE_0}" "${SCRIPT_OUTPUT_FILE_0}"
|
||||
[ -f "${SCRIPT_INPUT_FILE_1}" ] && ditto "${SCRIPT_INPUT_FILE_1}" "${SCRIPT_OUTPUT_FILE_1}" || rm "${SCRIPT_OUTPUT_FILE_1}"
|
||||
```
|
||||
|
||||
where
|
||||
* `SCRIPT_INPUT_FILE_0="$(DERIVED_SOURCES_DIR)/$(SWIFT_OBJC_INTERFACE_HEADER_NAME)"`
|
||||
* `SCRIPT_INPUT_FILE_1="$(DERIVED_SOURCES_DIR)/$(SWIFT_OBJC_INTERFACE_HEADER_NAME).md5"`
|
||||
* `SCRIPT_OUTPUT_FILE_0="$(BUILT_PRODUCTS_DIR)/include/$(PRODUCT_MODULE_NAME)/$(SWIFT_OBJC_INTERFACE_HEADER_NAME)"`
|
||||
* `SCRIPT_OUTPUT_FILE_1="$(BUILT_PRODUCTS_DIR)/include/$(PRODUCT_MODULE_NAME)/$(SWIFT_OBJC_INTERFACE_HEADER_NAME).md5"`
|
||||
|
||||
Note: This step is not required if at least one of these is true:
|
||||
|
||||
* you build a framework (not a static library)
|
||||
* you don't expose `NS_ENUM` type from ObjC to Swift via a bridging header
|
||||
|
||||
|
||||
## A full list of configuration parameters:
|
||||
|
||||
@@ -288,13 +327,15 @@ _Note that for the `producer` mode, the prebuild build phase and `xccc`, `xcld`,
|
||||
| `cache_commit_history` | Number of historical git commits to look for cache artifacts | `10` | ⬜️ |
|
||||
| `source_root` | Source root of the Xcode project | `""` | ⬜️ |
|
||||
| `fingerprint_override_extension` | Fingerprint override extension (sample override `Module.swiftmodule/x86_64.swiftmodule.md5`) | `md5` | ⬜️ |
|
||||
| `extra_configuration_file` | Configuration file that overrides project configuration | `user.rcinfo` | ⬜️ |
|
||||
| `extra_configuration_file` | Configuration file that overrides project configuration (this property can be overriden multiple times in different files to chain extra configuration files) | `user.rcinfo` | ⬜️ |
|
||||
| `publishing_sha` | Custom commit sha to publish artifact (producer only) | `nil` | ⬜️ |
|
||||
| `artifact_maximum_age` | Maximum age in days HTTP response should be locally cached before being evicted | `30` | ⬜️ |
|
||||
| `custom_fingerprint_envs` | Extra ENV keys that should be convoluted into the environment fingerprint | `[]` | ⬜️ |
|
||||
| `stats_dir` | Directory where all XCRemoteCache statistics (e.g. counters) are stored | `~/.xccache` | ⬜️ |
|
||||
| `download_retries` | Number of retries for download requests | `0` | ⬜️ |
|
||||
| `upload_retries` | Number of retries for upload requests | `3` | ⬜️ |
|
||||
| `retry_delay` | Delay between retries in seconds | `10` | ⬜️ |
|
||||
| `upload_batch_size` | Maximum number of simultaneous requests. 0 means no limits | `0` | ⬜️ |
|
||||
| `request_custom_headers` | Dictionary of extra HTTP headers for all remote server requests | `[]` | ⬜️ |
|
||||
| `thin_target_mock_filename` | Filename (without an extension) of the compilation input file that is used as a fake compilation for the forced-cached target (aka thin target) | `standin` | ⬜️ |
|
||||
| `focused_targets` | A list of all targets that are not thinned. If empty, all targets are meant to be non-thin | `[]` | ⬜️ |
|
||||
@@ -308,12 +349,14 @@ _Note that for the `producer` mode, the prebuild build phase and `xccc`, `xcld`,
|
||||
| `prettify_meta_files` | A Boolean value that opts-in pretty JSON formatting for meta files | `false` | ⬜️ |
|
||||
| `aws_secret_key` | Secret key for AWS V4 Signature Authorization. If this is set to a non-empty String - an Authentication Header will be added based on this and the other `aws_*` parameters.| `""` | ⬜️ |
|
||||
| `aws_access_key` | Access key for AWS V4 Signature Authorization. | `""` | ⬜️ |
|
||||
| `aws_security_token` | Temporary security token provided by the AWS Security Token Service. | `nil` | ⬜️ |
|
||||
| `aws_region` | Region for AWS V4 Signature Authorization. E.g. `eu`. | `""` | ⬜️ |
|
||||
| `aws_service` | Service for AWS V4 Signature Authorization. E.g. `storage`. | `""` | ⬜️ |
|
||||
| `out_of_band_mappings` | A dictionary of files path remapping that should be applied to make it absolute path agnostic on a list of dependencies. Useful if a project refers files out of repo root, either compilation files or precompiled dependencies. Keys represent generic replacement and values are substrings that should be replaced. Example: for mapping `["COOL_LIBRARY": "/CoolLibrary"]` `/CoolLibrary/main.swift`will be represented as `$(COOL_LIBRARY)/main.swift`). Warning: remapping order is not-deterministic so avoid remappings with multiple matchings. | `[:]` | ⬜️ |
|
||||
| `disable_certificate_verification` | A Boolean value that opts-in SSL certificate validation is disabled | `false` | ⬜️ |
|
||||
| `disable_vfs_overlay` | A feature flag to disable virtual file system overlay support (temporary) | `false` | ⬜️ |
|
||||
| `custom_rewrite_envs` | A list of extra ENVs that should be used as placeholders in the dependency list. ENV rewrite process is optimistic - does nothing if an ENV is not defined in the pre/postbuild process. | `[]` | ⬜️ |
|
||||
| `irrelevant_dependencies_paths` | Regexes of files that should not be included in a list of dependencies. Warning! Add entries here with caution - excluding dependencies that are relevant might lead to a target overcaching. The regex can match either partially or fully the filepath, e.g. `\\.modulemap$` will exclude all `.modulemap` files. | `[]` | ⬜️ |
|
||||
|
||||
## Backend cache server
|
||||
|
||||
@@ -351,6 +394,8 @@ XCRemoteCache supports Amazon S3 and Google Cloud Storage buckets to be used as
|
||||
|
||||
To set it up use the configuration parameters `aws_secret_key`, `aws_access_key`, `aws_region`, and `aws_service` in the `.rcinfo` file. Specify the URL to the bucket in cache-addresses field in the same file.
|
||||
|
||||
XCRemoteCache also supports [AWS Temporary Access Keys](https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html#temporary-access-keys). Use additional `aws_security_token` parameter combined with `aws_secret_key`, `aws_access_key` to set it up. [This page](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html) describes how to receive a security token.
|
||||
|
||||
Example
|
||||
```yaml
|
||||
...
|
||||
@@ -404,12 +449,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.
|
||||
* The new Swift driver (introduced by default in Xcode 14.0) is not supported and has to be disabled when using XCRemoteCache
|
||||
|
||||
## FAQ
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ DERIVED_DATA_DIR = File.join('.build').freeze
|
||||
RELEASES_ROOT_DIR = File.join('releases').freeze
|
||||
|
||||
EXECUTABLE_NAME = 'XCRemoteCache'
|
||||
EXECUTABLE_NAMES = ['xclibtool', 'xcpostbuild', 'xcprebuild', 'xcprepare', 'xcswiftc', 'xcld']
|
||||
EXECUTABLE_NAMES = ['xclibtool', 'xcpostbuild', 'xcprebuild', 'xcprepare', 'xcswiftc', 'xcld', 'xcldplusplus', 'xclipo']
|
||||
PROJECT_NAME = 'XCRemoteCache'
|
||||
|
||||
SWIFTLINT_ENABLED = true
|
||||
@@ -72,6 +72,22 @@ task :build, [:configuration, :arch, :sdks, :is_archive] do |task, args|
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Build release artifacts'
|
||||
task :prepare_release do
|
||||
system("rm -rf releases && rm -rf tmp")
|
||||
Rake::Task['build'].invoke("release", "x86_64-apple-macosx")
|
||||
system("mkdir -p tmp && unzip releases/XCRemoteCache.zip -d tmp/xcremotecache-x86_64")
|
||||
system("rm -rf releases")
|
||||
Rake::Task['build'].invoke("release", "arm64-apple-macosx")
|
||||
system("rake 'build[release, arm64-apple-macosx]'")
|
||||
system("mkdir -p tmp && unzip releases/XCRemoteCache.zip -d tmp/xcremotecache-arm64")
|
||||
system("rm -rf releases")
|
||||
system("mkdir -p releases && zip -jr releases/XCRemoteCache-macOS-x86_64.zip LICENSE README.md tmp/xcremotecache-x86_64")
|
||||
system("zip -jr releases/XCRemoteCache-macOS-arm64.zip LICENSE README.md tmp/xcremotecache-arm64")
|
||||
system("mkdir -p tmp/xcremotecache && ls tmp/xcremotecache-x86_64 | xargs -I {} lipo -create -output tmp/xcremotecache/{} tmp/xcremotecache-x86_64/{} tmp/xcremotecache-arm64/{}")
|
||||
system("zip -jr releases/XCRemoteCache-macOS-arm64-x86_64.zip LICENSE README.md tmp/xcremotecache")
|
||||
end
|
||||
|
||||
desc 'run tests with SPM'
|
||||
task :test do
|
||||
# Running tests
|
||||
|
||||
@@ -42,6 +42,7 @@ class BuildArtifactCreator: ArtifactSwiftProductsBuilderImpl, ArtifactCreator {
|
||||
private let modulesFolderPath: String
|
||||
private let dSYMPath: URL
|
||||
private let metaWriter: MetaWriter
|
||||
private let artifactProcessor: ArtifactProcessor
|
||||
private let fileManager: FileManager
|
||||
|
||||
init(
|
||||
@@ -52,6 +53,7 @@ class BuildArtifactCreator: ArtifactSwiftProductsBuilderImpl, ArtifactCreator {
|
||||
modulesFolderPath: String,
|
||||
dSYMPath: URL,
|
||||
metaWriter: MetaWriter,
|
||||
artifactProcessor: ArtifactProcessor,
|
||||
fileManager: FileManager
|
||||
) {
|
||||
self.buildDir = buildDir
|
||||
@@ -62,6 +64,7 @@ class BuildArtifactCreator: ArtifactSwiftProductsBuilderImpl, ArtifactCreator {
|
||||
self.fileManager = fileManager
|
||||
self.dSYMPath = dSYMPath
|
||||
self.metaWriter = metaWriter
|
||||
self.artifactProcessor = artifactProcessor
|
||||
super.init(workingDir: tempDir, moduleName: moduleName, fileManager: fileManager)
|
||||
}
|
||||
|
||||
@@ -87,6 +90,7 @@ class BuildArtifactCreator: ArtifactSwiftProductsBuilderImpl, ArtifactCreator {
|
||||
/// - Parameter tempDir: Temp location to organize file hierarchy in the artifact
|
||||
/// - returns: URLs to include into the artifact package
|
||||
fileprivate func prepareSwiftArtifacts(tempDir: URL) throws -> [URL] {
|
||||
try artifactProcessor.process(localArtifact: tempDir)
|
||||
var artifacts: [URL] = []
|
||||
|
||||
// Add optional directory with generated ObjC headers
|
||||
|
||||
@@ -31,7 +31,7 @@ enum ArtifactOrganizerLocationPreparationResult: Equatable {
|
||||
case preparedForArtifact(artifact: URL)
|
||||
}
|
||||
|
||||
/// Prepares .zip artifact for the local operations
|
||||
/// Prepares existing .zip artifact for the local operations
|
||||
protocol ArtifactOrganizer {
|
||||
/// Prepares the location for the artifact unzipping
|
||||
/// - Parameter fileKey: artifact fileKey that corresponds to the zip filename on the remote cache server
|
||||
@@ -48,10 +48,13 @@ protocol ArtifactOrganizer {
|
||||
|
||||
class ZipArtifactOrganizer: ArtifactOrganizer {
|
||||
private let cacheDir: URL
|
||||
// all processors that should "prepare" the unzipped raw artifact
|
||||
private let artifactProcessors: [ArtifactProcessor]
|
||||
private let fileManager: FileManager
|
||||
|
||||
init(targetTempDir: URL, fileManager: FileManager) {
|
||||
init(targetTempDir: URL, artifactProcessors: [ArtifactProcessor], fileManager: FileManager) {
|
||||
cacheDir = targetTempDir.appendingPathComponent("xccache")
|
||||
self.artifactProcessors = artifactProcessors
|
||||
self.fileManager = fileManager
|
||||
}
|
||||
|
||||
@@ -93,6 +96,10 @@ class ZipArtifactOrganizer: ArtifactOrganizer {
|
||||
// when the command was interrupted (internal crash or `kill -9` signal)
|
||||
let tempDestination = destinationURL.appendingPathExtension("tmp")
|
||||
try Zip.unzipFile(artifact, destination: tempDestination, overwrite: true, password: nil)
|
||||
|
||||
try artifactProcessors.forEach { processor in
|
||||
try processor.process(rawArtifact: tempDestination)
|
||||
}
|
||||
try fileManager.moveItem(at: tempDestination, to: destinationURL)
|
||||
return destinationURL
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright (c) 2022 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
/// Performs a pre/postprocessing on an artifact package
|
||||
/// Could be a place for file reorganization (to support legacy package formats) and/or
|
||||
/// remapp absolute paths in some package files
|
||||
protocol ArtifactProcessor {
|
||||
/// Processes a raw artifact in a directory. Raw artifact is a format of an artifact
|
||||
/// that is stored in a remote cache server (generic)
|
||||
/// - Parameter rawArtifact: directory that contains raw artifact content
|
||||
func process(rawArtifact: URL) throws
|
||||
|
||||
/// Processes a local artifact in a directory
|
||||
/// - Parameter localArtifact: directory that contains local (machine-specific) artifact content
|
||||
func process(localArtifact: URL) throws
|
||||
}
|
||||
|
||||
/// Processes downloaded artifact by replacing generic paths in generated ObjC headers placed in ./include
|
||||
class UnzippedArtifactProcessor: ArtifactProcessor {
|
||||
/// All directories in an artifact that should be processed by path remapping
|
||||
private static let remappingDirs = ["include"]
|
||||
private let fileRemapper: FileDependenciesRemapper
|
||||
private let dirScanner: DirScanner
|
||||
|
||||
init(fileRemapper: FileDependenciesRemapper, dirScanner: DirScanner) {
|
||||
self.fileRemapper = fileRemapper
|
||||
self.dirScanner = dirScanner
|
||||
}
|
||||
|
||||
private func findProcessingEligableFiles(path: String) throws -> [URL] {
|
||||
let remappingURL = URL(fileURLWithPath: path)
|
||||
let allFiles = try dirScanner.recursiveItems(at: remappingURL)
|
||||
return allFiles.filter({ !$0.isHidden })
|
||||
}
|
||||
|
||||
/// Replaces all generic paths in a raw artifact's `include` dir with
|
||||
/// absolute paths, specific for a given machine and configuration
|
||||
/// - Parameter rawArtifact: raw artifact location
|
||||
func process(rawArtifact url: URL) throws {
|
||||
for remappingDir in Self.remappingDirs {
|
||||
let remappingPath = url.appendingPathComponent(remappingDir).path
|
||||
let allFiles = try findProcessingEligableFiles(path: remappingPath)
|
||||
try allFiles.forEach(fileRemapper.remap(fromGeneric:))
|
||||
}
|
||||
}
|
||||
|
||||
func process(localArtifact url: URL) throws {
|
||||
for remappingDir in Self.remappingDirs {
|
||||
let remappingPath = url.appendingPathComponent(remappingDir).path
|
||||
let allFiles = try findProcessingEligableFiles(path: remappingPath)
|
||||
try allFiles.forEach(fileRemapper.remap(fromLocal:))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension URL {
|
||||
// Recognize hidden files starting with a dot
|
||||
var isHidden: Bool {
|
||||
lastPathComponent.hasPrefix(".")
|
||||
}
|
||||
}
|
||||
@@ -88,7 +88,7 @@ class ArtifactSwiftProductsBuilderImpl: ArtifactSwiftProductsBuilder {
|
||||
throw ArtifactSwiftProductsBuilderError.populatingNonExistingObjCHeader
|
||||
}
|
||||
try fileManager.createDirectory(at: moduleObjCURL, withIntermediateDirectories: true, attributes: nil)
|
||||
try fileManager.spt_forceLinkItem(at: headerURL, to: headerArtifactURL)
|
||||
try fileManager.spt_forceCopyItem(at: headerURL, to: headerArtifactURL)
|
||||
}
|
||||
|
||||
func includeModuleDefinitionsToTheArtifact(arch: String, moduleURL: URL) throws {
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright (c) 2022 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
enum FileDependenciesRemapperError: Error {
|
||||
/// Thrown when the file to remap is invalid (e.g. doesn't exist or has unexpected format)
|
||||
case invalidRemappingFile(URL)
|
||||
}
|
||||
|
||||
/// Replaces paths in a file content between generic (placeholder-based)
|
||||
/// and local formats
|
||||
protocol FileDependenciesRemapper {
|
||||
/// Replaces all generic paths (with placeholders) to a local, machine
|
||||
/// specific absolute paths
|
||||
/// - Parameter url: location of a file that should be remapped in-place
|
||||
func remap(fromGeneric url: URL) throws
|
||||
/// Replaces all local, machine specific absolute paths to
|
||||
/// generic ones
|
||||
/// - Parameter url: location of a file that should be remapped in-place
|
||||
func remap(fromLocal url: URL) throws
|
||||
}
|
||||
|
||||
/// Remaps absolute paths in a text files stored on a disk
|
||||
/// Note: That class can be used only for text-based files, not binaries
|
||||
class TextFileDependenciesRemapper: FileDependenciesRemapper {
|
||||
private static let linesSeparator = "\n"
|
||||
private let remapper: DependenciesRemapper
|
||||
private let fileAccessor: FileAccessor
|
||||
|
||||
init(remapper: DependenciesRemapper, fileAccessor: FileAccessor) {
|
||||
self.remapper = remapper
|
||||
self.fileAccessor = fileAccessor
|
||||
}
|
||||
|
||||
private func readFileLines(_ url: URL) throws -> [String] {
|
||||
guard let content = try fileAccessor.contents(atPath: url.path) else {
|
||||
// the file is empty
|
||||
return []
|
||||
}
|
||||
guard let contentString = String(data: content, encoding: .utf8) else {
|
||||
throw FileDependenciesRemapperError.invalidRemappingFile(url)
|
||||
}
|
||||
return contentString.components(separatedBy: .newlines)
|
||||
}
|
||||
|
||||
private func storeFileLines(lines: [String], url: URL) throws {
|
||||
let contentString = lines.joined(separator: "\n")
|
||||
let contentData = contentString.data(using: String.Encoding.utf8)
|
||||
try fileAccessor.write(toPath: url.path, contents: contentData)
|
||||
}
|
||||
|
||||
func remap(fromGeneric url: URL) throws {
|
||||
let contentLines = try readFileLines(url)
|
||||
let remappedContent = try remapper.replace(genericPaths: contentLines)
|
||||
try storeFileLines(lines: remappedContent, url: url)
|
||||
}
|
||||
|
||||
func remap(fromLocal url: URL) throws {
|
||||
let contentLines = try readFileLines(url)
|
||||
let remappedContent = try remapper.replace(localPaths: contentLines)
|
||||
try storeFileLines(lines: remappedContent, url: url)
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,8 @@ enum SwiftmoduleFileExtension: String {
|
||||
case swiftdoc
|
||||
case swiftsourceinfo
|
||||
case swiftinterface
|
||||
case privateSwiftinterface = "private.swiftinterface"
|
||||
case abiJson = "abi.json"
|
||||
}
|
||||
|
||||
extension SwiftmoduleFileExtension {
|
||||
@@ -40,5 +42,7 @@ extension SwiftmoduleFileExtension {
|
||||
.swiftdoc: .required,
|
||||
.swiftsourceinfo: .optional,
|
||||
.swiftinterface: .optional,
|
||||
.privateSwiftinterface: .optional,
|
||||
.abiJson: .optional,
|
||||
]
|
||||
}
|
||||
|
||||
+22
-12
@@ -19,31 +19,42 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
enum XCLibtoolCreateUniversalBinaryError: Error {
|
||||
enum XCCreateUniversalBinaryError: Error {
|
||||
/// Missing ar libraries that should constitute an universal build
|
||||
case missingInputLibrary
|
||||
}
|
||||
|
||||
/// Wrapper for `libtool` call for creating an universal binary
|
||||
class XCLibtoolCreateUniversalBinary: XCLibtoolLogic {
|
||||
/// Wrapper for `libtool`/`lipo` call for creating an universal binary
|
||||
class XCCreateUniversalBinary: XCLibtoolLogic {
|
||||
private let output: URL
|
||||
private let tempDir: URL
|
||||
private let firstInputURL: URL
|
||||
private let toolName: String
|
||||
private let fallbackCommand: String
|
||||
|
||||
init(output: String, inputs: [String]) throws {
|
||||
init(
|
||||
output: String,
|
||||
inputs: [String],
|
||||
toolName: String,
|
||||
fallbackCommand: String
|
||||
) throws {
|
||||
self.output = URL(fileURLWithPath: output)
|
||||
guard let firstInput = inputs.first else {
|
||||
throw XCLibtoolCreateUniversalBinaryError.missingInputLibrary
|
||||
throw XCCreateUniversalBinaryError.missingInputLibrary
|
||||
}
|
||||
let firstInputURL = URL(fileURLWithPath: firstInput)
|
||||
// inputs are place in $TARGET_TEMP_DIR/Objects-normal/$ARCH/Binary/$TARGET_NAME.a
|
||||
// TODO: find better (stable) technique to determine `$TARGET_TEMP_DIR`
|
||||
errorLog("\(firstInputURL.absoluteString)")
|
||||
|
||||
tempDir = firstInputURL
|
||||
.deletingLastPathComponent()
|
||||
.deletingLastPathComponent()
|
||||
.deletingLastPathComponent()
|
||||
.deletingLastPathComponent()
|
||||
self.firstInputURL = firstInputURL
|
||||
self.toolName = toolName
|
||||
self.fallbackCommand = fallbackCommand
|
||||
}
|
||||
|
||||
func run() {
|
||||
@@ -52,10 +63,10 @@ class XCLibtoolCreateUniversalBinary: XCLibtoolLogic {
|
||||
let config: XCRemoteCacheConfig
|
||||
do {
|
||||
let srcRoot: URL = URL(fileURLWithPath: fileManager.currentDirectoryPath)
|
||||
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileManager: fileManager)
|
||||
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileReader: fileManager)
|
||||
.readConfiguration()
|
||||
} catch {
|
||||
errorLog("Libtool initialization failed with error: \(error). Fallbacking to libtool")
|
||||
errorLog("\(toolName) initialization failed with error: \(error). Fallbacking to \(fallbackCommand)")
|
||||
fallbackToDefault()
|
||||
}
|
||||
|
||||
@@ -74,22 +85,21 @@ class XCLibtoolCreateUniversalBinary: XCLibtoolLogic {
|
||||
// that these are already an universal binary
|
||||
try fileManager.spt_forceLinkItem(at: firstInputURL, to: output)
|
||||
} catch {
|
||||
errorLog("Libtool failed with error: \(error). Fallbacking to libtool")
|
||||
errorLog("\(toolName) failed with error: \(error). Fallbacking to \(fallbackCommand)")
|
||||
do {
|
||||
try fileManager.removeItem(at: markerURL)
|
||||
fallbackToDefault()
|
||||
} catch {
|
||||
exit(1, "FATAL: libtool failed with error: \(error)")
|
||||
exit(1, "FATAL: \(fallbackCommand) failed with error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func fallbackToDefault() -> Never {
|
||||
let args = ProcessInfo().arguments
|
||||
let command = "libtool"
|
||||
let paramList = [command] + args.dropFirst()
|
||||
let paramList = [fallbackCommand] + args.dropFirst()
|
||||
let cargs = paramList.map { strdup($0) } + [nil]
|
||||
execvp(command, cargs)
|
||||
execvp(fallbackCommand, cargs)
|
||||
|
||||
/// C-function `execv` returns only when the command fails
|
||||
exit(1)
|
||||
@@ -20,7 +20,7 @@
|
||||
import Foundation
|
||||
|
||||
/// Represents a mode that libtool was called
|
||||
public enum XCLibtoolMode {
|
||||
public enum XCLibtoolMode: Equatable {
|
||||
/// Creating a static library (ar format) from a set of .o input files
|
||||
case createLibrary(output: String, filelist: String, dependencyInfo: String)
|
||||
/// Creating a universal library (multiple-architectures) from a set of input .a static libraries
|
||||
@@ -44,7 +44,12 @@ public class XCLibtool {
|
||||
stepDescription: "Libtool"
|
||||
)
|
||||
case .createUniversalBinary(let output, let inputs):
|
||||
logic = try XCLibtoolCreateUniversalBinary(output: output, inputs: inputs)
|
||||
logic = try XCCreateUniversalBinary(
|
||||
output: output,
|
||||
inputs: inputs,
|
||||
toolName: "Libtool",
|
||||
fallbackCommand: "libtool"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2023 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
|
||||
|
||||
/// Wrapper for a `lipo` tool that creates a fat archive
|
||||
public class XCLipo {
|
||||
private let logic: XCLibtoolLogic
|
||||
|
||||
public init(
|
||||
output: String,
|
||||
inputs: [String],
|
||||
fallbackCommand: String,
|
||||
stepDescription: String
|
||||
) throws {
|
||||
errorLog("\(output)")
|
||||
errorLog("\(inputs.joined(separator: ","))")
|
||||
logic = try XCCreateUniversalBinary(
|
||||
output: output,
|
||||
inputs: inputs,
|
||||
toolName: stepDescription,
|
||||
fallbackCommand: fallbackCommand
|
||||
)
|
||||
}
|
||||
|
||||
/// Handles a `-create` action which is responsible to create a fat archive
|
||||
/// If remote cache can reuse artifacts from a remote cache, it just links any of input
|
||||
/// files to the destination (output) location because the binary in XCRC artifact already
|
||||
/// contains a fat library
|
||||
/// If a remote artifact cannot be reused, a fallback to the `lipo` command is performed
|
||||
public func run() {
|
||||
logic.run()
|
||||
}
|
||||
}
|
||||
+8
-2
@@ -27,13 +27,19 @@ protocol ThinningConsumerArtifactsOrganizerFactory {
|
||||
}
|
||||
|
||||
class ThinningConsumerZipArtifactsOrganizerFactory: ThinningConsumerArtifactsOrganizerFactory {
|
||||
private let processors: [ArtifactProcessor]
|
||||
private let fileManager: FileManager
|
||||
|
||||
init(fileManager: FileManager) {
|
||||
init(processors: [ArtifactProcessor], fileManager: FileManager) {
|
||||
self.processors = processors
|
||||
self.fileManager = fileManager
|
||||
}
|
||||
|
||||
func build(targetTempDir: URL) -> ArtifactOrganizer {
|
||||
ZipArtifactOrganizer(targetTempDir: targetTempDir, fileManager: fileManager)
|
||||
ZipArtifactOrganizer(
|
||||
targetTempDir: targetTempDir,
|
||||
artifactProcessors: processors,
|
||||
fileManager: fileManager
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
+5
-2
@@ -58,7 +58,7 @@ class ThinningDiskSwiftcProductsGenerator: SwiftcProductsGenerator {
|
||||
func generateFrom(
|
||||
artifactSwiftModuleFiles sourceAtifactSwiftModuleFiles: [SwiftmoduleFileExtension: URL],
|
||||
artifactSwiftModuleObjCFile: URL
|
||||
) throws -> URL {
|
||||
) throws -> SwiftcProductsGeneratorOutput {
|
||||
// Move cached -Swift.h file to the expected location
|
||||
try diskCopier.copy(file: artifactSwiftModuleObjCFile, destination: objcHeaderOutput)
|
||||
for (ext, url) in sourceAtifactSwiftModuleFiles {
|
||||
@@ -79,6 +79,9 @@ class ThinningDiskSwiftcProductsGenerator: SwiftcProductsGenerator {
|
||||
}
|
||||
|
||||
// Build parent dir of the .swiftmodule file that contains a module
|
||||
return modulePathOutput.deletingLastPathComponent()
|
||||
return SwiftcProductsGeneratorOutput(
|
||||
swiftmoduleDir: modulePathOutput.deletingLastPathComponent(),
|
||||
objcHeaderFile: objcHeaderOutput
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 \
|
||||
|
||||
+3
-2
@@ -73,11 +73,12 @@ class UnzippedArtifactSwiftProductsOrganizer: SwiftProductsOrganizer {
|
||||
.appendingPathComponent(moduleName)
|
||||
.appendingPathComponent("\(moduleName)-Swift.h")
|
||||
|
||||
let generatedModuleDir = try productsGenerator.generateFrom(
|
||||
let generatedModule = try productsGenerator.generateFrom(
|
||||
artifactSwiftModuleFiles: artifactSwiftmoduleFiles,
|
||||
artifactSwiftModuleObjCFile: artifactSwiftModuleObjCFile
|
||||
)
|
||||
|
||||
try fingerprintSyncer.decorate(sourceDir: generatedModuleDir, fingerprint: fingerprint)
|
||||
try fingerprintSyncer.decorate(sourceDir: generatedModule.swiftmoduleDir, fingerprint: fingerprint)
|
||||
try fingerprintSyncer.decorate(file: generatedModule.objcHeaderFile, fingerprint: fingerprint)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,13 +238,32 @@ class Postbuild {
|
||||
let moduleSwiftProductURL = context.productsDir
|
||||
.appendingPathComponent(context.modulesFolderPath)
|
||||
.appendingPathComponent("\(modulename).swiftmodule")
|
||||
let objcHeaderSwiftProductURL = context.derivedSourcesDir
|
||||
.appendingPathComponent("\(modulename)-Swift.h")
|
||||
// This header is obly valid if building a frameworks
|
||||
let objcHeaderSwiftPublicPathURL = context.publicHeadersFolderPath?
|
||||
.appendingPathComponent("\(modulename)-Swift.h")
|
||||
if let fingerprint = contextSpecificFingerprint {
|
||||
try fingerprintSyncer.decorate(
|
||||
sourceDir: moduleSwiftProductURL,
|
||||
fingerprint: fingerprint
|
||||
)
|
||||
try fingerprintSyncer.decorate(
|
||||
file: objcHeaderSwiftProductURL,
|
||||
fingerprint: fingerprint
|
||||
)
|
||||
if let objcPublic = objcHeaderSwiftPublicPathURL {
|
||||
try fingerprintSyncer.decorate(
|
||||
file: objcPublic,
|
||||
fingerprint: fingerprint
|
||||
)
|
||||
}
|
||||
} else {
|
||||
try fingerprintSyncer.delete(sourceDir: moduleSwiftProductURL)
|
||||
try fingerprintSyncer.delete(sourceDir: objcHeaderSwiftProductURL)
|
||||
if let objcPublic = objcHeaderSwiftPublicPathURL {
|
||||
try fingerprintSyncer.delete(file: objcPublic)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ public struct PostbuildContext {
|
||||
var mode: Mode
|
||||
var targetName: String
|
||||
var targetTempDir: URL
|
||||
var derivedFilesDir: URL
|
||||
/// Location where all compilation outputs (.o) are placed
|
||||
var compilationTempDir: URL
|
||||
var configuration: String
|
||||
@@ -73,22 +74,29 @@ public struct PostbuildContext {
|
||||
let builtProductsDir: URL
|
||||
/// Location to the product bundle. Can be nil for libraries
|
||||
let bundleDir: URL?
|
||||
let derivedSourcesDir: URL
|
||||
var 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)
|
||||
/// location of the json file that define virtual files system overlay
|
||||
/// (mappings of the virtual location file -> local file path)
|
||||
let overlayHeadersPath: URL
|
||||
/// Regexes of files that should not be included in the dependency list
|
||||
let irrelevantDependenciesPaths: [String]
|
||||
/// Location of public headers. Not always available (e.g. static libraries)
|
||||
var publicHeadersFolderPath: 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")
|
||||
targetName = targetNameValue
|
||||
targetTempDir = try env.readEnv(key: "TARGET_TEMP_DIR")
|
||||
derivedFilesDir = try env.readEnv(key: "DERIVED_FILE_DIR")
|
||||
let archs: [String] = try env.readEnv(key: "ARCHS").split(separator: " ").map(String.init)
|
||||
guard let firstArch = archs.first, !firstArch.isEmpty else {
|
||||
throw PostbuildContextError.missingArchitecture
|
||||
@@ -131,5 +139,12 @@ extension PostbuildContext {
|
||||
modeMarkerPath = config.modeMarkerPath
|
||||
/// Note: The file has yaml extension, even it is in the json format
|
||||
overlayHeadersPath = targetTempDir.appendingPathComponent("all-product-headers.yaml")
|
||||
irrelevantDependenciesPaths = config.irrelevantDependenciesPaths
|
||||
let publicHeadersPathEnv: String? = env.readEnv(key: "PUBLIC_HEADERS_FOLDER_PATH")
|
||||
if let publicHeadersPath = publicHeadersPathEnv, publicHeadersPathEnv != "/usr/local/include" {
|
||||
// '/usr/local/include' is a value of PUBLIC_HEADERS_FOLDER_PATH when no public headers are automatically
|
||||
// generated and it is up to a project configuration to place it in a common location (e.g. static library)
|
||||
publicHeadersFolderPath = builtProductsDir.appendingPathComponent(publicHeadersPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
// swiftlint:disable type_body_length
|
||||
/// Checks current mode from a configuration and based on that:
|
||||
/// * triggers build completion
|
||||
/// * triggers uploading artifacts to the server for a 'producer' mode
|
||||
@@ -33,7 +34,7 @@ public class XCPostbuild {
|
||||
let context: PostbuildContext
|
||||
let cacheHitLogger: CacheHitLogger
|
||||
do {
|
||||
config = try XCRemoteCacheConfigReader(env: env, fileManager: fileManager).readConfiguration()
|
||||
config = try XCRemoteCacheConfigReader(env: env, fileReader: fileManager).readConfiguration()
|
||||
context = try PostbuildContext(config, env: env)
|
||||
updateProcessTag(context.targetName)
|
||||
let counterFactory: FileStatsCoordinator.CountersFactory = { file, count in
|
||||
@@ -86,8 +87,14 @@ public class XCPostbuild {
|
||||
fingerprintFilesGenerator,
|
||||
algorithm: MD5Algorithm()
|
||||
)
|
||||
let organizer = ZipArtifactOrganizer(targetTempDir: context.targetTempDir, fileManager: fileManager)
|
||||
let organizer = ZipArtifactOrganizer(
|
||||
targetTempDir: context.targetTempDir,
|
||||
// In postbuild we don't preprocess artifacts (no need to replace path placeholders)
|
||||
artifactProcessors: [],
|
||||
fileManager: fileManager
|
||||
)
|
||||
let metaWriter = JsonMetaWriter(fileWriter: fileManager, pretty: config.prettifyMetaFiles)
|
||||
let fileRemapper = TextFileDependenciesRemapper(remapper: envsRemapper, fileAccessor: fileManager)
|
||||
let artifactCreator = BuildArtifactCreator(
|
||||
buildDir: context.productsDir,
|
||||
tempDir: context.targetTempDir,
|
||||
@@ -96,6 +103,7 @@ public class XCPostbuild {
|
||||
modulesFolderPath: context.modulesFolderPath,
|
||||
dSYMPath: context.dSYMPath,
|
||||
metaWriter: metaWriter,
|
||||
artifactProcessor: UnzippedArtifactProcessor(fileRemapper: fileRemapper, dirScanner: fileManager),
|
||||
fileManager: fileManager
|
||||
)
|
||||
let dirAccessor = DirAccessorComposer(
|
||||
@@ -113,6 +121,7 @@ public class XCPostbuild {
|
||||
awsV4Signature = AWSV4Signature(
|
||||
secretKey: config.AWSSecretKey,
|
||||
accessKey: config.AWSAccessKey,
|
||||
securityToken: config.AWSSecurityToken,
|
||||
region: config.AWSRegion,
|
||||
service: config.AWSService,
|
||||
date: Date(timeIntervalSinceNow: 0)
|
||||
@@ -121,6 +130,7 @@ public class XCPostbuild {
|
||||
let networkClient = NetworkClientImpl(
|
||||
session: sessionFactory.build(),
|
||||
retries: config.uploadRetries,
|
||||
retryDelay: config.retryDelay,
|
||||
fileManager: fileManager,
|
||||
awsV4Signature: awsV4Signature
|
||||
)
|
||||
@@ -128,6 +138,7 @@ public class XCPostbuild {
|
||||
mode: context.mode,
|
||||
downloadStreamURL: context.recommendedCacheAddress,
|
||||
upstreamStreamURL: context.cacheAddresses,
|
||||
uploadBatchSize: config.uploadBatchSize,
|
||||
networkClient: networkClient,
|
||||
urlBuilderFactory: {
|
||||
try URLBuilderImpl(
|
||||
@@ -150,7 +161,8 @@ public class XCPostbuild {
|
||||
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.
|
||||
// captured from the swiftc invocation similarly is stored in the `history.compile`
|
||||
// for the consumer mode
|
||||
let overlayReader = JsonOverlayReader(
|
||||
context.overlayHeadersPath,
|
||||
mode: .bestEffort,
|
||||
@@ -168,7 +180,9 @@ public class XCPostbuild {
|
||||
product: context.productsDir,
|
||||
source: context.srcRoot,
|
||||
intermediate: context.targetTempDir,
|
||||
bundle: context.bundleDir
|
||||
derivedFiles: context.derivedFilesDir,
|
||||
bundle: context.bundleDir,
|
||||
skippedRegexes: context.irrelevantDependenciesPaths
|
||||
)
|
||||
// Override fingerprints for all produced '.swiftmodule' files
|
||||
let fingerprintOverrideManager = FingerprintOverrideManagerImpl(
|
||||
@@ -195,7 +209,10 @@ public class XCPostbuild {
|
||||
if context.moduleName == config.thinningTargetModuleName {
|
||||
switch context.mode {
|
||||
case .consumer:
|
||||
// no need to process artifacts in postbuild. Prebuild has already
|
||||
// run a processor on a downloaded artifact
|
||||
let artifactOrganizerFactory = ThinningConsumerZipArtifactsOrganizerFactory(
|
||||
processors: [],
|
||||
fileManager: fileManager
|
||||
)
|
||||
let swiftProductsLocationProvider =
|
||||
@@ -319,3 +336,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 = try 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,7 +43,8 @@ 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)
|
||||
/// location of the json file that define virtual files system overlay
|
||||
/// (mappings of the virtual location file -> local file path)
|
||||
let overlayHeadersPath: URL
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ public class XCPrebuild {
|
||||
let config: XCRemoteCacheConfig
|
||||
let context: PrebuildContext
|
||||
do {
|
||||
config = try XCRemoteCacheConfigReader(env: env, fileManager: fileManager).readConfiguration()
|
||||
config = try XCRemoteCacheConfigReader(env: env, fileReader: fileManager).readConfiguration()
|
||||
context = try PrebuildContext(config, env: env)
|
||||
updateProcessTag(context.targetName)
|
||||
} catch {
|
||||
@@ -78,6 +78,10 @@ public class XCPrebuild {
|
||||
exit(0)
|
||||
}
|
||||
|
||||
let compilationHistoryOrganizer = CompilationHistoryFileOrganizer(
|
||||
context.compilationHistoryFile,
|
||||
fileManager: fileManager
|
||||
)
|
||||
do {
|
||||
let envFingerprint = try EnvironmentFingerprintGenerator(
|
||||
configuration: config,
|
||||
@@ -96,6 +100,7 @@ public class XCPrebuild {
|
||||
awsV4Signature = AWSV4Signature(
|
||||
secretKey: config.AWSSecretKey,
|
||||
accessKey: config.AWSAccessKey,
|
||||
securityToken: config.AWSSecurityToken,
|
||||
region: config.AWSRegion,
|
||||
service: config.AWSService,
|
||||
date: Date(timeIntervalSinceNow: 0)
|
||||
@@ -104,6 +109,7 @@ public class XCPrebuild {
|
||||
let networkClient = NetworkClientImpl(
|
||||
session: sessionFactory.build(),
|
||||
retries: config.downloadRetries,
|
||||
retryDelay: config.retryDelay,
|
||||
fileManager: fileManager,
|
||||
awsV4Signature: awsV4Signature
|
||||
)
|
||||
@@ -124,7 +130,7 @@ public class XCPrebuild {
|
||||
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.
|
||||
// do not fail in case of a missing headers overlay file
|
||||
let overlayReader = JsonOverlayReader(
|
||||
context.overlayHeadersPath,
|
||||
mode: .bestEffort,
|
||||
@@ -146,9 +152,14 @@ public class XCPrebuild {
|
||||
filesFingerprintGenerator,
|
||||
algorithm: MD5Algorithm()
|
||||
)
|
||||
let organizer = ZipArtifactOrganizer(targetTempDir: context.targetTempDir, fileManager: fileManager)
|
||||
let compilationHistoryOrganizer = CompilationHistoryFileOrganizer(
|
||||
context.compilationHistoryFile,
|
||||
let fileRemapper = TextFileDependenciesRemapper(
|
||||
remapper: envsRemapper,
|
||||
fileAccessor: fileManager
|
||||
)
|
||||
let artifactProcessor = UnzippedArtifactProcessor(fileRemapper: fileRemapper, dirScanner: fileManager)
|
||||
let organizer = ZipArtifactOrganizer(
|
||||
targetTempDir: context.targetTempDir,
|
||||
artifactProcessors: [artifactProcessor],
|
||||
fileManager: fileManager
|
||||
)
|
||||
let metaReader = JsonMetaReader(fileAccessor: fileManager)
|
||||
@@ -156,7 +167,10 @@ public class XCPrebuild {
|
||||
|
||||
if config.thinningEnabled {
|
||||
if context.moduleName == config.thinningTargetModuleName, let thinnedTarget = context.thinnedTargets {
|
||||
let organizerFactory = ThinningConsumerZipArtifactsOrganizerFactory(fileManager: .default)
|
||||
let organizerFactory = ThinningConsumerZipArtifactsOrganizerFactory(
|
||||
processors: [artifactProcessor],
|
||||
fileManager: fileManager
|
||||
)
|
||||
let aggregationPlugin = ThinningConsumerPrebuildPlugin(
|
||||
targetName: context.targetName,
|
||||
tempDir: context.targetTempDir,
|
||||
@@ -188,7 +202,6 @@ public class XCPrebuild {
|
||||
case .compatible(localDependencies: let dependencies):
|
||||
// TODO: pass `allowedInputFiles` observed in the build time
|
||||
try modeController.enable(allowedInputFiles: dependencies, dependencies: dependencies)
|
||||
compilationHistoryOrganizer.reset()
|
||||
}
|
||||
} catch {
|
||||
disableRemoteCache(
|
||||
@@ -196,6 +209,7 @@ public class XCPrebuild {
|
||||
errorMessage: "Prebuild step failed with error: \(error)"
|
||||
)
|
||||
}
|
||||
compilationHistoryOrganizer.reset()
|
||||
}
|
||||
|
||||
private func disableRemoteCache(modeController: PhaseCacheModeController, errorMessage: String?) {
|
||||
|
||||
@@ -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)")
|
||||
}
|
||||
@@ -385,6 +395,9 @@ class TemplateBasedCCWrapperBuilder: CCWrapperBuilder {
|
||||
const char *dependency_arg_name = "-MF";
|
||||
const char *output_arg_name = "-o";
|
||||
const char *serialize_diagnostics_arg_name = "--serialize-diagnostics";
|
||||
const char *language_mode_arg_name = "-x";
|
||||
const char *precompile_objc_header_arg_value = "objective-c-header";
|
||||
const char *precompile_c_header_arg_value = "c-header";
|
||||
const char *clang_cmd = "\(clangCommand)";
|
||||
const char *markerFile = "\(markerFilename)";
|
||||
const char *compilationHistoryFile = "\(compilationHistoryFilename)";
|
||||
@@ -399,6 +412,7 @@ class TemplateBasedCCWrapperBuilder: CCWrapperBuilder {
|
||||
const char *output_file= NULL;
|
||||
const char *input_file = NULL;
|
||||
const char *diagnostics_file = NULL;
|
||||
const char *language_mode = NULL;
|
||||
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], dependency_arg_name) == 0 && i < (argc - 1) ) {
|
||||
@@ -419,6 +433,12 @@ class TemplateBasedCCWrapperBuilder: CCWrapperBuilder {
|
||||
i += 1;
|
||||
clang_args[i] = argv[i];
|
||||
diagnostics_file = argv[i];
|
||||
} if (strcmp(argv[i], language_mode_arg_name) == 0 && i < (argc - 1) ) {
|
||||
// called with "-x path" pattern and not the last argument
|
||||
clang_args[i] = argv[i];
|
||||
i += 1;
|
||||
clang_args[i] = argv[i];
|
||||
language_mode = argv[i];
|
||||
} else if (
|
||||
isSuffixed(argv[i],".m") ||
|
||||
isSuffixed(argv[i],".mm") ||
|
||||
@@ -426,10 +446,13 @@ class TemplateBasedCCWrapperBuilder: CCWrapperBuilder {
|
||||
isSuffixed(argv[i],".cc") ||
|
||||
isSuffixed(argv[i],".cpp") ||
|
||||
isSuffixed(argv[i],".c++") ||
|
||||
isSuffixed(argv[i],".cxx")
|
||||
isSuffixed(argv[i],".cxx") ||
|
||||
isSuffixed(argv[i],".S") ||
|
||||
isSuffixed(argv[i],".s")
|
||||
) {
|
||||
// a full list of extensions is taken from https://clang.llvm.org/docs/ClangFormatStyleOptions.html
|
||||
// support for .m,.mm,.c,.cc,.cpp,.c++,.cxx input files
|
||||
// .s and .S are assembly files
|
||||
clang_args[i] = argv[i];
|
||||
input_file = argv[i];
|
||||
} else {
|
||||
@@ -438,6 +461,14 @@ class TemplateBasedCCWrapperBuilder: CCWrapperBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
// null-terminating the args array needed for local compilation fallback
|
||||
clang_args[argc] = NULL;
|
||||
|
||||
// Verify mode. Even a target is cached, pch mode is not supported. Fallback to the local compilation
|
||||
if (language_mode != NULL && (strcmp(language_mode, precompile_objc_header_arg_value) == 0 || strcmp(language_mode, precompile_c_header_arg_value) == 0)) {
|
||||
return execvp(clang_cmd, (char *const*) clang_args);
|
||||
}
|
||||
|
||||
// Verify all input arguments
|
||||
if (dependency_file == NULL) {
|
||||
fprintf(stderr, "error: missing %s input\\n", dependency_arg_name);
|
||||
@@ -508,8 +539,6 @@ class TemplateBasedCCWrapperBuilder: CCWrapperBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
// null-terminating the args array
|
||||
clang_args[argc] = NULL;
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wincompatible-pointer-types-discards-qualifiers"
|
||||
/// execvp takes $PATH to consideration
|
||||
|
||||
+11
-3
@@ -33,20 +33,25 @@ protocol BuildSettingsIntegrateAppender {
|
||||
class XcodeProjBuildSettingsIntegrateAppender: BuildSettingsIntegrateAppender {
|
||||
private let mode: Mode
|
||||
private let repoRoot: URL
|
||||
private let fakeSrcRoot: URL
|
||||
|
||||
init(mode: Mode, repoRoot: URL) {
|
||||
init(mode: Mode, repoRoot: URL, fakeSrcRoot: URL) {
|
||||
self.mode = mode
|
||||
self.repoRoot = repoRoot
|
||||
self.fakeSrcRoot = fakeSrcRoot
|
||||
}
|
||||
|
||||
func appendToBuildSettings(buildSettings: BuildSettings, wrappers: XCRCBinariesPaths) -> BuildSettings {
|
||||
var result = buildSettings
|
||||
result["SWIFT_EXEC"] = wrappers.swiftc.path
|
||||
result["SWIFT_USE_INTEGRATED_DRIVER"] = "NO"
|
||||
// When generating artifacts, no need to shell-out all compilation commands to our wrappers
|
||||
if case .consumer = mode {
|
||||
result["CC"] = wrappers.cc.path
|
||||
result["LD"] = wrappers.ld.path
|
||||
result["LIBTOOL"] = wrappers.libtool.path
|
||||
result["LIPO"] = wrappers.lipo.path
|
||||
result["LDPLUSPLUS"] = wrappers.ldplusplus.path
|
||||
}
|
||||
|
||||
let existingSwiftFlags = result["OTHER_SWIFT_FLAGS"] as? String
|
||||
@@ -61,8 +66,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
|
||||
}
|
||||
|
||||
@@ -52,7 +52,9 @@ extension IntegrateContext {
|
||||
cc: binariesDir.appendingPathComponent("xccc"),
|
||||
swiftc: binariesDir.appendingPathComponent("xcswiftc"),
|
||||
libtool: binariesDir.appendingPathComponent("xclibtool"),
|
||||
lipo: binariesDir.appendingPathComponent("xclipo"),
|
||||
ld: binariesDir.appendingPathComponent("xcld"),
|
||||
ldplusplus: binariesDir.appendingPathComponent("xcldplusplus"),
|
||||
prebuild: binariesDir.appendingPathComponent("xcprebuild"),
|
||||
postbuild: binariesDir.appendingPathComponent("xcpostbuild")
|
||||
)
|
||||
|
||||
@@ -74,7 +74,7 @@ public class XCIntegrate {
|
||||
let binariesDir = commandURL.deletingLastPathComponent()
|
||||
|
||||
let srcRoot: URL = URL(fileURLWithPath: projectPath).deletingLastPathComponent()
|
||||
let config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileManager: fileManager)
|
||||
let config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileReader: fileManager)
|
||||
.readConfiguration()
|
||||
|
||||
let context = try IntegrateContext(
|
||||
@@ -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 {
|
||||
|
||||
@@ -25,7 +25,9 @@ struct XCRCBinariesPaths {
|
||||
let cc: URL
|
||||
let swiftc: URL
|
||||
let libtool: URL
|
||||
let lipo: URL
|
||||
let ld: URL
|
||||
let ldplusplus: URL
|
||||
let prebuild: URL
|
||||
let postbuild: URL
|
||||
}
|
||||
|
||||
@@ -230,10 +230,12 @@ struct XcodeProjIntegrate: Integrate {
|
||||
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)
|
||||
// add postbuild right after compilation as custom build steps may depend on files generated by
|
||||
// the xcpostbuild (e.g. to copy -Swift.h.md5)
|
||||
pbxproj.add(object: postbuildPhase)
|
||||
target.buildPhases.insert(postbuildPhase, at: sourceIndex + 1)
|
||||
pbxproj.add(object: prebuildPhase)
|
||||
target.buildPhases.insert(prebuildPhase, at: sourceIndex)
|
||||
pbxproj.add(object: postbuildPhase)
|
||||
target.buildPhases.append(postbuildPhase)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ public class XCConfig {
|
||||
let fileManager = FileManager.default
|
||||
let config: XCRemoteCacheConfig
|
||||
do {
|
||||
config = try XCRemoteCacheConfigReader(env: env, fileManager: fileManager).readConfiguration()
|
||||
config = try XCRemoteCacheConfigReader(env: env, fileReader: fileManager).readConfiguration()
|
||||
} catch {
|
||||
exit(1, "FATAL: Prepare initialization failed with error: \(error)")
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ public class XCPrepare {
|
||||
var context: PrepareContext
|
||||
let xcodeVersion: String
|
||||
do {
|
||||
config = try XCRemoteCacheConfigReader(env: env, fileManager: fileManager).readConfiguration()
|
||||
config = try XCRemoteCacheConfigReader(env: env, fileReader: fileManager).readConfiguration()
|
||||
context = try PrepareContext(config, offline: offline)
|
||||
xcodeVersion = try customXcodeBuildNumber ?? XcodeProbeImpl(shell: shellGetStdout).read().buildVersion
|
||||
} catch {
|
||||
@@ -78,6 +78,7 @@ public class XCPrepare {
|
||||
awsV4Signature = AWSV4Signature(
|
||||
secretKey: config.AWSSecretKey,
|
||||
accessKey: config.AWSAccessKey,
|
||||
securityToken: config.AWSSecurityToken,
|
||||
region: config.AWSRegion,
|
||||
service: config.AWSService,
|
||||
date: Date(timeIntervalSinceNow: 0)
|
||||
@@ -86,6 +87,7 @@ public class XCPrepare {
|
||||
let networkClient = NetworkClientImpl(
|
||||
session: sessionFactory.build(),
|
||||
retries: config.downloadRetries,
|
||||
retryDelay: config.retryDelay,
|
||||
fileManager: fileManager,
|
||||
awsV4Signature: awsV4Signature
|
||||
)
|
||||
|
||||
@@ -46,7 +46,7 @@ public class XCPrepareMark {
|
||||
let context: PrepareMarkContext
|
||||
let xcodeVersion: String
|
||||
do {
|
||||
config = try XCRemoteCacheConfigReader(env: env, fileManager: fileManager).readConfiguration()
|
||||
config = try XCRemoteCacheConfigReader(env: env, fileReader: fileManager).readConfiguration()
|
||||
context = try PrepareMarkContext(config)
|
||||
xcodeVersion = try xcode ?? XcodeProbeImpl(shell: shellGetStdout).read().buildVersion
|
||||
} catch {
|
||||
@@ -60,6 +60,7 @@ public class XCPrepareMark {
|
||||
awsV4Signature = AWSV4Signature(
|
||||
secretKey: config.AWSSecretKey,
|
||||
accessKey: config.AWSAccessKey,
|
||||
securityToken: config.AWSSecurityToken,
|
||||
region: config.AWSRegion,
|
||||
service: config.AWSService,
|
||||
date: Date(timeIntervalSinceNow: 0)
|
||||
@@ -68,6 +69,7 @@ public class XCPrepareMark {
|
||||
let networkClient = NetworkClientImpl(
|
||||
session: sessionFactory.build(),
|
||||
retries: config.uploadRetries,
|
||||
retryDelay: config.retryDelay,
|
||||
fileManager: fileManager,
|
||||
awsV4Signature: awsV4Signature
|
||||
)
|
||||
@@ -75,6 +77,7 @@ public class XCPrepareMark {
|
||||
mode: .producer,
|
||||
downloadStreamURL: context.recommendedCacheAddress,
|
||||
upstreamStreamURL: context.cacheAddresses,
|
||||
uploadBatchSize: config.uploadBatchSize,
|
||||
networkClient: networkClient
|
||||
) { [configuration, platform] cacheAddress in
|
||||
// Prepare URLs don't include target name or envFingperint, which are valid only for a target level
|
||||
|
||||
@@ -36,7 +36,7 @@ public class XCStats {
|
||||
let config: XCRemoteCacheConfig
|
||||
let context: XCStatsContext
|
||||
do {
|
||||
config = try XCRemoteCacheConfigReader(env: env, fileManager: fileManager).readConfiguration()
|
||||
config = try XCRemoteCacheConfigReader(env: env, fileReader: fileManager).readConfiguration()
|
||||
try context = XCStatsContext(config, fileManager: fileManager)
|
||||
} catch {
|
||||
exit(1, "FATAL: Prepare initialization failed with error: \(error)")
|
||||
|
||||
@@ -70,7 +70,7 @@ public class XCCreateBinary {
|
||||
let config: XCRemoteCacheConfig
|
||||
do {
|
||||
let srcRoot: URL = URL(fileURLWithPath: fileManager.currentDirectoryPath)
|
||||
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileManager: fileManager)
|
||||
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileReader: fileManager)
|
||||
.readConfiguration()
|
||||
} catch {
|
||||
errorLog("\(stepDescription) initialization failed with error: \(error). Fallbacking to \(fallbackCommand)")
|
||||
@@ -78,7 +78,12 @@ public class XCCreateBinary {
|
||||
}
|
||||
let markerURL = tempDir.appendingPathComponent(config.modeMarkerPath)
|
||||
do {
|
||||
let organizer = ZipArtifactOrganizer(targetTempDir: tempDir, fileManager: fileManager)
|
||||
let organizer = ZipArtifactOrganizer(
|
||||
targetTempDir: tempDir,
|
||||
// Creation binary doesn't call artifact preprocessing
|
||||
artifactProcessors: [],
|
||||
fileManager: fileManager
|
||||
)
|
||||
let dependenciesWriter = FileDatWriter(dependencyInfo, fileManager: fileManager)
|
||||
let markerReader = FileMarkerReader(markerURL, fileManager: fileManager)
|
||||
guard fileManager.fileExists(atPath: markerURL.path) else {
|
||||
|
||||
@@ -55,7 +55,7 @@ class MirroredLinkingSwiftcProductsGenerator: SwiftcProductsGenerator {
|
||||
func generateFrom(
|
||||
artifactSwiftModuleFiles: [SwiftmoduleFileExtension: URL],
|
||||
artifactSwiftModuleObjCFile: URL
|
||||
) throws -> URL {
|
||||
) throws -> SwiftcProductsGeneratorOutput {
|
||||
/// Predict moduleName from the `*.swiftmodule` artifact
|
||||
let foundSwiftmoduleFile = artifactSwiftModuleFiles[.swiftmodule]
|
||||
guard let mainSwiftmoduleFile = foundSwiftmoduleFile else {
|
||||
|
||||
@@ -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,20 +20,25 @@
|
||||
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
|
||||
}
|
||||
|
||||
struct SwiftcProductsGeneratorOutput {
|
||||
let swiftmoduleDir: URL
|
||||
let objcHeaderFile: URL
|
||||
}
|
||||
|
||||
/// Generates swiftc product to the expected location
|
||||
protocol SwiftcProductsGenerator {
|
||||
/// Generates products from given files
|
||||
/// - Returns: location dir where .swiftmodule files have been placed
|
||||
/// - Returns: location dir where .swiftmodule and ObjC header files have been placed
|
||||
func generateFrom(
|
||||
artifactSwiftModuleFiles: [SwiftmoduleFileExtension: URL],
|
||||
artifactSwiftModuleObjCFile: URL
|
||||
) throws -> URL
|
||||
) throws -> SwiftcProductsGeneratorOutput
|
||||
}
|
||||
|
||||
/// Generator that produces all products in the locations where Xcode expects it, using provided disk copier
|
||||
@@ -64,7 +69,7 @@ class DiskSwiftcProductsGenerator: SwiftcProductsGenerator {
|
||||
func generateFrom(
|
||||
artifactSwiftModuleFiles sourceAtifactSwiftModuleFiles: [SwiftmoduleFileExtension: URL],
|
||||
artifactSwiftModuleObjCFile: URL
|
||||
) throws -> URL {
|
||||
) throws -> SwiftcProductsGeneratorOutput {
|
||||
// Move cached -Swift.h file to the expected location
|
||||
try diskCopier.copy(file: artifactSwiftModuleObjCFile, destination: objcHeaderOutput)
|
||||
for (ext, url) in sourceAtifactSwiftModuleFiles {
|
||||
@@ -85,6 +90,9 @@ class DiskSwiftcProductsGenerator: SwiftcProductsGenerator {
|
||||
}
|
||||
|
||||
// Build parent dir of the .swiftmodule file that contains a module
|
||||
return modulePathOutput.deletingLastPathComponent()
|
||||
return SwiftcProductsGeneratorOutput(
|
||||
swiftmoduleDir: modulePathOutput.deletingLastPathComponent(),
|
||||
objcHeaderFile: objcHeaderOutput
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ public class XCSwiftc {
|
||||
let context: SwiftcContext
|
||||
do {
|
||||
let srcRoot: URL = URL(fileURLWithPath: fileManager.currentDirectoryPath)
|
||||
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileManager: fileManager)
|
||||
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileReader: fileManager)
|
||||
.readConfiguration()
|
||||
context = try SwiftcContext(config: config, input: inputArgs)
|
||||
} catch {
|
||||
@@ -83,7 +83,12 @@ public class XCSwiftc {
|
||||
|
||||
let inputReader = SwiftcFilemapInputEditor(context.filemap, fileManager: fileManager)
|
||||
let fileListEditor = FileListEditor(context.fileList, fileManager: fileManager)
|
||||
let artifactOrganizer = ZipArtifactOrganizer(targetTempDir: context.tempDir, fileManager: fileManager)
|
||||
let artifactOrganizer = ZipArtifactOrganizer(
|
||||
targetTempDir: context.tempDir,
|
||||
// xcswiftc doesn't call artifact preprocessing
|
||||
artifactProcessors: [],
|
||||
fileManager: fileManager
|
||||
)
|
||||
// TODO: check for allowedFile comparing a list of all inputfiles, not dependencies from a marker
|
||||
let makerReferencedFilesListScanner = FileListScannerImpl(markerReader, caseSensitive: false)
|
||||
let allowedFilesListScanner = ExceptionsFilteredFileListScanner(
|
||||
|
||||
@@ -83,6 +83,10 @@ public struct XCRemoteCacheConfig: Encodable {
|
||||
var downloadRetries: Int = 0
|
||||
/// Number of retries for upload requests
|
||||
var uploadRetries: Int = 3
|
||||
/// Delay between retries in seconds
|
||||
var retryDelay: Double = 10.0
|
||||
/// Maximum number of simultaneous requests. 0 means no limits
|
||||
var uploadBatchSize: Int = 0
|
||||
/// Extra headers appended to all remote HTTP(S) requests
|
||||
var requestCustomHeaders: [String: String] = [:]
|
||||
/// Filename (without an extension) of the compilation input file that is used
|
||||
@@ -95,10 +99,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
|
||||
@@ -107,7 +111,8 @@ public struct XCRemoteCacheConfig: Encodable {
|
||||
var turnOffRemoteCacheOnFirstTimeout: Bool = false
|
||||
/// List of all extensions that should carry over source fingerprints. Extensions of all product files that
|
||||
/// contain non-deterministic content (absolute paths, timestamp, etc) should be included
|
||||
var productFilesExtensionsWithContentOverride = ["swiftmodule"]
|
||||
/// .h files may contain absolute paths if NS_ENUM is used in a public API from Swift code
|
||||
var productFilesExtensionsWithContentOverride = ["swiftmodule", "h"]
|
||||
/// If true, plugins for thinning support should be enabled
|
||||
var thinningEnabled: Bool = false
|
||||
/// Module name of a target that works as a helper for thinned targets
|
||||
@@ -118,24 +123,31 @@ public struct XCRemoteCacheConfig: Encodable {
|
||||
var AWSSecretKey: String = ""
|
||||
/// Access key for AWS V4 Signature
|
||||
var AWSAccessKey: String = ""
|
||||
/// Temporary security token provided by the AWS Security Token Service
|
||||
var AWSSecurityToken: String?
|
||||
/// Region for AWS V4 Signature (e.g. `eu`)
|
||||
var AWSRegion: String = ""
|
||||
/// Service for AWS V4 Signature (e.g. `storage`)
|
||||
var AWSService: String = ""
|
||||
/// A dictionary of files path remapping that should be applied to make it absolute path agnostic on a list of dependencies.
|
||||
/// Useful if a project refers files out of repo root, either compilation files or precompiled dependencies.
|
||||
/// Keys represent generic replacement and values are substrings that should be replaced.
|
||||
/// 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.
|
||||
/// A list of extra ENVs that should be used as placeholders in the dependency list
|
||||
/// ENV rewrite process is optimistic - does nothing if an ENV is not defined in the pre/postbuild process
|
||||
var customRewriteEnvs: [String] = []
|
||||
/// Regexes of files that should not be included in a list of dependencies. Warning! Add entries here
|
||||
/// with caution - excluding dependencies that are relevant might lead to a target overcaching
|
||||
/// Note: The regex can match either partially or fully the filepath, e.g. `\\.modulemap$` will exclude
|
||||
/// all `.modulemap` files
|
||||
var irrelevantDependenciesPaths: [String] = []
|
||||
}
|
||||
|
||||
extension XCRemoteCacheConfig {
|
||||
@@ -168,6 +180,8 @@ extension XCRemoteCacheConfig {
|
||||
merge.statsDir = scheme.statsDir ?? statsDir
|
||||
merge.downloadRetries = scheme.downloadRetries ?? downloadRetries
|
||||
merge.uploadRetries = scheme.uploadRetries ?? uploadRetries
|
||||
merge.retryDelay = scheme.retryDelay ?? retryDelay
|
||||
merge.uploadBatchSize = scheme.uploadBatchSize ?? uploadBatchSize
|
||||
merge.requestCustomHeaders = scheme.requestCustomHeaders ?? requestCustomHeaders
|
||||
merge.thinTargetMockFilename = scheme.thinTargetMockFilename ?? thinTargetMockFilename
|
||||
merge.focusedTargets = scheme.focusedTargets ?? focusedTargets
|
||||
@@ -184,12 +198,14 @@ extension XCRemoteCacheConfig {
|
||||
merge.prettifyMetaFiles = scheme.prettifyMetaFiles ?? prettifyMetaFiles
|
||||
merge.AWSAccessKey = scheme.AWSAccessKey ?? AWSAccessKey
|
||||
merge.AWSSecretKey = scheme.AWSSecretKey ?? AWSSecretKey
|
||||
merge.AWSSecurityToken = scheme.AWSSecurityToken ?? AWSSecurityToken
|
||||
merge.AWSRegion = scheme.AWSRegion ?? AWSRegion
|
||||
merge.AWSService = scheme.AWSService ?? AWSService
|
||||
merge.outOfBandMappings = scheme.outOfBandMappings ?? outOfBandMappings
|
||||
merge.disableCertificateVerification = scheme.disableCertificateVerification ?? disableCertificateVerification
|
||||
merge.disableVFSOverlay = scheme.disableVFSOverlay ?? disableVFSOverlay
|
||||
merge.customRewriteEnvs = scheme.customRewriteEnvs ?? customRewriteEnvs
|
||||
merge.irrelevantDependenciesPaths = scheme.irrelevantDependenciesPaths ?? irrelevantDependenciesPaths
|
||||
return merge
|
||||
}
|
||||
|
||||
@@ -234,6 +250,8 @@ struct ConfigFileScheme: Decodable {
|
||||
let statsDir: String?
|
||||
let downloadRetries: Int?
|
||||
let uploadRetries: Int?
|
||||
let retryDelay: Double?
|
||||
let uploadBatchSize: Int?
|
||||
let requestCustomHeaders: [String: String]?
|
||||
let thinTargetMockFilename: String?
|
||||
let focusedTargets: [String]?
|
||||
@@ -247,12 +265,14 @@ struct ConfigFileScheme: Decodable {
|
||||
let prettifyMetaFiles: Bool?
|
||||
let AWSSecretKey: String?
|
||||
let AWSAccessKey: String?
|
||||
let AWSSecurityToken: String?
|
||||
let AWSRegion: String?
|
||||
let AWSService: String?
|
||||
let outOfBandMappings: [String: String]?
|
||||
let disableCertificateVerification: Bool?
|
||||
let disableVFSOverlay: Bool?
|
||||
let customRewriteEnvs: [String]?
|
||||
let irrelevantDependenciesPaths: [String]?
|
||||
|
||||
// Yams library doesn't support encoding strategy, see https://github.com/jpsim/Yams/issues/84
|
||||
enum CodingKeys: String, CodingKey {
|
||||
@@ -280,6 +300,8 @@ struct ConfigFileScheme: Decodable {
|
||||
case statsDir = "stats_dir"
|
||||
case downloadRetries = "download_retries"
|
||||
case uploadRetries = "upload_retries"
|
||||
case retryDelay = "retry_delay"
|
||||
case uploadBatchSize = "upload_batch_size"
|
||||
case requestCustomHeaders = "request_custom_headers"
|
||||
case thinTargetMockFilename = "thin_target_mock_filename"
|
||||
case focusedTargets = "focused_targets"
|
||||
@@ -293,12 +315,14 @@ struct ConfigFileScheme: Decodable {
|
||||
case prettifyMetaFiles = "prettify_meta_files"
|
||||
case AWSSecretKey = "aws_secret_key"
|
||||
case AWSAccessKey = "aws_access_key"
|
||||
case AWSSecurityToken = "aws_security_token"
|
||||
case AWSRegion = "aws_region"
|
||||
case AWSService = "aws_service"
|
||||
case outOfBandMappings = "out_of_band_mappings"
|
||||
case disableCertificateVerification = "disable_certificate_verification"
|
||||
case disableVFSOverlay = "disable_vfs_overlay"
|
||||
case customRewriteEnvs = "custom_rewrite_envs"
|
||||
case irrelevantDependenciesPaths = "irrelevant_dependencies_paths"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,31 +335,42 @@ class XCRemoteCacheConfigReader {
|
||||
/// Name of the configuration file, required in $(SRCROOT) location
|
||||
private static let configurationFile = ".rcinfo"
|
||||
private let srcRoot: String
|
||||
private let fileManager: FileManager
|
||||
private let fileReader: FileReader
|
||||
private lazy var yamlDecorer = YAMLDecoder(encoding: .utf8)
|
||||
|
||||
init(env: [String: String], fileManager: FileManager) throws {
|
||||
init(env: [String: String], fileReader: FileReader) throws {
|
||||
let explicitSrcRoot: String? = env.readEnv(key: "SRCROOT")
|
||||
srcRoot = explicitSrcRoot ?? fileManager.currentDirectoryPath
|
||||
self.fileManager = fileManager
|
||||
srcRoot = explicitSrcRoot ?? FileManager.default.currentDirectoryPath
|
||||
self.fileReader = fileReader
|
||||
}
|
||||
|
||||
init(srcRootPath srcRoot: String, fileManager: FileManager) {
|
||||
init(srcRootPath srcRoot: String, fileReader: FileReader) {
|
||||
self.srcRoot = srcRoot
|
||||
self.fileManager = fileManager
|
||||
self.fileReader = fileReader
|
||||
}
|
||||
|
||||
// Reads the final configuration by loading all extra configs
|
||||
// until reaching a config that doesn't override `extraConfigurationFile`
|
||||
func readConfiguration() throws -> XCRemoteCacheConfig {
|
||||
let rootURL = URL(fileURLWithPath: srcRoot)
|
||||
let configURL = URL(fileURLWithPath: Self.configurationFile, relativeTo: rootURL)
|
||||
let userConfigs = try readUserConfig(configURL)
|
||||
var config = XCRemoteCacheConfig(sourceRoot: srcRoot).merged(with: userConfigs)
|
||||
let extraConfURL = URL(fileURLWithPath: config.extraConfigurationFile, relativeTo: rootURL)
|
||||
do {
|
||||
let extraConfig = try readUserConfig(extraConfURL)
|
||||
config = config.merged(with: extraConfig)
|
||||
} catch {
|
||||
infoLog("Extra config override failed with \(error). Skipping extra configuration")
|
||||
var extraConfURL = URL(fileURLWithPath: config.extraConfigurationFile, relativeTo: rootURL)
|
||||
var visitedFiles = Set([configURL])
|
||||
while !visitedFiles.contains(extraConfURL) {
|
||||
do {
|
||||
let extraConfig = try readUserConfig(extraConfURL)
|
||||
debugLog("Reading extra configuration from \(extraConfURL)")
|
||||
config = config.merged(with: extraConfig)
|
||||
visitedFiles.insert(extraConfURL)
|
||||
// Advance extra configuration
|
||||
extraConfURL = URL(fileURLWithPath: config.extraConfigurationFile, relativeTo: rootURL)
|
||||
} catch {
|
||||
infoLog("Extra config override failed with \(error). Skipping extra configuration")
|
||||
// swiftlint:disable:next unneeded_break_in_switch
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return try config.verifyAndApplyDefaults()
|
||||
@@ -343,7 +378,7 @@ class XCRemoteCacheConfigReader {
|
||||
|
||||
/// Reads user configuration from a file
|
||||
private func readUserConfig(_ file: URL) throws -> ConfigFileScheme {
|
||||
let configurationContent = fileManager.contents(atPath: file.path)
|
||||
let configurationContent = try fileReader.contents(atPath: file.path)
|
||||
guard let configurationData = configurationContent else {
|
||||
throw XCRemoteCacheConfigReaderError.missingConfigurationFile(file)
|
||||
}
|
||||
|
||||
@@ -51,34 +51,22 @@ public class FileDependenciesReader: DependenciesReader {
|
||||
public func findDependencies() throws -> [String] {
|
||||
let yaml = try readRaw()
|
||||
|
||||
struct ParseState {
|
||||
var buffer: String = ""
|
||||
var prevChar: Character?
|
||||
var result: [String] = []
|
||||
func with(buffer: String? = nil, prevChar: Character? = nil, result: [String]? = nil) -> ParseState {
|
||||
var new = self
|
||||
new.buffer = buffer ?? new.buffer
|
||||
new.prevChar = prevChar ?? new.prevChar
|
||||
new.result = result ?? new.result
|
||||
return new
|
||||
}
|
||||
}
|
||||
|
||||
let dependencies = yaml.reduce(Set<String>()) { prev, arg1 -> Set<String> in
|
||||
let (key, value) = arg1
|
||||
switch key {
|
||||
case "dependencies":
|
||||
// 'clang' output formatting
|
||||
return Set(splitDependencyFileList(value))
|
||||
return Set(parseDependencyFileList(value))
|
||||
case let s where s.hasSuffix(".o") || s.hasSuffix(".bc"):
|
||||
// 'swiftc' output formatting
|
||||
// take dependencies from any .o or .bc file.
|
||||
// take dependencies from any .o or .bc file
|
||||
// Note: For WMO, all .{o|bc} files have the same dependencies
|
||||
return Set(splitDependencyFileList(value))
|
||||
return Set(parseDependencyFileList(value))
|
||||
default:
|
||||
return prev
|
||||
}
|
||||
}
|
||||
|
||||
return Array(dependencies)
|
||||
}
|
||||
|
||||
@@ -92,56 +80,92 @@ public class FileDependenciesReader: DependenciesReader {
|
||||
return yaml.mapValues { $0.components(separatedBy: .whitespaces) }
|
||||
}
|
||||
|
||||
private func readRaw() throws -> [String: String] {
|
||||
func readRaw() throws -> [String: String] {
|
||||
let fileData = try getFileData()
|
||||
let fileString = try getFileStringFromData(fileData: fileData)
|
||||
let yaml = try getYaml(fileString: fileString)
|
||||
return yaml
|
||||
}
|
||||
|
||||
func getFileData() throws -> Data {
|
||||
guard let fileData = fileManager.contents(atPath: file.path) else {
|
||||
throw DependenciesReaderError.readingError
|
||||
}
|
||||
return fileData
|
||||
}
|
||||
|
||||
func getFileStringFromData(fileData: Data) throws -> String {
|
||||
guard let fileString = String(data: fileData, encoding: .utf8) else {
|
||||
throw DependenciesReaderError.invalidFile
|
||||
}
|
||||
// .d matches the .yaml format
|
||||
return fileString
|
||||
}
|
||||
|
||||
func getYaml(fileString: String) throws -> [String: String] {
|
||||
guard let yaml = try Yams.load(yaml: fileString) as? [String: String] else {
|
||||
throw DependenciesReaderError.invalidFile
|
||||
}
|
||||
return yaml
|
||||
}
|
||||
|
||||
/// Splits space or new line separated files into a set of files
|
||||
/// Parses the String to get the list of files
|
||||
/// It iterates over the String using its UTF8View since it is more performant (String type operates
|
||||
/// in a higher abstraction level and supports features that have a negative impact in the performance)
|
||||
/// It supports escaping whitespace charaters, prefixed with "\\"
|
||||
/// - Parameter string: string of whitespace charaters separated file paths
|
||||
/// - Returns: Array of all file paths
|
||||
private func splitDependencyFileList(_ string: String) -> [String] {
|
||||
struct ParseState {
|
||||
var buffer: String = ""
|
||||
var prevChar: Character?
|
||||
var result: [String] = []
|
||||
func with(buffer: String? = nil, prevChar: Character? = nil, result: [String]? = nil) -> ParseState {
|
||||
var new = self
|
||||
new.buffer = buffer ?? new.buffer
|
||||
new.prevChar = prevChar ?? new.prevChar
|
||||
new.result = result ?? new.result
|
||||
return new
|
||||
}
|
||||
func parseDependencyFileList(_ string: String) -> [String] {
|
||||
var result: [String] = []
|
||||
var prevChar: UTF8.CodeUnit?
|
||||
|
||||
// These index are used to move over the UTF8View of the string
|
||||
// The goal is to optimize the memory used, since UTF8View uses
|
||||
// the same memory as the original String without copying it
|
||||
var startIndex = string.utf8.startIndex
|
||||
var endIndex = startIndex
|
||||
|
||||
// This buffer is only used to save the part of the path that has been already parsed when finding a backslash
|
||||
var buffer: String = ""
|
||||
|
||||
for c in string.utf8 {
|
||||
switch c {
|
||||
case UTF8.CodeUnit(ascii: "\n") where prevChar == UTF8.CodeUnit(ascii: "\\"):
|
||||
startIndex = string.utf8.index(after: startIndex)
|
||||
endIndex = startIndex
|
||||
case UTF8.CodeUnit(ascii: " ") where startIndex == endIndex && buffer.isEmpty:
|
||||
startIndex = string.utf8.index(after: startIndex)
|
||||
endIndex = startIndex
|
||||
case UTF8.CodeUnit(ascii: " ") where prevChar != UTF8.CodeUnit(ascii: "\\"):
|
||||
// If a space is found and it is not escaped, then that's the end of the file path
|
||||
buffer += String(Substring(string.utf8[startIndex ..< endIndex]))
|
||||
result.append(buffer)
|
||||
buffer = ""
|
||||
prevChar = nil
|
||||
startIndex = string.utf8.index(after: endIndex)
|
||||
endIndex = startIndex
|
||||
case UTF8.CodeUnit(ascii: "\\"):
|
||||
// If a backslash is found it is not included in the file path
|
||||
// The current parsed range of the UTF8View is saved in the buffer as a String
|
||||
buffer += String(Substring(string.utf8[startIndex ..< endIndex]))
|
||||
// The backslash is assigned as the previous char
|
||||
prevChar = c
|
||||
// The indexes are moved to the next char so we continue parsing the String
|
||||
startIndex = string.utf8.index(after: endIndex)
|
||||
endIndex = startIndex
|
||||
default:
|
||||
// As long as it is possible the indexes are used to track the range of the string that
|
||||
// will be included in the file path (until it ends or until a backslash is found)
|
||||
endIndex = string.utf8.index(after: endIndex)
|
||||
// The char is assigned as the previous char
|
||||
prevChar = c
|
||||
}
|
||||
}
|
||||
let parseResult = string.reduce(ParseState()) { total, char in
|
||||
switch char {
|
||||
case "\n" where total.prevChar == "\\":
|
||||
return total
|
||||
case " " where total.buffer.isEmpty:
|
||||
return total
|
||||
case " " where total.prevChar == "\\":
|
||||
return total.with(buffer: "\(total.buffer) ")
|
||||
case " ":
|
||||
return total.with(buffer: "", prevChar: nil, result: total.result + [total.buffer])
|
||||
case "\\":
|
||||
return total.with(prevChar: "\\")
|
||||
default:
|
||||
return total.with(buffer: "\(total.buffer)\(char)", prevChar: char, result: total.result)
|
||||
}
|
||||
|
||||
if startIndex != endIndex {
|
||||
buffer += String(Substring(string.utf8[startIndex ..< endIndex]))
|
||||
result.append(buffer)
|
||||
}
|
||||
if !parseResult.buffer.isEmpty {
|
||||
return parseResult.result + [parseResult.buffer]
|
||||
}
|
||||
return parseResult.result
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ public class FileDependenciesWriter: DependenciesWriter {
|
||||
var content = ""
|
||||
for (file, deps) in dependencies {
|
||||
content.append(file + ": ")
|
||||
content.append(deps.joined(separator: " "))
|
||||
content.append(deps.map { $0.replacingOccurrences(of: " ", with: "\\ ") }.joined(separator: " "))
|
||||
content.append("\n")
|
||||
}
|
||||
try content.write(to: file, atomically: true, encoding: .utf8)
|
||||
|
||||
@@ -27,8 +27,11 @@ public struct Dependency: Equatable {
|
||||
case source
|
||||
case fingerprint
|
||||
case intermediate
|
||||
case derivedFile
|
||||
// Product of the target itself
|
||||
case ownProduct
|
||||
// User-excluded path
|
||||
case userExcluded
|
||||
case unknown
|
||||
}
|
||||
|
||||
@@ -55,14 +58,18 @@ class DependencyProcessorImpl: DependencyProcessor {
|
||||
private let productPath: String
|
||||
private let sourcePath: String
|
||||
private let intermediatePath: String
|
||||
private let derivedFilesPath: String
|
||||
private let bundlePath: String?
|
||||
private let skippedRegexes: [String]
|
||||
|
||||
init(xcode: URL, product: URL, source: URL, intermediate: URL, bundle: URL?) {
|
||||
init(xcode: URL, product: URL, source: URL, intermediate: URL, derivedFiles: URL, bundle: URL?, skippedRegexes: [String]) {
|
||||
xcodePath = xcode.path.dirPath()
|
||||
productPath = product.path.dirPath()
|
||||
sourcePath = source.path.dirPath()
|
||||
intermediatePath = intermediate.path.dirPath()
|
||||
derivedFilesPath = derivedFiles.path.dirPath()
|
||||
bundlePath = bundle?.path.dirPath()
|
||||
self.skippedRegexes = skippedRegexes
|
||||
}
|
||||
|
||||
func process(_ files: [URL]) -> [Dependency] {
|
||||
@@ -73,10 +80,14 @@ class DependencyProcessorImpl: DependencyProcessor {
|
||||
private func classify(_ files: [URL]) -> [Dependency] {
|
||||
return files.map { file -> Dependency in
|
||||
let filePath = file.resolvingSymlinksInPath().path
|
||||
if filePath.hasPrefix(xcodePath) {
|
||||
if skippedRegexes.contains(where: { filePath.range(of: $0, options: .regularExpression) != nil }) {
|
||||
return Dependency(url: file, type: .userExcluded)
|
||||
} else if filePath.hasPrefix(xcodePath) {
|
||||
return Dependency(url: file, type: .xcode)
|
||||
} else if filePath.hasPrefix(intermediatePath) {
|
||||
return Dependency(url: file, type: .intermediate)
|
||||
} else if filePath.hasPrefix(derivedFilesPath) {
|
||||
return Dependency(url: file, type: .derivedFile)
|
||||
} else if let bundle = bundlePath, filePath.hasPrefix(bundle) {
|
||||
// If a target produces a bundle, explicitly classify all
|
||||
// of products to distinguish from other targets products
|
||||
@@ -107,7 +118,12 @@ class DependencyProcessorImpl: DependencyProcessor {
|
||||
// - All files in `*/Interemediates/*` - this file are created on-fly for a given target
|
||||
// - Some files may depend on its own product (e.g. .m may #include *-Swift.h) - we know products will match
|
||||
// because in case of a hit, these will be taken from the artifact
|
||||
let irrelevantDependenciesType: [Dependency.Kind] = [.xcode, .intermediate, .ownProduct]
|
||||
// - Customized DERIVED_FILE_DIR may change a directory of
|
||||
// derived files, which by default is under `*/Interemediates`
|
||||
// - User-specified (in .rcinfo) files to exclude
|
||||
let irrelevantDependenciesType: [Dependency.Kind] = [
|
||||
.xcode, .intermediate, .ownProduct, .derivedFile, .userExcluded,
|
||||
]
|
||||
return !irrelevantDependenciesType.contains(dependency.type)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,10 @@ protocol FingerprintSyncer {
|
||||
func decorate(sourceDir: URL, fingerprint: String) throws
|
||||
/// Deletes fingerprint overrides in the dir (if already created)
|
||||
func delete(sourceDir: URL) throws
|
||||
/// Sets a fingerprint override for a singe file placed
|
||||
func decorate(file: URL, fingerprint: String) throws
|
||||
/// Deletes fingerprint override for a file (if already created)
|
||||
func delete(file: URL) throws
|
||||
}
|
||||
|
||||
class FileFingerprintSyncer: FingerprintSyncer {
|
||||
@@ -78,4 +82,25 @@ class FileFingerprintSyncer: FingerprintSyncer {
|
||||
try dirAccessor.removeItem(atPath: file.path)
|
||||
}
|
||||
}
|
||||
|
||||
func decorate(file: URL, fingerprint: String) throws {
|
||||
guard let fingerprintData = fingerprint.data(using: .utf8) else {
|
||||
throw FingerprintSyncerError.invalidFingerprint
|
||||
}
|
||||
let fingerprintFile = file.appendingPathExtension(fingerprintExtension)
|
||||
try dirAccessor.write(toPath: fingerprintFile.path, contents: fingerprintData)
|
||||
}
|
||||
|
||||
func delete(file: URL) throws {
|
||||
guard case .file = try dirAccessor.itemType(atPath: file.path) else {
|
||||
// no file to decorate (no module was generated)
|
||||
return
|
||||
}
|
||||
let overrideURL = file.appendingPathExtension(fingerprintExtension)
|
||||
guard case .file = try dirAccessor.itemType(atPath: overrideURL.path) else {
|
||||
// no override
|
||||
return
|
||||
}
|
||||
try dirAccessor.removeItem(atPath: overrideURL.path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,8 +42,8 @@ class OverlayDependenciesRemapper: DependenciesRemapper {
|
||||
|
||||
private func mapPath(
|
||||
_ path: String,
|
||||
source: KeyPath<OverlayMapping,URL>,
|
||||
destination: KeyPath<OverlayMapping,URL>
|
||||
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
|
||||
|
||||
@@ -91,7 +91,7 @@ class JsonOverlayReader: OverlayReader {
|
||||
case .strict:
|
||||
throw JsonOverlayReaderError.missingSourceFile(json)
|
||||
case .bestEffort:
|
||||
printWarning("overlay mapping file \(json) doesn't exist. Skipping overlay for the best-effort mode.")
|
||||
debugLog("overlay mapping file \(json) doesn't exist. Skipping overlay for the best-effort mode.")
|
||||
return []
|
||||
}
|
||||
}
|
||||
@@ -100,7 +100,7 @@ class JsonOverlayReader: OverlayReader {
|
||||
let mappings: [OverlayMapping] = try overlay.roots.reduce([]) { prev, root in
|
||||
switch root.type {
|
||||
case .directory:
|
||||
//iterate all contents
|
||||
// iterate all contents
|
||||
let dir = URL(fileURLWithPath: root.name)
|
||||
let mappings: [OverlayMapping] = try root.contents.map { content in
|
||||
switch content.type {
|
||||
@@ -124,7 +124,7 @@ class JsonOverlayReader: OverlayReader {
|
||||
case .strict:
|
||||
throw error
|
||||
case .bestEffort:
|
||||
printWarning("Overlay reader has failed with an error \(error). Best-effort mode - skipping an overlay.")
|
||||
errorLog("Overlay reader has failed with an error \(error). Best-effort mode - skipping an overlay.")
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,10 +30,10 @@ class PathDependenciesRemapperFactory {
|
||||
envs: [String: String],
|
||||
customMappings: [String: String]
|
||||
) throws -> StringDependenciesRemapper {
|
||||
let mappingMap = try envs.merging(customMappings) { envValue, outOfBandMapping in
|
||||
let mappingMap = try envs.merging(customMappings) { _, _ in
|
||||
throw PathDependenciesRemapperFactoryError.mappingKeyDuplication
|
||||
}
|
||||
let mappingOrderKeys = orderKeys + customMappings.keys
|
||||
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")
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -33,8 +33,13 @@ protocol DirScanner {
|
||||
|
||||
/// Returns all items in a directory (shallow search)
|
||||
/// - Parameter at: url of an existing directory to search
|
||||
/// - Throws: an error if dir doesn't exist or I/O error
|
||||
/// - Throws: an error if a dir doesn't exist or on I/O error
|
||||
func items(at dir: URL) throws -> [URL]
|
||||
|
||||
/// Returns all items in a directory (recursive search)
|
||||
/// - Parameter at: url of an existing directory to search
|
||||
/// - Throws: an error if a dir doesn't exist or on I/O error
|
||||
func recursiveItems(at dir: URL) throws -> [URL]
|
||||
}
|
||||
|
||||
typealias DirAccessor = FileAccessor & DirScanner
|
||||
@@ -54,4 +59,18 @@ extension FileManager: DirScanner {
|
||||
let resolvedDir = dir.resolvingSymlinksInPath()
|
||||
return try contentsOfDirectory(at: resolvedDir, includingPropertiesForKeys: nil, options: [])
|
||||
}
|
||||
|
||||
func recursiveItems(at dir: URL) throws -> [URL] {
|
||||
// Iterating DFS
|
||||
var queue: [URL] = [dir]
|
||||
var results: [URL] = []
|
||||
while let item = queue.popLast() {
|
||||
if try itemType(atPath: item.path) == .dir {
|
||||
try queue.append(contentsOf: items(at: item))
|
||||
} else {
|
||||
results.append(item)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,4 +52,8 @@ class DirAccessorComposer: DirAccessor {
|
||||
func items(at dir: URL) throws -> [URL] {
|
||||
try dirScanner.items(at: dir)
|
||||
}
|
||||
|
||||
func recursiveItems(at dir: URL) throws -> [URL] {
|
||||
try dirScanner.recursiveItems(at: dir)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
@@ -52,7 +52,9 @@ class EnvironmentFingerprintGenerator {
|
||||
}
|
||||
try fill(envKeys: Self.defaultEnvFingerprintKeys + customFingerprintEnvs)
|
||||
try generator.append(version)
|
||||
return try generator.generate()
|
||||
let result = try generator.generate()
|
||||
generatedFingerprint = result
|
||||
return result
|
||||
}
|
||||
|
||||
/// Creates a fingerprint of the environemtn, by hashing all ENVs specified in keys
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -23,17 +23,21 @@ struct AWSV4Signature {
|
||||
|
||||
let secretKey: String
|
||||
let accessKey: String
|
||||
let securityToken: String?
|
||||
let region: String
|
||||
let service: String
|
||||
let date: Date
|
||||
|
||||
|
||||
func addSignatureHeaderTo(request: inout URLRequest) {
|
||||
|
||||
request.setValue(request.url?.host, forHTTPHeaderField: "host")
|
||||
request.setValue(StringToSign.ISO8601BasicFormatter.string(from: date), forHTTPHeaderField: "x-amz-date")
|
||||
request.setValue((request.httpBody ?? Data()).sha256(), forHTTPHeaderField: "x-amz-content-sha256")
|
||||
|
||||
if let securityToken = securityToken {
|
||||
request.setValue(securityToken, forHTTPHeaderField: "x-amz-security-token")
|
||||
}
|
||||
|
||||
let canonicalRequest = CanonicalRequest(request: request)
|
||||
let stringToSign = StringToSign(
|
||||
region: region,
|
||||
|
||||
@@ -25,7 +25,7 @@ final class IgnoringCertificatesTrustManager: NSObject, URLSessionDelegate {
|
||||
completionHandler(.performDefaultHandling, nil)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let urlCredential = URLCredential(trust: serverTrust)
|
||||
completionHandler(.useCredential, urlCredential)
|
||||
}
|
||||
|
||||
@@ -29,12 +29,14 @@ class NetworkClientImpl: NetworkClient {
|
||||
private let session: URLSession
|
||||
private let fileManager: FileManager
|
||||
private let maxRetries: Int
|
||||
private let retryDelay: TimeInterval
|
||||
private let awsV4Signature: AWSV4Signature?
|
||||
|
||||
init(session: URLSession, retries: Int, fileManager: FileManager, awsV4Signature: AWSV4Signature?) {
|
||||
init(session: URLSession, retries: Int, retryDelay: TimeInterval, fileManager: FileManager, awsV4Signature: AWSV4Signature?) {
|
||||
self.session = session
|
||||
self.fileManager = fileManager
|
||||
maxRetries = retries
|
||||
self.maxRetries = retries
|
||||
self.retryDelay = retryDelay
|
||||
self.awsV4Signature = awsV4Signature
|
||||
}
|
||||
|
||||
@@ -75,7 +77,12 @@ class NetworkClientImpl: NetworkClient {
|
||||
func download(_ url: URL, to location: URL, completion: @escaping (Result<Void, NetworkClientError>) -> Void) {
|
||||
var request = URLRequest(url: url)
|
||||
setupAuthenticationSignatureIfPresent(&request)
|
||||
makeDownloadRequest(request, output: location, completion: completion)
|
||||
makeDownloadRequest(
|
||||
request,
|
||||
output: location,
|
||||
retries: maxRetries,
|
||||
completion: completion
|
||||
)
|
||||
}
|
||||
|
||||
func upload(_ file: URL, as url: URL, completion: @escaping (Result<Void, NetworkClientError>) -> Void) {
|
||||
@@ -123,7 +130,7 @@ class NetworkClientImpl: NetworkClient {
|
||||
dataTask.resume()
|
||||
}
|
||||
|
||||
private func makeDownloadRequest(_ request: URLRequest, output: URL, completion: @escaping (Result<Void, NetworkClientError>) -> Void) {
|
||||
private func makeDownloadRequest(_ request: URLRequest, output: URL, retries: Int, completion: @escaping (Result<Void, NetworkClientError>) -> Void) {
|
||||
guard fileManager.fileExists(atPath: output.path) == false else {
|
||||
infoLog("Download file found in the destination, skipping download.")
|
||||
completion(.success(()))
|
||||
@@ -133,6 +140,17 @@ class NetworkClientImpl: NetworkClient {
|
||||
let dataTask = session.downloadTask(with: request) { [fileManager] fileURL, _, error in
|
||||
guard let fileURL = fileURL else {
|
||||
let networkError = error.map(NetworkClientError.build) ?? .inconsistentSession
|
||||
if retries > 0 {
|
||||
infoLog("Download request failed with \(networkError). Left retries: \(retries).")
|
||||
self.retryDownload(
|
||||
request,
|
||||
output: output,
|
||||
retries: retries,
|
||||
completion: completion,
|
||||
after: self.retryDelay
|
||||
)
|
||||
return
|
||||
}
|
||||
errorLog("Download request failed: \(networkError)")
|
||||
completion(.failure(networkError))
|
||||
return
|
||||
@@ -173,7 +191,13 @@ class NetworkClientImpl: NetworkClient {
|
||||
if let error = responseError {
|
||||
if retries > 0 {
|
||||
infoLog("Upload request failed with \(error). Left retries: \(retries).")
|
||||
self.makeUploadRequest(request, input: input, retries: retries - 1, completion: completion)
|
||||
self.retryUpload(
|
||||
request,
|
||||
input: input,
|
||||
retries: retries,
|
||||
completion: completion,
|
||||
after: self.retryDelay
|
||||
)
|
||||
return
|
||||
}
|
||||
errorLog("Upload request failed: \(error)")
|
||||
@@ -184,6 +208,30 @@ class NetworkClientImpl: NetworkClient {
|
||||
}
|
||||
dataTask.resume()
|
||||
}
|
||||
|
||||
private func retryUpload(_ request: URLRequest, input: URL, retries: Int, completion: @escaping (Result<Void, NetworkClientError>) -> Void, after: TimeInterval) {
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + after) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.makeUploadRequest(
|
||||
request,
|
||||
input: input,
|
||||
retries: retries - 1,
|
||||
completion: completion
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func retryDownload(_ request: URLRequest, output: URL, retries: Int, completion: @escaping (Result<Void, NetworkClientError>) -> Void, after: TimeInterval) {
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + after) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.makeDownloadRequest(
|
||||
request,
|
||||
output: output,
|
||||
retries: retries - 1,
|
||||
completion: completion
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension NetworkClientError {
|
||||
|
||||
@@ -27,11 +27,20 @@ class RemoteNetworkClientAbstractFactory {
|
||||
private let upstreamStreamURL: [URL]
|
||||
private let networkClient: NetworkClient
|
||||
private let urlBuilderFactory: (URL) throws -> URLBuilder
|
||||
private let uploadBatchSize: Int
|
||||
|
||||
init(mode: Mode, downloadStreamURL: URL, upstreamStreamURL: [URL], networkClient: NetworkClient, urlBuilderFactory: @escaping (URL) throws -> URLBuilder) {
|
||||
init(
|
||||
mode: Mode,
|
||||
downloadStreamURL: URL,
|
||||
upstreamStreamURL: [URL],
|
||||
uploadBatchSize: Int,
|
||||
networkClient: NetworkClient,
|
||||
urlBuilderFactory: @escaping (URL) throws -> URLBuilder
|
||||
) {
|
||||
self.mode = mode
|
||||
self.downloadStreamURL = downloadStreamURL
|
||||
self.upstreamStreamURL = upstreamStreamURL
|
||||
self.uploadBatchSize = uploadBatchSize
|
||||
self.networkClient = networkClient
|
||||
self.urlBuilderFactory = urlBuilderFactory
|
||||
}
|
||||
@@ -49,7 +58,8 @@ class RemoteNetworkClientAbstractFactory {
|
||||
return ReplicatedRemotesNetworkClient(
|
||||
networkClient,
|
||||
download: downloadURLBuilder,
|
||||
uploads: upstreamBuilders
|
||||
uploads: upstreamBuilders,
|
||||
uploadBatchSize: uploadBatchSize
|
||||
)
|
||||
case .consumer:
|
||||
return RemoteNetworkClientImpl(networkClient, downloadURLBuilder)
|
||||
|
||||
@@ -23,10 +23,12 @@ import Foundation
|
||||
class ReplicatedRemotesNetworkClient: RemoteNetworkClientImpl {
|
||||
private let networkClient: NetworkClient
|
||||
private let uploadURLBuilders: [URLBuilder]
|
||||
private let uploadBatchSize: Int
|
||||
|
||||
init(_ networkClient: NetworkClient, download: URLBuilder, uploads uploadURLBuilders: [URLBuilder]) {
|
||||
init(_ networkClient: NetworkClient, download: URLBuilder, uploads uploadURLBuilders: [URLBuilder], uploadBatchSize: Int) {
|
||||
self.networkClient = networkClient
|
||||
self.uploadURLBuilders = uploadURLBuilders
|
||||
self.uploadBatchSize = uploadBatchSize
|
||||
super.init(networkClient, download)
|
||||
}
|
||||
|
||||
@@ -39,6 +41,9 @@ class ReplicatedRemotesNetworkClient: RemoteNetworkClientImpl {
|
||||
let group = DispatchGroup()
|
||||
var results: [Result<Void, NetworkClientError>] = Array(repeating: .failure(.noResponse), count: urls.count)
|
||||
urls.enumerated().forEach { index, url in
|
||||
if uploadBatchSize > 0 && index > 0 && index % uploadBatchSize == 0 {
|
||||
group.wait()
|
||||
}
|
||||
group.enter()
|
||||
networkClient.upload(file, as: url) { receivedResult in
|
||||
results[index] = receivedResult
|
||||
@@ -58,6 +63,9 @@ class ReplicatedRemotesNetworkClient: RemoteNetworkClientImpl {
|
||||
let group = DispatchGroup()
|
||||
var results: [Result<Void, NetworkClientError>] = Array(repeating: .failure(.noResponse), count: urls.count)
|
||||
urls.enumerated().forEach { index, url in
|
||||
if uploadBatchSize > 0 && index > 0 && index % uploadBatchSize == 0 {
|
||||
group.wait()
|
||||
}
|
||||
group.enter()
|
||||
networkClient.create(url) { receivedResult in
|
||||
results[index] = receivedResult
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import Foundation
|
||||
import XCRemoteCache
|
||||
|
||||
/// Wrapper for a `LD` program that copies the dynamic executable from a cached-downloaded location
|
||||
/// Fallbacks to a standard `clang` when the Ramote cache is not applicable (e.g. modified sources)
|
||||
/// Fallbacks to a standard `clang` when the Remote cache is not applicable (e.g. modified sources)
|
||||
public class XCLDMain {
|
||||
public func main() {
|
||||
let args = ProcessInfo().arguments
|
||||
@@ -48,7 +48,16 @@ public class XCLDMain {
|
||||
i += 1
|
||||
}
|
||||
guard let outputInput = output, let filelistInput = filelist, let dependencyInfoInput = dependencyInfo else {
|
||||
exit(1, "Missing 'output' argument. Args: \(args)")
|
||||
let ldCommand = "clang"
|
||||
print("Fallbacking to compilation using \(ldCommand).")
|
||||
|
||||
let args = ProcessInfo().arguments
|
||||
let paramList = [ldCommand] + args.dropFirst()
|
||||
let cargs = paramList.map { strdup($0) } + [nil]
|
||||
execvp(ldCommand, cargs)
|
||||
|
||||
/// C-function `execv` returns only when the command fails
|
||||
exit(1)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) 2021 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import Foundation
|
||||
import XCRemoteCache
|
||||
|
||||
/// Wrapper for a `LDPLUSPLUS` program that copies the dynamic executable from a cached-downloaded location
|
||||
/// Fallbacks to a standard `clang++` when the Remote cache is not applicable (e.g. modified sources)
|
||||
public class XCLDPlusPlusMain {
|
||||
public func main() {
|
||||
let args = ProcessInfo().arguments
|
||||
var output: String?
|
||||
var filelist: String?
|
||||
var dependencyInfo: String?
|
||||
var i = 0
|
||||
while i < args.count {
|
||||
switch args[i] {
|
||||
case "-o":
|
||||
output = args[i + 1]
|
||||
i += 1
|
||||
case "-filelist":
|
||||
filelist = args[i + 1]
|
||||
i += 1
|
||||
case "-dependency_info":
|
||||
// Skip following `-Xlinker` argument. Sample call:
|
||||
// `clang -dynamiclib ... -Xlinker -dependency_info -Xlinker /path/Target_dependency_info.dat`
|
||||
dependencyInfo = args[i + 2]
|
||||
i += 2
|
||||
default:
|
||||
break
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
guard let outputInput = output, let filelistInput = filelist, let dependencyInfoInput = dependencyInfo else {
|
||||
let ldCommand = "clang++"
|
||||
print("Fallbacking to compilation using \(ldCommand).")
|
||||
|
||||
let args = ProcessInfo().arguments
|
||||
let paramList = [ldCommand] + args.dropFirst()
|
||||
let cargs = paramList.map { strdup($0) } + [nil]
|
||||
execvp(ldCommand, cargs)
|
||||
|
||||
/// C-function `execv` returns only when the command fails
|
||||
exit(1)
|
||||
}
|
||||
|
||||
|
||||
// TODO: consider using `clang_command` from .rcinfo
|
||||
/// concrete clang path should be taken from the current toolchain
|
||||
let fallbackCommand = "clang++"
|
||||
XCCreateBinary(
|
||||
output: outputInput,
|
||||
filelist: filelistInput,
|
||||
dependencyInfo: dependencyInfoInput,
|
||||
fallbackCommand: fallbackCommand,
|
||||
stepDescription: "xcldplusplus"
|
||||
).run()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) 2021 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import XCRemoteCache
|
||||
|
||||
XCLDPlusPlusMain().main()
|
||||
@@ -18,53 +18,24 @@
|
||||
// under the License.
|
||||
|
||||
import Foundation
|
||||
import xclibtoolSupport
|
||||
import XCRemoteCache
|
||||
|
||||
public enum XCLibtoolMainError: Error {
|
||||
case missingOutput
|
||||
case unsupportedMode
|
||||
}
|
||||
|
||||
/// Wrapper for a `libtool` program that copies the build executable (e.g. .a) from a cached-downloaded location
|
||||
/// Fallbacks to a standard `libtool` when the Ramote cache is not applicable (e.g. modified sources)
|
||||
public class XCLibtoolMain {
|
||||
public init() { }
|
||||
|
||||
public func main() {
|
||||
let args = ProcessInfo().arguments
|
||||
var output: String?
|
||||
// all input arguments library '.a'. Used to create an universal binary
|
||||
var inputLibraries: [String] = []
|
||||
var filelist: String?
|
||||
var dependencyInfo: String?
|
||||
var i = 0
|
||||
while i < args.count {
|
||||
switch args[i] {
|
||||
case "-o":
|
||||
output = args[i + 1]
|
||||
i += 1
|
||||
case "-filelist":
|
||||
filelist = args[i + 1]
|
||||
i += 1
|
||||
case "-dependency_info":
|
||||
dependencyInfo = args[i + 1]
|
||||
i += 1
|
||||
case let input where input.hasSuffix(".a"):
|
||||
inputLibraries.append(input)
|
||||
default:
|
||||
break
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
guard let outputInput = output else {
|
||||
exit(1, "Missing 'output' argument. Args: \(args)")
|
||||
}
|
||||
|
||||
let mode: XCLibtoolMode
|
||||
if let filelistInput = filelist, let dependencyInfoInput = dependencyInfo {
|
||||
// libtool is creating a library
|
||||
mode = .createLibrary(output: outputInput, filelist: filelistInput, dependencyInfo: dependencyInfoInput)
|
||||
} else if !inputLibraries.isEmpty {
|
||||
// multiple input libraries suggest creating an universal binary
|
||||
mode = .createUniversalBinary(output: outputInput, inputs: inputLibraries)
|
||||
} else {
|
||||
// unknown mode
|
||||
exit(1, "Unsupported mode. Args: \(args)")
|
||||
}
|
||||
do {
|
||||
let mode = try XCLibtoolHelper.buildMode(args: args)
|
||||
try XCLibtool(mode).run()
|
||||
} catch {
|
||||
exit(1, "Failed with: \(error). Args: \(args)")
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) 2023 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import Foundation
|
||||
import XCRemoteCache
|
||||
|
||||
public enum XCLibtoolHelperError: Error {
|
||||
case missingOutput
|
||||
case unsupportedMode
|
||||
}
|
||||
|
||||
public class XCLibtoolHelper {
|
||||
public static func buildMode(args: [String]) throws -> XCLibtoolMode {
|
||||
var output: String?
|
||||
// all input arguments are '*.a' or no path extension. Used to create an universal binary
|
||||
var inputLibraries: [String] = []
|
||||
var filelist: String?
|
||||
var dependencyInfo: String?
|
||||
var i = 0
|
||||
while i < args.count {
|
||||
switch args[i] {
|
||||
case "-o":
|
||||
output = args[i + 1]
|
||||
i += 1
|
||||
case "-filelist":
|
||||
filelist = args[i + 1]
|
||||
i += 1
|
||||
case "-dependency_info":
|
||||
dependencyInfo = args[i + 1]
|
||||
i += 1
|
||||
case let input where ["", "a"].contains(URL(string: args[i])?.pathExtension):
|
||||
// Support for static frameworks (no extension) and static libraries (.a)
|
||||
inputLibraries.append(input)
|
||||
default:
|
||||
break
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
guard let outputInput = output else {
|
||||
throw XCLibtoolHelperError.missingOutput
|
||||
}
|
||||
|
||||
let mode: XCLibtoolMode
|
||||
if let filelistInput = filelist, let dependencyInfoInput = dependencyInfo {
|
||||
// libtool is creating a library
|
||||
mode = .createLibrary(output: outputInput, filelist: filelistInput, dependencyInfo: dependencyInfoInput)
|
||||
} else if !inputLibraries.isEmpty {
|
||||
// multiple input libraries suggest creating an universal binary
|
||||
mode = .createUniversalBinary(output: outputInput, inputs: inputLibraries)
|
||||
} else {
|
||||
// unknown mode
|
||||
throw XCLibtoolHelperError.unsupportedMode
|
||||
}
|
||||
return mode
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) 2023 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import Foundation
|
||||
import XCRemoteCache
|
||||
|
||||
/// Wrapper for a `lipo` program that links any of input binaries to the destination paths
|
||||
/// Fallbacks to a standard `lipo` when the Ramote cache is not applicable (e.g. modified sources)
|
||||
public class XCLipoMain {
|
||||
public init() { }
|
||||
|
||||
public func main() {
|
||||
let args = ProcessInfo().arguments
|
||||
var output: String?
|
||||
var create = false
|
||||
var inputs: [String] = []
|
||||
|
||||
var i = 1
|
||||
while i < args.count {
|
||||
switch args[i] {
|
||||
case "-output":
|
||||
output = args[i + 1]
|
||||
i += 1
|
||||
case "-create":
|
||||
create = true
|
||||
default:
|
||||
inputs.append(args[i])
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
let lipoCommand = "lipo"
|
||||
guard let output = output, !inputs.isEmpty, create else {
|
||||
print("Fallbacking to compilation using \(lipoCommand).")
|
||||
|
||||
let args = ProcessInfo().arguments
|
||||
let paramList = [lipoCommand] + args.dropFirst()
|
||||
let cargs = paramList.map { strdup($0) } + [nil]
|
||||
execvp(lipoCommand, cargs)
|
||||
|
||||
/// C-function `execv` returns only when the command fails
|
||||
exit(1)
|
||||
}
|
||||
|
||||
do {
|
||||
try XCLipo(
|
||||
output: output,
|
||||
inputs: inputs,
|
||||
fallbackCommand: lipoCommand,
|
||||
stepDescription: "xclipo"
|
||||
).run()
|
||||
} catch {
|
||||
exit(1, "Failed with: \(error). Args: \(args)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) 2023 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import XCRemoteCache
|
||||
|
||||
XCLipoMain().main()
|
||||
@@ -60,8 +60,16 @@ public class XCSwiftcMain {
|
||||
let targetInputInput = target,
|
||||
let swiftFileListInput = swiftFileList
|
||||
else {
|
||||
print("Missing argument. Args: \(args)")
|
||||
exit(1)
|
||||
let swiftcCommand = "swiftc"
|
||||
print("Fallbacking to compilation using \(swiftcCommand).")
|
||||
|
||||
let args = ProcessInfo().arguments
|
||||
let paramList = [swiftcCommand] + args.dropFirst()
|
||||
let cargs = paramList.map { strdup($0) } + [nil]
|
||||
execvp(swiftcCommand, cargs)
|
||||
|
||||
/// C-function `execv` returns only when the command fails
|
||||
exit(1)
|
||||
}
|
||||
let swiftcArgsInput = SwiftcArgInput(
|
||||
objcHeaderOutput: objcHeaderOutputInput,
|
||||
|
||||
@@ -28,6 +28,8 @@ class ArtifactSwiftProductsBuilderImplTests: FileXCTestCase {
|
||||
private var swiftmoduleDocFile: URL!
|
||||
private var swiftmoduleSourceInfoFile: URL!
|
||||
private var swiftmoduleInterfaceFile: URL!
|
||||
private var privateSwiftmoduleInterfaceFile: URL!
|
||||
private var abiJsonFile: URL!
|
||||
private var workingDir: URL!
|
||||
private var builder: ArtifactSwiftProductsBuilderImpl!
|
||||
|
||||
@@ -39,6 +41,9 @@ class ArtifactSwiftProductsBuilderImplTests: FileXCTestCase {
|
||||
swiftmoduleDocFile = moduleDir.appendingPathComponent("MyModule.swiftdoc")
|
||||
swiftmoduleSourceInfoFile = moduleDir.appendingPathComponent("MyModule.swiftsourceinfo")
|
||||
swiftmoduleInterfaceFile = moduleDir.appendingPathComponent("MyModule.swiftinterface")
|
||||
privateSwiftmoduleInterfaceFile = moduleDir.appendingPathComponent("MyModule.private.swiftinterface")
|
||||
abiJsonFile = moduleDir.appendingPathComponent("MyModule.abi.json")
|
||||
|
||||
workingDir = rootDir.appendingPathComponent("working")
|
||||
builder = ArtifactSwiftProductsBuilderImpl(
|
||||
workingDir: workingDir,
|
||||
@@ -98,6 +103,8 @@ class ArtifactSwiftProductsBuilderImplTests: FileXCTestCase {
|
||||
try fileManager.spt_createEmptyFile(swiftmoduleDocFile)
|
||||
try fileManager.spt_createEmptyFile(swiftmoduleSourceInfoFile)
|
||||
try fileManager.spt_createEmptyFile(swiftmoduleInterfaceFile)
|
||||
try fileManager.spt_createEmptyFile(privateSwiftmoduleInterfaceFile)
|
||||
try fileManager.spt_createEmptyFile(abiJsonFile)
|
||||
let builderSwiftmoduleDir =
|
||||
builder
|
||||
.buildingArtifactSwiftModulesLocation()
|
||||
@@ -110,6 +117,10 @@ class ArtifactSwiftProductsBuilderImplTests: FileXCTestCase {
|
||||
builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftsourceinfo")
|
||||
let expectedBuildedSwiftInterfaceFile =
|
||||
builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftinterface")
|
||||
let expectedPrivateSwiftmoduleInterfaceFile =
|
||||
builderSwiftmoduleDir.appendingPathComponent("MyModule.private.swiftinterface")
|
||||
let expectedAbiJsonFile =
|
||||
builderSwiftmoduleDir.appendingPathComponent("MyModule.abi.json")
|
||||
|
||||
try builder.includeModuleDefinitionsToTheArtifact(arch: "arm64", moduleURL: swiftmoduleFile)
|
||||
|
||||
@@ -117,6 +128,8 @@ class ArtifactSwiftProductsBuilderImplTests: FileXCTestCase {
|
||||
XCTAssertTrue(fileManager.fileExists(atPath: expectedBuildedSwiftmoduledocFile.path))
|
||||
XCTAssertTrue(fileManager.fileExists(atPath: expectedBuildedSwiftSourceInfoFile.path))
|
||||
XCTAssertTrue(fileManager.fileExists(atPath: expectedBuildedSwiftInterfaceFile.path))
|
||||
XCTAssertTrue(fileManager.fileExists(atPath: expectedPrivateSwiftmoduleInterfaceFile.path))
|
||||
XCTAssertTrue(fileManager.fileExists(atPath: expectedAbiJsonFile.path))
|
||||
}
|
||||
|
||||
func testFailsIncludingWhenMissingRequiredSwiftmoduleFiles() throws {
|
||||
|
||||
@@ -32,6 +32,8 @@ class BuildArtifactCreatorTests: FileXCTestCase {
|
||||
private var swiftdocURL: URL!
|
||||
private var swiftSourceInfoURL: URL!
|
||||
private var swiftInterfaceURL: URL!
|
||||
private var privateSwiftInterfaceURL: URL!
|
||||
private var abiJsonURL: URL!
|
||||
private var executablePath: String!
|
||||
private var executableURL: URL!
|
||||
private var creator: BuildArtifactCreator!
|
||||
@@ -53,11 +55,16 @@ class BuildArtifactCreatorTests: FileXCTestCase {
|
||||
.appendingPathComponent("Target.swiftsourceinfo")
|
||||
swiftInterfaceURL = workDirectory.appendingPathComponent("Objects-normal")
|
||||
.appendingPathComponent("Target.swiftinterface")
|
||||
privateSwiftInterfaceURL = workDirectory.appendingPathComponent("Objects-normal")
|
||||
.appendingPathComponent("Target.private.swiftinterface")
|
||||
abiJsonURL = workDirectory.appendingPathComponent("Objects-normal")
|
||||
.appendingPathComponent("Target.abi.json")
|
||||
executablePath = "libTarget.a"
|
||||
executableURL = buildDir.appendingPathComponent(executablePath)
|
||||
dSYM = executableURL.deletingPathExtension().appendingPathExtension(".dSYM")
|
||||
try fileManager.spt_createEmptyFile(executableURL)
|
||||
try fileManager.spt_createEmptyFile(headerURL)
|
||||
let artifactProcessor = NoopArtifactProcessor()
|
||||
|
||||
creator = BuildArtifactCreator(
|
||||
buildDir: buildDir,
|
||||
@@ -67,6 +74,7 @@ class BuildArtifactCreatorTests: FileXCTestCase {
|
||||
modulesFolderPath: "",
|
||||
dSYMPath: dSYM,
|
||||
metaWriter: JsonMetaWriter(fileWriter: fileManager, pretty: false),
|
||||
artifactProcessor: artifactProcessor,
|
||||
fileManager: fileManager
|
||||
)
|
||||
}
|
||||
@@ -122,6 +130,8 @@ class BuildArtifactCreatorTests: FileXCTestCase {
|
||||
try fileManager.spt_createEmptyFile(swiftdocURL)
|
||||
try fileManager.spt_createEmptyFile(swiftSourceInfoURL)
|
||||
try fileManager.spt_createEmptyFile(swiftInterfaceURL)
|
||||
try fileManager.spt_createEmptyFile(privateSwiftInterfaceURL)
|
||||
try fileManager.spt_createEmptyFile(abiJsonURL)
|
||||
|
||||
try creator.includeModuleDefinitionsToTheArtifact(arch: "arch", moduleURL: swiftmoduleURL)
|
||||
let artifact = try creator.createArtifact(artifactKey: "key", meta: sampleMeta)
|
||||
@@ -136,6 +146,8 @@ class BuildArtifactCreatorTests: FileXCTestCase {
|
||||
unzippedURL.appendingPathComponent("swiftmodule/arch/Target.swiftdoc"),
|
||||
unzippedURL.appendingPathComponent("swiftmodule/arch/Target.swiftsourceinfo"),
|
||||
unzippedURL.appendingPathComponent("swiftmodule/arch/Target.swiftinterface"),
|
||||
unzippedURL.appendingPathComponent("swiftmodule/arch/Target.private.swiftinterface"),
|
||||
unzippedURL.appendingPathComponent("swiftmodule/arch/Target.abi.json"),
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
// Copyright (c) 2022 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
@testable import XCRemoteCache
|
||||
|
||||
import XCTest
|
||||
|
||||
class TextFileDependenciesRemapperTests: FileXCTestCase {
|
||||
|
||||
let stringsRemapper = StringDependenciesRemapper(
|
||||
mappings: [
|
||||
.init(generic: "$(SRCROOT)", local: "/example"),
|
||||
])
|
||||
let fileAccessor = FileAccessorFake(mode: .strict)
|
||||
var remapper: TextFileDependenciesRemapper!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
remapper = TextFileDependenciesRemapper(
|
||||
remapper: stringsRemapper,
|
||||
fileAccessor: fileAccessor
|
||||
)
|
||||
}
|
||||
|
||||
func testRemapsGenericPlaceholders() throws {
|
||||
try fileAccessor.write(toPath: "/file", contents: "Some $(SRCROOT).")
|
||||
|
||||
try remapper.remap(fromGeneric: "/file")
|
||||
|
||||
try XCTAssertEqual(fileAccessor.contents(atPath: "/file"), "Some /example.")
|
||||
}
|
||||
|
||||
func testRemapsLocalPathsToPlaceholders() throws {
|
||||
try fileAccessor.write(toPath: "/file", contents: "Some /example.")
|
||||
|
||||
try remapper.remap(fromLocal: "/file")
|
||||
|
||||
try XCTAssertEqual(fileAccessor.contents(atPath: "/file"), "Some $(SRCROOT).")
|
||||
}
|
||||
|
||||
func testPersistsEmptyLines() throws {
|
||||
let multilineData = """
|
||||
Line1
|
||||
|
||||
Line 2
|
||||
""".data(using: .utf8)
|
||||
try fileAccessor.write(toPath: "/file", contents: multilineData)
|
||||
|
||||
try remapper.remap(fromGeneric: "/file")
|
||||
|
||||
try XCTAssertEqual(fileAccessor.contents(atPath: "/file"), multilineData)
|
||||
}
|
||||
|
||||
func testPersistsEmptyLineAtTheEnd() throws {
|
||||
// swiftlint:disable trailing_whitespace
|
||||
let multilineData = """
|
||||
Line1
|
||||
|
||||
Line 2
|
||||
|
||||
""".data(using: .utf8)
|
||||
// swiftlint:enable trailing_whitespace
|
||||
try fileAccessor.write(toPath: "/file", contents: multilineData)
|
||||
|
||||
try remapper.remap(fromGeneric: "/file")
|
||||
|
||||
try XCTAssertEqual(fileAccessor.contents(atPath: "/file"), multilineData)
|
||||
}
|
||||
|
||||
func testReplacesMultipletimesInLine() throws {
|
||||
try fileAccessor.write(toPath: "/file", contents: "$(SRCROOT) and $(SRCROOT)")
|
||||
|
||||
try remapper.remap(fromGeneric: "/file")
|
||||
|
||||
try XCTAssertEqual(fileAccessor.contents(atPath: "/file"), "/example and /example")
|
||||
}
|
||||
|
||||
func testReplacesInMultipleLine() throws {
|
||||
try fileAccessor.write(toPath: "/file", contents: "$(SRCROOT)\n$(SRCROOT)")
|
||||
|
||||
try remapper.remap(fromGeneric: "/file")
|
||||
|
||||
try XCTAssertEqual(fileAccessor.contents(atPath: "/file"), "/example\n/example")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) 2022 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
@testable import XCRemoteCache
|
||||
|
||||
import XCTest
|
||||
|
||||
class UnzippedArtifactProcessorTests: FileXCTestCase {
|
||||
|
||||
private let fileAccessor = FileAccessorFake(mode: .strict)
|
||||
private let remapper = StringDependenciesRemapper(mappings: [.init(generic: "$(SRCROOT)", local: "/local")])
|
||||
private var fileRemapper: FileDependenciesRemapper!
|
||||
private var processor: UnzippedArtifactProcessor!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
fileRemapper = TextFileDependenciesRemapper(remapper: remapper, fileAccessor: fileAccessor)
|
||||
processor = UnzippedArtifactProcessor(
|
||||
fileRemapper: fileRemapper,
|
||||
dirScanner: fileAccessor
|
||||
)
|
||||
}
|
||||
|
||||
func testProcessingRawArtifactReplacesPlaceholders() throws {
|
||||
try fileAccessor.write(toPath: "/artifact/include/file", contents: "Some $(SRCROOT)")
|
||||
|
||||
try processor.process(rawArtifact: "/artifact")
|
||||
|
||||
XCTAssertEqual(try fileAccessor.contents(atPath: "/artifact/include/file"), "Some /local")
|
||||
}
|
||||
|
||||
func testProcessingRawArtifactReplacesInNestedInclude() throws {
|
||||
try fileAccessor.write(toPath: "/artifact/include/nested/file", contents: "Some $(SRCROOT)")
|
||||
|
||||
try processor.process(rawArtifact: "/artifact")
|
||||
|
||||
XCTAssertEqual(try fileAccessor.contents(atPath: "/artifact/include/nested/file"), "Some /local")
|
||||
}
|
||||
|
||||
func testProcessingRawArtifactDoesntReplacesInNonIncludeDir() throws {
|
||||
try fileAccessor.write(toPath: "/artifact/some/file", contents: "Some $(SRCROOT)")
|
||||
|
||||
try processor.process(rawArtifact: "/artifact")
|
||||
|
||||
XCTAssertEqual(try fileAccessor.contents(atPath: "/artifact/some/file"), "Some $(SRCROOT)")
|
||||
}
|
||||
|
||||
func testDoesntProcessEmptyFiles() throws {
|
||||
try fileAccessor.write(toPath: "/artifact/include/.hidden", contents: "Some $(SRCROOT)")
|
||||
|
||||
try processor.process(rawArtifact: "/artifact")
|
||||
|
||||
XCTAssertEqual(try fileAccessor.contents(atPath: "/artifact/include/.hidden"), "Some $(SRCROOT)")
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,11 @@ class ZipArtifactOrganizerTests: XCTestCase {
|
||||
|
||||
func testPreparePlacesArtifactInTheActiveLocation() throws {
|
||||
let zipURL = try prepareZipFile(content: "Magic", fileName: "content.txt")
|
||||
let organizer = ZipArtifactOrganizer(targetTempDir: workingDirectory, fileManager: fileManager)
|
||||
let organizer = ZipArtifactOrganizer(
|
||||
targetTempDir: workingDirectory,
|
||||
artifactProcessors: [],
|
||||
fileManager: fileManager
|
||||
)
|
||||
|
||||
let preparedArtifact = try organizer.prepare(artifact: zipURL)
|
||||
|
||||
@@ -64,7 +68,11 @@ class ZipArtifactOrganizerTests: XCTestCase {
|
||||
|
||||
func testPreparingExistingArtifact() throws {
|
||||
let zipURL = try prepareZipFile(content: "Magic", fileName: "content.txt")
|
||||
let organizer = ZipArtifactOrganizer(targetTempDir: workingDirectory, fileManager: fileManager)
|
||||
let organizer = ZipArtifactOrganizer(
|
||||
targetTempDir: workingDirectory,
|
||||
artifactProcessors: [],
|
||||
fileManager: fileManager
|
||||
)
|
||||
|
||||
_ = try organizer.prepare(artifact: zipURL)
|
||||
let preparedArtifact = try organizer.prepare(artifact: zipURL)
|
||||
@@ -75,7 +83,11 @@ class ZipArtifactOrganizerTests: XCTestCase {
|
||||
|
||||
func testPreparePlacesArtifactInTheFileKeyRelatedLocation() throws {
|
||||
let zipURL = try prepareZipFile(content: "Magic", fileName: "content.txt", zipFileName: "abb32_fileKey")
|
||||
let organizer = ZipArtifactOrganizer(targetTempDir: workingDirectory, fileManager: fileManager)
|
||||
let organizer = ZipArtifactOrganizer(
|
||||
targetTempDir: workingDirectory,
|
||||
artifactProcessors: [],
|
||||
fileManager: fileManager
|
||||
)
|
||||
let expectedArtifactLocation = workingDirectory.appendingPathComponent("abb32_fileKey")
|
||||
|
||||
let preparedArtifact = try organizer.prepare(artifact: zipURL)
|
||||
@@ -89,7 +101,11 @@ class ZipArtifactOrganizerTests: XCTestCase {
|
||||
let artifactLocation = workingDirectory.appendingPathComponent("xccache")
|
||||
.appendingPathComponent(sampleFileKey, isDirectory: true)
|
||||
try fileManager.createDirectory(at: artifactLocation, withIntermediateDirectories: true, attributes: nil)
|
||||
let organizer = ZipArtifactOrganizer(targetTempDir: workingDirectory, fileManager: fileManager)
|
||||
let organizer = ZipArtifactOrganizer(
|
||||
targetTempDir: workingDirectory,
|
||||
artifactProcessors: [],
|
||||
fileManager: fileManager
|
||||
)
|
||||
|
||||
let result = try organizer.prepareArtifactLocationFor(fileKey: sampleFileKey)
|
||||
if case .artifactExists(artifactDir: let u) = result {
|
||||
@@ -105,7 +121,11 @@ class ZipArtifactOrganizerTests: XCTestCase {
|
||||
.appendingPathComponent("xccache")
|
||||
.appendingPathComponent(sampleFileKey)
|
||||
.appendingPathExtension("zip")
|
||||
let organizer = ZipArtifactOrganizer(targetTempDir: workingDirectory, fileManager: fileManager)
|
||||
let organizer = ZipArtifactOrganizer(
|
||||
targetTempDir: workingDirectory,
|
||||
artifactProcessors: [],
|
||||
fileManager: fileManager
|
||||
)
|
||||
|
||||
let result = try organizer.prepareArtifactLocationFor(fileKey: sampleFileKey)
|
||||
|
||||
@@ -126,7 +146,11 @@ class ZipArtifactOrganizerTests: XCTestCase {
|
||||
try fileManager.createDirectory(at: activeArtifact, withIntermediateDirectories: true, attributes: nil)
|
||||
try fileManager.spt_forceSymbolicLink(at: activeLink, withDestinationURL: activeArtifact)
|
||||
|
||||
let organizer = ZipArtifactOrganizer(targetTempDir: workingDirectory, fileManager: fileManager)
|
||||
let organizer = ZipArtifactOrganizer(
|
||||
targetTempDir: workingDirectory,
|
||||
artifactProcessors: [],
|
||||
fileManager: fileManager
|
||||
)
|
||||
|
||||
let fileKey = try organizer.getActiveArtifactFilekey()
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ class ThinningCreatorPluginTests: FileXCTestCase {
|
||||
|
||||
XCTAssertEqual(extraKeys, [
|
||||
"thinning_Generated": "000",
|
||||
"thinning_Reused": "999"
|
||||
"thinning_Reused": "999",
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -67,7 +67,8 @@ class ThinningDiskSwiftcProductsGeneratorTests: FileXCTestCase {
|
||||
artifactSwiftModuleObjCFile: headerFile
|
||||
)
|
||||
|
||||
XCTAssertEqual(generatedModulePath, destinationSwiftModuleDir)
|
||||
XCTAssertEqual(generatedModulePath.swiftmoduleDir, destinationSwiftModuleDir)
|
||||
XCTAssertEqual(generatedModulePath.objcHeaderFile, objCHeader)
|
||||
XCTAssertEqual(fileManager.contents(atPath: expectedSwiftSourceInfoFile.path), "sourceInfo".data(using: .utf8))
|
||||
XCTAssertEqual(fileManager.contents(atPath: objCHeader.path), "header".data(using: .utf8))
|
||||
}
|
||||
|
||||
+1
@@ -30,6 +30,7 @@ class UnzippedArtifactSwiftProductsOrganizerTests: XCTestCase {
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
let destination = SwiftcProductsGeneratorOutput(swiftmoduleDir: destination, objcHeaderFile: "")
|
||||
generator = SwiftcProductsGeneratorSpy(generatedDestination: destination)
|
||||
dirAccessor = DirAccessorFake()
|
||||
syncer = FileFingerprintSyncer(
|
||||
|
||||
@@ -26,6 +26,7 @@ class PostbuildContextTests: FileXCTestCase {
|
||||
private static let SampleEnvs = [
|
||||
"TARGET_NAME": "TARGET_NAME",
|
||||
"TARGET_TEMP_DIR": "TARGET_TEMP_DIR",
|
||||
"DERIVED_FILE_DIR": "DERIVED_FILE_DIR",
|
||||
"ARCHS": "x86_64",
|
||||
"OBJECT_FILE_DIR_normal": "/OBJECT_FILE_DIR_normal" ,
|
||||
"CONFIGURATION": "CONFIGURATION",
|
||||
@@ -42,7 +43,8 @@ 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",
|
||||
"PUBLIC_HEADERS_FOLDER_PATH": "/usr/local/include",
|
||||
]
|
||||
|
||||
override func setUpWithError() throws {
|
||||
@@ -129,4 +131,32 @@ class PostbuildContextTests: FileXCTestCase {
|
||||
|
||||
XCTAssertEqual(context.compilationTempDir, "/OBJECT_FILE_DIR_custom/x86_64")
|
||||
}
|
||||
|
||||
func testGenericPublicHeaderDestinationIsSkipped() throws {
|
||||
var envs = Self.SampleEnvs
|
||||
envs["PUBLIC_HEADERS_FOLDER_PATH"] = "/usr/local/include"
|
||||
|
||||
let context = try PostbuildContext(config, env: envs)
|
||||
|
||||
XCTAssertNil(context.publicHeadersFolderPath)
|
||||
}
|
||||
|
||||
func testPublicHeaderFolderIsRelativeToProductsDir() throws {
|
||||
var envs = Self.SampleEnvs
|
||||
envs["BUILT_PRODUCTS_DIR"] = "/MyBuiltProductsDir"
|
||||
envs["PUBLIC_HEADERS_FOLDER_PATH"] = "MyModule.grameworks/Headers"
|
||||
|
||||
let context = try PostbuildContext(config, env: envs)
|
||||
|
||||
XCTAssertEqual(context.publicHeadersFolderPath, "/MyBuiltProductsDir/MyModule.grameworks/Headers")
|
||||
}
|
||||
|
||||
func testPublicHeaderFolderPathEnvIsOptional() throws {
|
||||
var envs = Self.SampleEnvs
|
||||
envs.removeValue(forKey: "PUBLIC_HEADERS_FOLDER_PATH")
|
||||
|
||||
let context = try PostbuildContext(config, env: envs)
|
||||
|
||||
XCTAssertNil(context.publicHeadersFolderPath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,12 +20,14 @@
|
||||
@testable import XCRemoteCache
|
||||
import XCTest
|
||||
|
||||
// swiftlint:disable file_length
|
||||
// swiftlint:disable:next type_body_length
|
||||
class PostbuildTests: FileXCTestCase {
|
||||
private var postbuildContext = PostbuildContext(
|
||||
mode: .producer,
|
||||
targetName: "",
|
||||
targetTempDir: "",
|
||||
derivedFilesDir: "",
|
||||
compilationTempDir: "",
|
||||
configuration: "",
|
||||
platform: "",
|
||||
@@ -52,7 +54,9 @@ class PostbuildTests: FileXCTestCase {
|
||||
thinnedTargets: [],
|
||||
action: .build,
|
||||
modeMarkerPath: "",
|
||||
overlayHeadersPath: ""
|
||||
overlayHeadersPath: "",
|
||||
irrelevantDependenciesPaths: [],
|
||||
publicHeadersFolderPath: nil
|
||||
)
|
||||
private var network = RemoteNetworkClientImpl(
|
||||
NetworkClientFake(fileManager: .default),
|
||||
@@ -77,7 +81,9 @@ class PostbuildTests: FileXCTestCase {
|
||||
product: "/Product",
|
||||
source: "/Source",
|
||||
intermediate: "/Intermediate",
|
||||
bundle: nil
|
||||
derivedFiles: "/DerivedFiles",
|
||||
bundle: nil,
|
||||
skippedRegexes: []
|
||||
)
|
||||
private var overrideManager = FingerprintOverrideManagerImpl(
|
||||
overridingFileExtensions: ["swiftmodule"],
|
||||
@@ -638,5 +644,79 @@ class PostbuildTests: FileXCTestCase {
|
||||
|
||||
XCTAssertEqual(downloadedMeta, expectedMeta)
|
||||
}
|
||||
|
||||
func testDecoratesDerivedSwiftHeaderWithEmptyModulesFolderPath() throws {
|
||||
let dir = try prepareTempDir()
|
||||
let derivedSourcesDir = dir
|
||||
.appendingPathComponent("DerivedSources")
|
||||
let swiftSwiftHeader = derivedSourcesDir
|
||||
.appendingPathComponent("MyModule-Swift.h")
|
||||
let swiftSwiftHeaderOverride = swiftSwiftHeader
|
||||
.appendingPathExtension("md5")
|
||||
|
||||
try fileManager.spt_createEmptyDir(derivedSourcesDir)
|
||||
try fileManager.spt_createEmptyFile(swiftSwiftHeader)
|
||||
postbuildContext.moduleName = "MyModule"
|
||||
postbuildContext.derivedSourcesDir = derivedSourcesDir
|
||||
let postbuild = Postbuild(
|
||||
context: postbuildContext,
|
||||
networkClient: network,
|
||||
remapper: remapper,
|
||||
fingerprintAccumulator: fingerprintGenerator,
|
||||
artifactsOrganizer: organizer,
|
||||
artifactCreator: artifactCreator,
|
||||
fingerprintSyncer: syncer,
|
||||
dependenciesReader: dependenciesReader,
|
||||
dependencyProcessor: processor,
|
||||
fingerprintOverrideManager: overrideManager,
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: []
|
||||
)
|
||||
|
||||
try postbuild.performBuildCompletion()
|
||||
|
||||
XCTAssertTrue(fileManager.fileExists(atPath: swiftSwiftHeaderOverride.path))
|
||||
}
|
||||
|
||||
func testDecoratesPublicSwiftHeaderWithEmptyModulesFolderPath() throws {
|
||||
let dir = try prepareTempDir()
|
||||
let productsDir = dir
|
||||
.appendingPathComponent("MyModule.framework")
|
||||
.appendingPathComponent("Headers")
|
||||
let swiftSwiftHeader = productsDir
|
||||
.appendingPathComponent("MyModule-Swift.h")
|
||||
let swiftSwiftHeaderOverride = swiftSwiftHeader
|
||||
.appendingPathExtension("md5")
|
||||
|
||||
try fileManager.spt_createEmptyDir(productsDir)
|
||||
try fileManager.spt_createEmptyFile(swiftSwiftHeader)
|
||||
postbuildContext.moduleName = "MyModule"
|
||||
postbuildContext.publicHeadersFolderPath = productsDir
|
||||
let postbuild = Postbuild(
|
||||
context: postbuildContext,
|
||||
networkClient: network,
|
||||
remapper: remapper,
|
||||
fingerprintAccumulator: fingerprintGenerator,
|
||||
artifactsOrganizer: organizer,
|
||||
artifactCreator: artifactCreator,
|
||||
fingerprintSyncer: syncer,
|
||||
dependenciesReader: dependenciesReader,
|
||||
dependencyProcessor: processor,
|
||||
fingerprintOverrideManager: overrideManager,
|
||||
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
|
||||
modeController: modeController,
|
||||
metaReader: metaReader,
|
||||
metaWriter: metaWriter,
|
||||
creatorPlugins: [],
|
||||
consumerPlugins: []
|
||||
)
|
||||
|
||||
try postbuild.performBuildCompletion()
|
||||
|
||||
XCTAssertTrue(fileManager.fileExists(atPath: swiftSwiftHeaderOverride.path))
|
||||
}
|
||||
}
|
||||
// swiftlint:disable:next file_length
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) 2021 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
@testable import XCRemoteCache
|
||||
import XCTest
|
||||
|
||||
class 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"),
|
||||
lipo: binariesDir.appendingPathComponent("lipo"),
|
||||
ld: binariesDir.appendingPathComponent("xcld"),
|
||||
ldplusplus: binariesDir.appendingPathComponent("xcldplusplus"),
|
||||
prebuild: binariesDir.appendingPathComponent("xcprebuild"),
|
||||
postbuild: binariesDir.appendingPathComponent("xcpostbuild")
|
||||
)
|
||||
}
|
||||
|
||||
func testProducerSettingFakeSrcRoot() throws {
|
||||
let mode: Mode = .producer
|
||||
let fakeRootURL: URL = "/xxxxxxxxxxP"
|
||||
let appender = XcodeProjBuildSettingsIntegrateAppender(mode: mode, repoRoot: rootURL, fakeSrcRoot: fakeRootURL)
|
||||
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
|
||||
let resultURL = try XCTUnwrap(result["XCRC_FAKE_SRCROOT"] as? String)
|
||||
|
||||
XCTAssertEqual(resultURL, fakeRootURL.path)
|
||||
}
|
||||
|
||||
func testConsumerSettingFakeSrcRoot() throws {
|
||||
let mode: Mode = .consumer
|
||||
let fakeRootURL: URL = "/xxxxxxxxxxC"
|
||||
let appender = XcodeProjBuildSettingsIntegrateAppender(mode: mode, repoRoot: rootURL, fakeSrcRoot: fakeRootURL)
|
||||
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
|
||||
let resultURL: String = try XCTUnwrap(result["XCRC_FAKE_SRCROOT"] as? String)
|
||||
|
||||
XCTAssertEqual(resultURL, fakeRootURL.path)
|
||||
}
|
||||
|
||||
func testConsumerSettingLdPlusPlus() 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 ldPlusPlus: String = try XCTUnwrap(result["LDPLUSPLUS"] as? String)
|
||||
|
||||
XCTAssertEqual(ldPlusPlus, binaries.ldplusplus.path)
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,9 @@ class SwiftcTests: FileXCTestCase {
|
||||
)
|
||||
context = try SwiftcContext(config: config, input: input)
|
||||
markerWriter = MarkerWriterSpy()
|
||||
productsGenerator = SwiftcProductsGeneratorSpy()
|
||||
productsGenerator = SwiftcProductsGeneratorSpy(
|
||||
generatedDestination: SwiftcProductsGeneratorOutput(swiftmoduleDir: "", objcHeaderFile: "")
|
||||
)
|
||||
let dependenciesWriterSpy = DependenciesWriterSpy()
|
||||
self.dependenciesWriterSpy = dependenciesWriterSpy
|
||||
dependenciesWriterFactory = { [dependenciesWriterSpy] _, _ in dependenciesWriterSpy }
|
||||
@@ -283,6 +285,12 @@ class SwiftcTests: FileXCTestCase {
|
||||
let artifactSwiftInterfaceInfo = URL(
|
||||
fileURLWithPath: "/cachedArtifact/swiftmodule/archTest/Target.swiftinterface"
|
||||
)
|
||||
let artifactPrivateSwiftInterfaceInfo = URL(
|
||||
fileURLWithPath: "/cachedArtifact/swiftmodule/archTest/Target.private.swiftinterface"
|
||||
)
|
||||
let artifactAbiJsonInfo = URL(
|
||||
fileURLWithPath: "/cachedArtifact/swiftmodule/archTest/Target.abi.json"
|
||||
)
|
||||
|
||||
artifactOrganizer = ArtifactOrganizerFake(artifactRoot: artifactRoot)
|
||||
let swiftc = Swiftc(
|
||||
@@ -303,17 +311,14 @@ class SwiftcTests: FileXCTestCase {
|
||||
_ = try swiftc.mockCompilation()
|
||||
|
||||
let swiftModuleFiles = try productsGenerator.generated.first.unwrap()
|
||||
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)
|
||||
XCTAssertEqual(swiftModuleFiles.0[.swiftmodule], artifactSwiftmodule)
|
||||
XCTAssertEqual(swiftModuleFiles.0[.swiftdoc], artifactSwiftdoc)
|
||||
XCTAssertEqual(swiftModuleFiles.0[.swiftsourceinfo], artifactSwiftSourceInfo)
|
||||
XCTAssertEqual(swiftModuleFiles.0[.swiftinterface], artifactSwiftInterfaceInfo)
|
||||
XCTAssertEqual(swiftModuleFiles.0[.privateSwiftinterface], artifactPrivateSwiftInterfaceInfo)
|
||||
XCTAssertEqual(swiftModuleFiles.0[.abiJson], artifactAbiJsonInfo)
|
||||
XCTAssertEqual(swiftModuleFiles.1, artifactObjCHeader)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -47,7 +47,8 @@ class TemplateBasedCCWrapperBuilderTests: FileXCTestCase {
|
||||
let app = appDir.appendingPathComponent("xccc")
|
||||
try? fileManager.removeItem(at: appDir)
|
||||
try? FileManager.default.createDirectory(at: appDir, withIntermediateDirectories: true, attributes: nil)
|
||||
try? builder.compile(to: app, commitSha: commitSha)
|
||||
// swiftlint:disable:next force_try
|
||||
try! builder.compile(to: app, commitSha: commitSha)
|
||||
return app
|
||||
}()
|
||||
|
||||
@@ -332,11 +333,42 @@ class TemplateBasedCCWrapperBuilderTests: FileXCTestCase {
|
||||
XCTAssertNotEqual(newFileOutputData, Data())
|
||||
}
|
||||
|
||||
func testPCHObjCCompilationFallbacks() throws {
|
||||
// Marker is empty to mimic the new file scenario
|
||||
let pchFile = directory.appendingPathComponent("input.pch")
|
||||
createValidPCHFile(pchFile)
|
||||
arguments = ["-x", "objective-c-header", "-MF", dependencyFile.path, "-o", outputFile.path, pchFile.path]
|
||||
|
||||
try shellExec(Self.xccc.path, args: arguments, inDir: directory.path)
|
||||
|
||||
XCTAssertTrue(fileManager.fileExists(atPath: outputFile.path))
|
||||
}
|
||||
|
||||
func testPCHCCompilationFallbacks() throws {
|
||||
// Marker is empty to mimic the new file scenario
|
||||
let pchFile = directory.appendingPathComponent("input.pch")
|
||||
createValidPCHFile(pchFile)
|
||||
arguments = ["-x", "c-header", "-MF", dependencyFile.path, "-o", outputFile.path, pchFile.path]
|
||||
|
||||
try shellExec(Self.xccc.path, args: arguments, inDir: directory.path)
|
||||
|
||||
XCTAssertTrue(fileManager.fileExists(atPath: outputFile.path))
|
||||
}
|
||||
|
||||
/// Creates a simple C code in the location
|
||||
private func createValidCFile(_ location: URL) {
|
||||
fileManager.createFile(atPath: location.path, contents: "int main(){}".data(using: .utf8), attributes: nil)
|
||||
}
|
||||
|
||||
/// Creates a simple PCH code in the location
|
||||
private func createValidPCHFile(_ location: URL) {
|
||||
fileManager.createFile(
|
||||
atPath: location.path,
|
||||
contents: "#import <Availability.h>".data(using: .utf8),
|
||||
attributes: nil
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a C code that requires extra CUSTOM_STR clang macro to compile
|
||||
private func createInvalidCFile(_ location: URL) {
|
||||
fileManager.createFile(
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) 2022 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
@testable import XCRemoteCache
|
||||
import XCTest
|
||||
|
||||
class XCRemoteCacheConfigReaderTests: XCTestCase {
|
||||
|
||||
private var fileReader: FileAccessorFake!
|
||||
private var reader: XCRemoteCacheConfigReader!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
fileReader = FileAccessorFake(mode: .normal)
|
||||
reader = XCRemoteCacheConfigReader(srcRootPath: "/", fileReader: fileReader)
|
||||
}
|
||||
|
||||
func testReadsFromExtraConfig() throws {
|
||||
let contents = [
|
||||
"cache_addresses: [test]",
|
||||
"retry_delay: 30",
|
||||
"upload_batch_size: 5",
|
||||
].joined(separator: "\n").data(using: .utf8)
|
||||
try fileReader.write(toPath: "/.rcinfo", contents: contents)
|
||||
|
||||
let config = try reader.readConfiguration()
|
||||
|
||||
XCTAssertEqual(config.cacheAddresses, ["test"])
|
||||
XCTAssertEqual(config.retryDelay, 30)
|
||||
XCTAssertEqual(config.uploadBatchSize, 5)
|
||||
}
|
||||
|
||||
func testOverridesExtraConfigFromExtraFile() throws {
|
||||
try fileReader.write(toPath: "/.rcinfo", contents: "cache_addresses: [test]")
|
||||
try fileReader.write(toPath: "/user.rcinfo", contents: "cache_addresses: [user]")
|
||||
|
||||
let config = try reader.readConfiguration()
|
||||
|
||||
XCTAssertEqual(config.cacheAddresses, ["user"])
|
||||
}
|
||||
|
||||
func testReadsExtraConfigMultipleTimes() throws {
|
||||
try fileReader.write(toPath: "/.rcinfo", contents: "cache_addresses: [test]")
|
||||
try fileReader.write(toPath: "/user.rcinfo", contents: """
|
||||
cache_addresses: [user]
|
||||
extra_configuration_file: user2.rcinfo
|
||||
""")
|
||||
try fileReader.write(toPath: "/user2.rcinfo", contents: "cache_addresses: [user2]")
|
||||
|
||||
let config = try reader.readConfiguration()
|
||||
|
||||
XCTAssertEqual(config.cacheAddresses, ["user2"])
|
||||
}
|
||||
|
||||
func testBreaksImportingExtraConfigIfReachingALoop() throws {
|
||||
try fileReader.write(toPath: "/.rcinfo", contents: "cache_addresses: [test]")
|
||||
try fileReader.write(toPath: "/user.rcinfo", contents: """
|
||||
cache_addresses: [user]
|
||||
extra_configuration_file: .rcinfo
|
||||
""")
|
||||
|
||||
let config = try reader.readConfiguration()
|
||||
|
||||
XCTAssertEqual(config.cacheAddresses, ["user"])
|
||||
}
|
||||
|
||||
func testBreaksImportingExtraConfigIfFileDoesntExist() throws {
|
||||
try fileReader.write(toPath: "/.rcinfo", contents: "cache_addresses: [test]")
|
||||
try fileReader.write(toPath: "/user.rcinfo", contents: """
|
||||
cache_addresses: [user]
|
||||
extra_configuration_file: nonexisting.rcinfo
|
||||
""")
|
||||
|
||||
let config = try reader.readConfiguration()
|
||||
|
||||
XCTAssertEqual(config.cacheAddresses, ["user"])
|
||||
XCTAssertEqual(config.extraConfigurationFile, "nonexisting.rcinfo")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
// Copyright (c) 2021 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
@testable import XCRemoteCache
|
||||
|
||||
import XCTest
|
||||
|
||||
class DependenciesReaderPerformanceTest: XCTestCase {
|
||||
|
||||
private static let resourcesSubdirectory = "TestData/Dependencies/DependenciesReaderPerformanceTest"
|
||||
|
||||
private func pathForTestData(name: String) throws -> URL {
|
||||
return try XCTUnwrap(Bundle.module.url(
|
||||
forResource: name,
|
||||
withExtension: "d",
|
||||
subdirectory: DependenciesReaderPerformanceTest.resourcesSubdirectory
|
||||
))
|
||||
}
|
||||
|
||||
func testFindDependenciesPerformance() throws {
|
||||
let file = try pathForTestData(name: "dependencies")
|
||||
let reader = FileDependenciesReader(file, accessor: FileManager.default)
|
||||
|
||||
self.measure { // 0.005
|
||||
do {
|
||||
_ = try reader.findDependencies()
|
||||
} catch {
|
||||
print("Error reading dependencies")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testReadRawFilePerformance() throws {
|
||||
let file = try pathForTestData(name: "dependencies")
|
||||
let reader = FileDependenciesReader(file, accessor: FileManager.default)
|
||||
|
||||
self.measure { // 0.002
|
||||
do {
|
||||
_ = try reader.readRaw()
|
||||
} catch {
|
||||
print("Error reading dependencies")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testGetFileDataPerformance() throws {
|
||||
let file = try pathForTestData(name: "dependencies")
|
||||
let reader = FileDependenciesReader(file, accessor: FileManager.default)
|
||||
|
||||
self.measure { // 0.00008
|
||||
do {
|
||||
_ = try reader.getFileData()
|
||||
} catch {
|
||||
print("Error reading dependencies")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testGetFileStringPerformance() throws {
|
||||
let file = try pathForTestData(name: "dependencies")
|
||||
let reader = FileDependenciesReader(file, accessor: FileManager.default)
|
||||
let fileData = try reader.getFileData()
|
||||
|
||||
self.measure { // 0.00002
|
||||
do {
|
||||
_ = try reader.getFileStringFromData(fileData: fileData)
|
||||
} catch {
|
||||
print("Error reading dependencies")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testGetYamlPerformance() throws { // 0.222
|
||||
let file = try pathForTestData(name: "dependencies")
|
||||
let reader = FileDependenciesReader(file, accessor: FileManager.default)
|
||||
let fileData = try reader.getFileData()
|
||||
let fileString = try reader.getFileStringFromData(fileData: fileData)
|
||||
|
||||
self.measure { // 0.0022
|
||||
do {
|
||||
_ = try reader.getYaml(fileString: fileString)
|
||||
} catch {
|
||||
print("Error reading dependencies")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testParseDependencyFileListUsingUTF8View() throws {
|
||||
let file = try pathForTestData(name: "dependencies")
|
||||
let reader = FileDependenciesReader(file, accessor: FileManager.default)
|
||||
let fileData = try reader.getFileData()
|
||||
let fileString = try reader.getFileStringFromData(fileData: fileData)
|
||||
let yaml = try reader.getYaml(fileString: fileString)
|
||||
|
||||
guard let dependencies = yaml["dependencies"] else {
|
||||
XCTAssertTrue(false)
|
||||
return
|
||||
}
|
||||
|
||||
self.measure { // 0.004
|
||||
let deps = reader.parseDependencyFileList(dependencies)
|
||||
XCTAssertTrue(deps.count == 1000)
|
||||
}
|
||||
}
|
||||
|
||||
func testDeprecatedParseDependenciesFilesListOfAnObjectUsingUTF8View() throws {
|
||||
let file = try pathForTestData(name: "dependencies")
|
||||
let reader = FileDependenciesReader(file, accessor: FileManager.default)
|
||||
let fileData = try reader.getFileData()
|
||||
let fileString = try reader.getFileStringFromData(fileData: fileData)
|
||||
let yaml = try reader.getYaml(fileString: fileString)
|
||||
|
||||
guard let dependencies = yaml["/This/Is/A/Path/To/Some/Object/objectfile.o"] else {
|
||||
XCTAssertTrue(false)
|
||||
return
|
||||
}
|
||||
|
||||
self.measure { // 0.00048
|
||||
let deps = reader.parseDependencyFileList(dependencies)
|
||||
XCTAssertTrue(deps.count == 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,8 +81,18 @@ class DependenciesRemapperCompositeTests: XCTestCase {
|
||||
|
||||
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"]
|
||||
|
||||
@@ -93,8 +103,18 @@ class DependenciesRemapperCompositeTests: XCTestCase {
|
||||
|
||||
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"]
|
||||
|
||||
@@ -105,8 +125,18 @@ class DependenciesRemapperCompositeTests: XCTestCase {
|
||||
|
||||
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"]
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) 2021 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
@testable import XCRemoteCache
|
||||
import XCTest
|
||||
|
||||
class FileDependenciesWriterTests: XCTestCase {
|
||||
|
||||
private func generateTempFileURL(name: String = #function) throws -> URL {
|
||||
let directory = NSTemporaryDirectory()
|
||||
return try NSURL.fileURL(withPathComponents: [directory, name]).unwrap()
|
||||
}
|
||||
|
||||
func testWriteDependencyWithSpace() throws {
|
||||
let url = try generateTempFileURL()
|
||||
let writer = FileDependenciesWriter(url, accessor: FileManager.default)
|
||||
|
||||
try writer.writeGeneric(dependencies: [
|
||||
"/SomePath/Pods/Target Support Files/lottie-ios/lottie-ios-dummy.m",
|
||||
])
|
||||
|
||||
let expectedContent = """
|
||||
dependencies: /SomePath/Pods/Target\\ Support\\ Files/lottie-ios/lottie-ios-dummy.m
|
||||
|
||||
"""
|
||||
|
||||
let content = String(data: try Data(contentsOf: url), encoding: .utf8)
|
||||
|
||||
XCTAssertEqual(content, expectedContent)
|
||||
}
|
||||
}
|
||||
@@ -27,17 +27,21 @@ class DependencyProcessorImplTests: FileXCTestCase {
|
||||
product: "/Product",
|
||||
source: "/Source",
|
||||
intermediate: "/Intermediate",
|
||||
bundle: "/Bundle"
|
||||
derivedFiles: "/DerivedFiles",
|
||||
bundle: "/Bundle",
|
||||
skippedRegexes: []
|
||||
)
|
||||
|
||||
func testIntermediateFileIsSkippedForProductAndSourceSubdirectory() {
|
||||
func testIntermediateFileIsskippedRegexesForProductAndSourceSubdirectory() {
|
||||
let intermediateFile: URL = "/Intermediate/some"
|
||||
let processor = DependencyProcessorImpl(
|
||||
xcode: "/Xcode",
|
||||
product: "/",
|
||||
source: "/",
|
||||
intermediate: "/Intermediate",
|
||||
bundle: nil
|
||||
derivedFiles: "/DerivedFiles",
|
||||
bundle: nil,
|
||||
skippedRegexes: []
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
@@ -46,14 +50,16 @@ class DependencyProcessorImplTests: FileXCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
func testBundleFileIsSkippedForProductAndSourceSubdirectory() {
|
||||
func testBundleFileIsskippedRegexesForProductAndSourceSubdirectory() {
|
||||
let bundleFile: URL = "/Bundle/some"
|
||||
let processor = DependencyProcessorImpl(
|
||||
xcode: "/Xcode",
|
||||
product: "/",
|
||||
source: "/",
|
||||
intermediate: "/Intermediate",
|
||||
bundle: "/Bundle"
|
||||
derivedFiles: "/DerivedFiles",
|
||||
bundle: "/Bundle",
|
||||
skippedRegexes: []
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
@@ -62,6 +68,22 @@ class DependencyProcessorImplTests: FileXCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
func testFiltersOutGeneratedSwiftHeaders() throws {
|
||||
let dependencies = processor.process([
|
||||
"/DerivedFiles/ModuleName-Swift.h",
|
||||
])
|
||||
|
||||
XCTAssertEqual(dependencies, [])
|
||||
}
|
||||
|
||||
func testFiltersOutDerivedFile() throws {
|
||||
let dependencies = processor.process([
|
||||
"/DerivedFiles/output.h",
|
||||
])
|
||||
|
||||
XCTAssertEqual(dependencies, [])
|
||||
}
|
||||
|
||||
func testFiltersOutProductModulemap() throws {
|
||||
let dependencies = processor.process([
|
||||
"/Product/some.modulemap",
|
||||
@@ -130,13 +152,19 @@ class DependencyProcessorImplTests: FileXCTestCase {
|
||||
product: "/Product",
|
||||
source: "/Source",
|
||||
intermediate: intermediateDirReal,
|
||||
bundle: "/Bundle"
|
||||
derivedFiles: "/DerivedFiles",
|
||||
bundle: "/Bundle",
|
||||
skippedRegexes: []
|
||||
)
|
||||
|
||||
let intermediateFileSymlink = createSymlink(filename: someFilename, sourceDir: symlink, destinationDir: intermediateDirReal)
|
||||
let intermediateFileSymlink = createSymlink(
|
||||
filename: someFilename,
|
||||
sourceDir: symlink,
|
||||
destinationDir: intermediateDirReal
|
||||
)
|
||||
|
||||
let dependencies = processor.process([
|
||||
intermediateFileSymlink
|
||||
intermediateFileSymlink,
|
||||
])
|
||||
|
||||
XCTAssertEqual(dependencies, [])
|
||||
@@ -154,13 +182,19 @@ class DependencyProcessorImplTests: FileXCTestCase {
|
||||
product: "/Product",
|
||||
source: sourceDirReal,
|
||||
intermediate: "/Intermediate",
|
||||
bundle: "/Bundle"
|
||||
derivedFiles: "/DerivedFiles",
|
||||
bundle: "/Bundle",
|
||||
skippedRegexes: []
|
||||
)
|
||||
|
||||
let sourceFileSymlink = createSymlink(filename: someFilename, sourceDir: symlink, destinationDir: sourceDirReal)
|
||||
let sourceFileSymlink = createSymlink(
|
||||
filename: someFilename,
|
||||
sourceDir: symlink,
|
||||
destinationDir: sourceDirReal
|
||||
)
|
||||
|
||||
let dependencies = processor.process([
|
||||
sourceFileSymlink
|
||||
sourceFileSymlink,
|
||||
])
|
||||
|
||||
XCTAssertEqual(dependencies, [.init(url: sourceFileSymlink, type: .source)])
|
||||
@@ -173,10 +207,84 @@ class DependencyProcessorImplTests: FileXCTestCase {
|
||||
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_forceSymbolicLink(
|
||||
at: sourceDir,
|
||||
withDestinationURL: destinationDir
|
||||
))
|
||||
XCTAssertNoThrow(try fileMng.spt_createEmptyFile(destinationDir.appendingPathComponent(filename)))
|
||||
|
||||
return sourceDir.appendingPathComponent(filename)
|
||||
}
|
||||
|
||||
func testSkipsCustomizedDerivedDirFileUnderSources() {
|
||||
let derivedFile: URL = "/DerivedFiles/Module-Swift.h"
|
||||
let processor = DependencyProcessorImpl(
|
||||
xcode: "/Xcode",
|
||||
product: "/",
|
||||
source: "/",
|
||||
intermediate: "/Intermediate",
|
||||
derivedFiles: "/DerivedFiles",
|
||||
bundle: nil,
|
||||
skippedRegexes: []
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
processor.process([derivedFile]),
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
||||
func testSkippsFilesWithFullMatch() {
|
||||
let source: URL = "/someFile.m"
|
||||
let processor = DependencyProcessorImpl(
|
||||
xcode: "/Xcode",
|
||||
product: "/",
|
||||
source: "/",
|
||||
intermediate: "/Intermediate",
|
||||
derivedFiles: "/DerivedFiles",
|
||||
bundle: nil,
|
||||
skippedRegexes: ["/someFile\\.m"]
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
processor.process([source]),
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
||||
func testSkippsFilesWithPartialMatch() {
|
||||
let derivedModulemap: URL = "/module.modulemap"
|
||||
let processor = DependencyProcessorImpl(
|
||||
xcode: "/Xcode",
|
||||
product: "/product",
|
||||
source: "/",
|
||||
intermediate: "/Intermediate",
|
||||
derivedFiles: "/DerivedFiles",
|
||||
bundle: nil,
|
||||
skippedRegexes: ["\\.modulemap$"]
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
processor.process([derivedModulemap]),
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
||||
func testDoesntSkipFileIfInvalidRegex() {
|
||||
let source: URL = "/someFile.m"
|
||||
let processor = DependencyProcessorImpl(
|
||||
xcode: "/Xcode",
|
||||
product: "/product",
|
||||
source: "/",
|
||||
intermediate: "/Intermediate",
|
||||
derivedFiles: "/DerivedFiles",
|
||||
bundle: nil,
|
||||
skippedRegexes: ["\\"]
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
processor.process([source]),
|
||||
[.init(url: source, type: .source)]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,4 +74,51 @@ class FileFingerprintSyncerTests: FileXCTestCase {
|
||||
|
||||
XCTAssertTrue(fileManager.fileExists(atPath: nonOverrideFile.path))
|
||||
}
|
||||
|
||||
func testDecoratesFile() throws {
|
||||
let header = swiftmoduleDir.appendingPathComponent("Module-Swift.h")
|
||||
let headerOverride = swiftmoduleDir.appendingPathComponent("Module-Swift.h.md5")
|
||||
try fileManager.spt_createEmptyFile(header)
|
||||
|
||||
|
||||
try syncer.decorate(file: header, fingerprint: "1")
|
||||
|
||||
XCTAssertEqual(try String(contentsOf: headerOverride), "1")
|
||||
}
|
||||
|
||||
func testFileDecorateOverridesPreviousOverlay() throws {
|
||||
let header = swiftmoduleDir.appendingPathComponent("Module-Swift.h")
|
||||
let headerOverride = swiftmoduleDir.appendingPathComponent("Module-Swift.h.md5")
|
||||
try fileManager.spt_createEmptyFile(header)
|
||||
try "1".write(to: headerOverride, atomically: true, encoding: .utf8)
|
||||
|
||||
try syncer.decorate(file: header, fingerprint: "2")
|
||||
|
||||
XCTAssertEqual(try String(contentsOf: headerOverride), "2")
|
||||
}
|
||||
|
||||
func testDeletesFileOverride() throws {
|
||||
let header = swiftmoduleDir.appendingPathComponent("Module-Swift.h")
|
||||
let headerOverride = swiftmoduleDir.appendingPathComponent("Module-Swift.h.md5")
|
||||
try fileManager.spt_createEmptyFile(header)
|
||||
try fileManager.spt_createEmptyFile(headerOverride)
|
||||
|
||||
|
||||
try syncer.delete(file: header)
|
||||
|
||||
XCTAssertFalse(fileManager.fileExists(atPath: headerOverride.path))
|
||||
}
|
||||
|
||||
func testDeletesDoesntDeleteWhenFileIsMissing() throws {
|
||||
let nonExistingFile = swiftmoduleDir.appendingPathComponent("Module-Swift.h")
|
||||
|
||||
XCTAssertNoThrow(try syncer.delete(file: nonExistingFile))
|
||||
}
|
||||
|
||||
func testDeletesDoesntDeleteWhenOverrideIsMissing() throws {
|
||||
let header = swiftmoduleDir.appendingPathComponent("Module-Swift.h")
|
||||
try fileManager.spt_createEmptyFile(header)
|
||||
|
||||
XCTAssertNoThrow(try syncer.delete(file: header))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,4 +68,3 @@ class OverlayDependenciesRemapperTests: XCTestCase {
|
||||
XCTAssertEqual(secondDependencies, ["/file.h"])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,8 +29,14 @@ class JsonOverlayReaderTests: FileXCTestCase {
|
||||
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")
|
||||
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))
|
||||
}
|
||||
@@ -79,6 +85,10 @@ class JsonOverlayReaderTests: FileXCTestCase {
|
||||
}
|
||||
|
||||
private func pathForTestData(name: String) throws -> URL {
|
||||
return try XCTUnwrap(Bundle.module.url(forResource: name, withExtension: "json", subdirectory: JsonOverlayReaderTests.resourcesSubdirectory))
|
||||
return try XCTUnwrap(Bundle.module.url(
|
||||
forResource: name,
|
||||
withExtension: "json",
|
||||
subdirectory: JsonOverlayReaderTests.resourcesSubdirectory
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ class PathDependenciesRemapperFactoryTests: XCTestCase {
|
||||
private var factory: PathDependenciesRemapperFactory!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
factory = PathDependenciesRemapperFactory()
|
||||
}
|
||||
|
||||
|
||||
@@ -71,4 +71,17 @@ class FileManagerDirScannerTests: FileXCTestCase {
|
||||
|
||||
try XCTAssertThrowsError(dirScanner.items(at: dir))
|
||||
}
|
||||
|
||||
func testFindsAllFilesRecursively() throws {
|
||||
let dir = workingDirectory!.appendingPathComponent("dir")
|
||||
let nestedDir = dir.appendingPathComponent("nested")
|
||||
let nestedFile = nestedDir.appendingPathComponent("file")
|
||||
try fileManager.spt_createEmptyFile(nestedFile)
|
||||
|
||||
let allFiles = try dirScanner.recursiveItems(at: dir)
|
||||
|
||||
// returned items may contain symbolic links in a path
|
||||
let allFilesResolve = allFiles.map { $0.resolvingSymlinksInPath() }
|
||||
XCTAssertEqual(allFilesResolve, [nestedFile])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,19 +33,23 @@ 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"
|
||||
|
||||
private var config: XCRemoteCacheConfig!
|
||||
private var generator: FingerprintAccumulator!
|
||||
private var generator: FingerprintAccumulator! {
|
||||
return generatorFake
|
||||
}
|
||||
|
||||
private var generatorFake: FingerprintAccumulatorFake!
|
||||
private var fingerprintGenerator: EnvironmentFingerprintGenerator!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
config = XCRemoteCacheConfig(sourceRoot: "")
|
||||
generator = FingerprintAccumulatorFake()
|
||||
generatorFake = FingerprintAccumulatorFake()
|
||||
fingerprintGenerator = EnvironmentFingerprintGenerator(
|
||||
configuration: config,
|
||||
env: Self.defaultENV,
|
||||
@@ -92,4 +96,11 @@ class EnvironmentFingerprintGeneratorTests: XCTestCase {
|
||||
|
||||
XCTAssertEqual(fingerprint, "GCC,YES,TARGET,CONG,PLAT,XC,1,2,3,4,AR,CUSTOM_VALUE,\(Self.currentVersion)")
|
||||
}
|
||||
|
||||
func testFingerprintIsGeneratedOnce() throws {
|
||||
let fingerprint1 = try fingerprintGenerator.generateFingerprint()
|
||||
let fingerprint2 = try fingerprintGenerator.generateFingerprint()
|
||||
XCTAssertEqual(fingerprint1, fingerprint2)
|
||||
XCTAssertEqual(generatorFake.generateCallsCount, 1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +35,12 @@ class AWSV4SignatureTest: XCTestCase {
|
||||
|
||||
let key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY"
|
||||
let accessKey = "AKIDEXAMPLE"
|
||||
let securityToken = "IQoJb3JpZ2luX2VjENv//////////wEaCXVzLWVhc3Q+bsHwqnovXtl/1JVe61XHMnAw3AIXwOAOxqMvhw=="
|
||||
|
||||
AWSV4Signature(
|
||||
secretKey: key,
|
||||
accessKey: accessKey,
|
||||
securityToken: securityToken,
|
||||
region: "us-east-1",
|
||||
service: "iam",
|
||||
date: Date(timeIntervalSince1970: 1_440_938_160)
|
||||
@@ -49,8 +51,8 @@ class AWSV4SignatureTest: XCTestCase {
|
||||
func testAuthHeaderContainsCorrectSignature() throws {
|
||||
XCTAssertEqual(
|
||||
"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, " +
|
||||
"SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date, " +
|
||||
"Signature=dd479fa8a80364edf2119ec24bebde66712ee9c9cb2b0d92eb3ab9ccdc0c3947",
|
||||
"SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date;x-amz-security-token, " +
|
||||
"Signature=d26ab974bba1b248f041ea1120064e1fa672d6f06cac2cff42b38acea87b76e5",
|
||||
request.allHTTPHeaderFields?["Authorization"]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -72,7 +72,13 @@ class NetworkClientImplTests: XCTestCase {
|
||||
configuration.protocolClasses = [URLProtocolStub.self]
|
||||
session = URLSession(configuration: configuration)
|
||||
fileManager = FileManager.default
|
||||
client = NetworkClientImpl(session: session, retries: 0, fileManager: fileManager, awsV4Signature: nil)
|
||||
client = NetworkClientImpl(
|
||||
session: session,
|
||||
retries: 0,
|
||||
retryDelay: 0,
|
||||
fileManager: fileManager,
|
||||
awsV4Signature: nil
|
||||
)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
@@ -87,7 +93,7 @@ class NetworkClientImplTests: XCTestCase {
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func waitForResponse<R>(_ action: (@escaping Completion<R>) -> Void) throws -> Result<R, NetworkClientError> {
|
||||
func waitForResponse<R>(_ action: (@escaping Completion<R>) -> Void, timeout: TimeInterval = 0.1) throws -> Result<R, NetworkClientError> {
|
||||
let responseExpectation = expectation(description: "RequestResponse")
|
||||
var receivedResponse: Result<R, NetworkClientError>?
|
||||
|
||||
@@ -95,7 +101,7 @@ class NetworkClientImplTests: XCTestCase {
|
||||
receivedResponse = response
|
||||
responseExpectation.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 0.1)
|
||||
waitForExpectations(timeout: timeout)
|
||||
return try receivedResponse.unwrap()
|
||||
}
|
||||
|
||||
@@ -141,9 +147,15 @@ class NetworkClientImplTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testUploadFilureWith400Retries() throws {
|
||||
client = NetworkClientImpl(session: session, retries: 2, fileManager: fileManager, awsV4Signature: nil)
|
||||
client = NetworkClientImpl(
|
||||
session: session,
|
||||
retries: 2,
|
||||
retryDelay: 0,
|
||||
fileManager: fileManager,
|
||||
awsV4Signature: nil
|
||||
)
|
||||
responses[url] = .success(failureResponse, Data())
|
||||
_ = try waitForResponse { client.upload(fileURL, as: url, completion: $0) }
|
||||
_ = try waitForResponse({ client.upload(fileURL, as: url, completion: $0) }, timeout: 0.5)
|
||||
|
||||
XCTAssertEqual(
|
||||
requests.map(\.url),
|
||||
@@ -153,13 +165,51 @@ class NetworkClientImplTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testUploadSuccessDoesntRetry() throws {
|
||||
client = NetworkClientImpl(session: session, retries: 0, fileManager: fileManager, awsV4Signature: nil)
|
||||
client = NetworkClientImpl(
|
||||
session: session,
|
||||
retries: 2,
|
||||
retryDelay: 0,
|
||||
fileManager: fileManager,
|
||||
awsV4Signature: nil
|
||||
)
|
||||
responses[url] = .success(successResponse, Data())
|
||||
_ = try waitForResponse { client.upload(fileURL, as: url, completion: $0) }
|
||||
|
||||
XCTAssertEqual(requests.map(\.url), [url], "Expected 1 request - original only")
|
||||
}
|
||||
|
||||
func testDownloadFilureWith400Retries() throws {
|
||||
client = NetworkClientImpl(
|
||||
session: session,
|
||||
retries: 2,
|
||||
retryDelay: 0,
|
||||
fileManager: fileManager,
|
||||
awsV4Signature: nil
|
||||
)
|
||||
responses[url] = nil
|
||||
_ = try waitForResponse({ client.download(url, to: fileURL, completion: $0) }, timeout: 0.5)
|
||||
|
||||
XCTAssertEqual(
|
||||
requests.map(\.url),
|
||||
Array(repeating: url, count: 3),
|
||||
"Expected 3 requests (original + 2 retries)"
|
||||
)
|
||||
}
|
||||
|
||||
func testDownloadSuccessDoesntRetry() throws {
|
||||
client = NetworkClientImpl(
|
||||
session: session,
|
||||
retries: 2,
|
||||
retryDelay: 0,
|
||||
fileManager: fileManager,
|
||||
awsV4Signature: nil
|
||||
)
|
||||
responses[url] = .success(successResponse, Data())
|
||||
_ = try waitForResponse { client.download(url, to: fileURL, completion: $0) }
|
||||
|
||||
XCTAssertEqual(requests.map(\.url), [url], "Expected 1 request - original only")
|
||||
}
|
||||
|
||||
func testFileExits400CompletesWithFalse() throws {
|
||||
responses[url] = .success(failureResponse, Data())
|
||||
let response = try waitForResponse { client.fileExists(url, completion: $0) }
|
||||
@@ -203,18 +253,25 @@ class NetworkClientImplTests: XCTestCase {
|
||||
let signature = AWSV4Signature(
|
||||
secretKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
|
||||
accessKey: "AKIDEXAMPLE",
|
||||
securityToken: "IQoJb3JpZ2luX2VjENv//////////wEaCXVzLWVhc3Q+bsHwqnovXtl/1JVe61XHMnAw3AIXwOAOxqMvhw==",
|
||||
region: "us-east-1",
|
||||
service: "iam",
|
||||
date: Date(timeIntervalSince1970: 1_440_938_160)
|
||||
)
|
||||
client = NetworkClientImpl(session: session, retries: 0, fileManager: fileManager, awsV4Signature: signature)
|
||||
client = NetworkClientImpl(
|
||||
session: session,
|
||||
retries: 0,
|
||||
retryDelay: 0,
|
||||
fileManager: fileManager,
|
||||
awsV4Signature: signature
|
||||
)
|
||||
responses[url] = .success(successResponse, Data())
|
||||
_ = try waitForResponse { client.fetch(url, completion: $0) }
|
||||
|
||||
XCTAssertEqual(
|
||||
"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, " +
|
||||
"SignedHeaders=host;x-amz-content-sha256;x-amz-date, " +
|
||||
"Signature=acdb223475463b6ce711b063f199387a7a12bd638e4dccaaeb5efcbf159b6454",
|
||||
"SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-security-token, " +
|
||||
"Signature=e5578464567fb97fd26e871702e4ec4ff7d61cb87eb72a40d22b80e12da30c34",
|
||||
try requests[0].allHTTPHeaderFields?["Authorization"].unwrap()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -39,7 +39,12 @@ class ReplicatedRemotesNetworkClientTests: XCTestCase {
|
||||
uploadURLs = try [URL(string: "http://upload1.com").unwrap(), URL(string: "http://upload2.com").unwrap()]
|
||||
download = URLBuilderFake(downloadURL)
|
||||
uploads = uploadURLs.map(URLBuilderFake.init)
|
||||
client = ReplicatedRemotesNetworkClient(networkClient, download: download, uploads: uploads)
|
||||
client = ReplicatedRemotesNetworkClient(
|
||||
networkClient,
|
||||
download: download,
|
||||
uploads: uploads,
|
||||
uploadBatchSize: 1
|
||||
)
|
||||
}
|
||||
|
||||
private func prepareLocalEmptyFile() throws -> URL {
|
||||
@@ -62,6 +67,30 @@ class ReplicatedRemotesNetworkClientTests: XCTestCase {
|
||||
XCTAssertTrue(try networkClient.fileExistsSynchronously(expectedArtifact2))
|
||||
}
|
||||
|
||||
func testUploadsWithLimit() throws {
|
||||
var expectedArtifacts = [URL]()
|
||||
var uploadURLs = [URL]()
|
||||
for index in 0...99 {
|
||||
let expectedArtifact = try URL(string: "http://upload\(index).com/file/id1").unwrap()
|
||||
expectedArtifacts.append(expectedArtifact)
|
||||
let uploadURL = try URL(string: "http://upload\(index).com").unwrap()
|
||||
uploadURLs.append(uploadURL)
|
||||
}
|
||||
uploads = uploadURLs.map(URLBuilderFake.init)
|
||||
client = ReplicatedRemotesNetworkClient(
|
||||
networkClient,
|
||||
download: download,
|
||||
uploads: uploads,
|
||||
uploadBatchSize: 10
|
||||
)
|
||||
|
||||
try client.uploadSynchronously(localSampleFile, as: .artifact(id: "id1"))
|
||||
|
||||
for expectedArtifact in expectedArtifacts {
|
||||
XCTAssertTrue(try networkClient.fileExistsSynchronously(expectedArtifact))
|
||||
}
|
||||
}
|
||||
|
||||
func testCreatesInAllStreams() throws {
|
||||
let expectedMeta1 = try URL(string: "http://upload1.com/meta/commit_id").unwrap()
|
||||
let expectedMeta2 = try URL(string: "http://upload2.com/meta/commit_id").unwrap()
|
||||
@@ -71,4 +100,5 @@ class ReplicatedRemotesNetworkClientTests: XCTestCase {
|
||||
XCTAssertTrue(try networkClient.fileExistsSynchronously(expectedMeta1))
|
||||
XCTAssertTrue(try networkClient.fileExistsSynchronously(expectedMeta2))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1002
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user