Compare commits
165 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| e6b56024b9 | |||
| 8c34a31110 | |||
| f7c32d6e80 | |||
| b3a16ae5d0 | |||
| d013fe4c81 | |||
| 4aefee078e |
@@ -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": "c75c3acc25460195cfd203a04dde165395bf00e0",
|
||||
"version": "8.7.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -51,8 +60,8 @@
|
||||
"repositoryURL": "https://github.com/jpsim/Yams.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "53741ba55ecca5c7149d8c9f810913ec80845c69",
|
||||
"version": "3.0.0"
|
||||
"revision": "00c403debcd0a007b854bb35e598466207a2d58c",
|
||||
"version": "5.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -60,8 +69,8 @@
|
||||
"repositoryURL": "https://github.com/marmelroy/Zip.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "80b1c3005ee25b4c7ce46c4029ac3347e8d5e37e",
|
||||
"version": "2.0.0"
|
||||
"revision": "67fa55813b9e7b3b9acee9c0ae501def28746d76",
|
||||
"version": "2.1.2"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
+19
-5
@@ -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.7.1"),
|
||||
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
@@ -31,9 +33,13 @@ let package = Package(
|
||||
dependencies: ["XCRemoteCache"]
|
||||
),
|
||||
.target(
|
||||
name: "xclibtool",
|
||||
name: "xclibtoolSupport",
|
||||
dependencies: ["XCRemoteCache"]
|
||||
),
|
||||
.target(
|
||||
name: "xclibtool",
|
||||
dependencies: ["XCRemoteCache", "xclibtoolSupport"]
|
||||
),
|
||||
.target(
|
||||
name: "xcpostbuild",
|
||||
dependencies: ["XCRemoteCache"]
|
||||
@@ -49,15 +55,23 @@ let package = Package(
|
||||
name: "xcld",
|
||||
dependencies: ["XCRemoteCache"]
|
||||
),
|
||||
.target(
|
||||
name: "xcldplusplus",
|
||||
dependencies: ["XCRemoteCache"]
|
||||
),
|
||||
.target(
|
||||
// Wrapper target that builds all binaries but does nothing in runtime
|
||||
name: "Aggregator",
|
||||
dependencies: ["xcprebuild", "xcswiftc", "xclibtool", "xcpostbuild", "xcprepare", "xcld"]
|
||||
dependencies: ["xcprebuild", "xcswiftc", "xclibtool", "xcpostbuild", "xcprepare", "xcld", "xcldplusplus"]
|
||||
),
|
||||
.testTarget(
|
||||
name: "XCRemoteCacheTests",
|
||||
dependencies: ["XCRemoteCache"],
|
||||
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)
|
||||
@@ -195,7 +196,9 @@ Configure Xcode targets that **should use** XCRemoteCache:
|
||||
* `SWIFT_EXEC` - location of `xcprepare` (e.g. `xcremotecache/xcswiftc`)
|
||||
* `LIBTOOL` - location of `xclibtool` (e.g. `xcremotecache/xclibtool`)
|
||||
* `LD` - location of `xcld` (e.g. `xcremotecache/xcld`)
|
||||
* `LDPLUSPLUS` - location of `xcldplusplus` (e.g. `xcremotecache/xcldplusplus`)
|
||||
* `XCRC_PLATFORM_PREFERRED_ARCH` - `$(LINK_FILE_LIST_$(CURRENT_VARIANT)_$(PLATFORM_PREFERRED_ARCH):dir:standardizepath:file:default=arm64)`
|
||||
* `SWIFT_USE_INTEGRATED_DRIVER` - `NO` (required in Xcode 14.0+)
|
||||
|
||||
<details>
|
||||
<summary>Screenshot</summary>
|
||||
@@ -264,7 +267,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` 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 +326,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,11 +348,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
|
||||
|
||||
@@ -350,6 +393,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
|
||||
...
|
||||
@@ -403,12 +448,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']
|
||||
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,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -52,7 +52,7 @@ class XCLibtoolCreateUniversalBinary: XCLibtoolLogic {
|
||||
let config: XCRemoteCacheConfig
|
||||
do {
|
||||
let srcRoot: URL = URL(fileURLWithPath: fileManager.currentDirectoryPath)
|
||||
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileManager: fileManager)
|
||||
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileReader: fileManager)
|
||||
.readConfiguration()
|
||||
} catch {
|
||||
errorLog("Libtool initialization failed with error: \(error). Fallbacking to libtool")
|
||||
|
||||
+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
|
||||
@@ -119,7 +127,7 @@ extension PostbuildContext {
|
||||
dSYMPath = try env.readEnv(key: "DWARF_DSYM_FOLDER_PATH")
|
||||
.appendingPathComponent(env.readEnv(key: "DWARF_DSYM_FILE_NAME"))
|
||||
builtProductsDir = try env.readEnv(key: "BUILT_PRODUCTS_DIR")
|
||||
if let contentsFolderPath = env.readEnv(key: "CONTENTS_FOLDER_PATH") {
|
||||
if let contentsFolderPath: String = env.readEnv(key: "CONTENTS_FOLDER_PATH") {
|
||||
bundleDir = productsDir.appendingPathComponent(contentsFolderPath)
|
||||
} else {
|
||||
bundleDir = nil
|
||||
@@ -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,8 +34,9 @@ public class XCPostbuild {
|
||||
let context: PostbuildContext
|
||||
let cacheHitLogger: CacheHitLogger
|
||||
do {
|
||||
config = try XCRemoteCacheConfigReader(env: env, fileManager: fileManager).readConfiguration()
|
||||
config = try XCRemoteCacheConfigReader(env: env, fileReader: fileManager).readConfiguration()
|
||||
context = try PostbuildContext(config, env: env)
|
||||
updateProcessTag(context.targetName)
|
||||
let counterFactory: FileStatsCoordinator.CountersFactory = { file, count in
|
||||
ExclusiveFileCounter(ExclusiveFile(file, mode: .override), countersCount: count)
|
||||
}
|
||||
@@ -66,8 +68,8 @@ public class XCPostbuild {
|
||||
// Initialize dependencies
|
||||
let primaryGitBranch = GitBranch(repoLocation: config.primaryRepo, branch: config.primaryBranch)
|
||||
let gitClient = GitClientImpl(repoRoot: config.repoRoot, primary: primaryGitBranch, shell: shellGetStdout)
|
||||
let envsRemapper = try StringDependenciesRemapperFactory().build(
|
||||
orderKeys: DependenciesMapping.rewrittenEnvs,
|
||||
let envsRemapper = try PathDependenciesRemapperFactory().build(
|
||||
orderKeys: DependenciesMapping.rewrittenEnvs + config.customRewriteEnvs,
|
||||
envs: env,
|
||||
customMappings: config.outOfBandMappings
|
||||
)
|
||||
@@ -85,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,
|
||||
@@ -95,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(
|
||||
@@ -112,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)
|
||||
@@ -120,6 +130,7 @@ public class XCPostbuild {
|
||||
let networkClient = NetworkClientImpl(
|
||||
session: sessionFactory.build(),
|
||||
retries: config.uploadRetries,
|
||||
retryDelay: config.retryDelay,
|
||||
fileManager: fileManager,
|
||||
awsV4Signature: awsV4Signature
|
||||
)
|
||||
@@ -127,6 +138,7 @@ public class XCPostbuild {
|
||||
mode: context.mode,
|
||||
downloadStreamURL: context.recommendedCacheAddress,
|
||||
upstreamStreamURL: context.cacheAddresses,
|
||||
uploadBatchSize: config.uploadBatchSize,
|
||||
networkClient: networkClient,
|
||||
urlBuilderFactory: {
|
||||
try URLBuilderImpl(
|
||||
@@ -149,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,
|
||||
@@ -167,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(
|
||||
@@ -194,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 =
|
||||
@@ -318,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,8 +29,9 @@ public class XCPrebuild {
|
||||
let config: XCRemoteCacheConfig
|
||||
let context: PrebuildContext
|
||||
do {
|
||||
config = try XCRemoteCacheConfigReader(env: env, fileManager: fileManager).readConfiguration()
|
||||
config = try XCRemoteCacheConfigReader(env: env, fileReader: fileManager).readConfiguration()
|
||||
context = try PrebuildContext(config, env: env)
|
||||
updateProcessTag(context.targetName)
|
||||
} catch {
|
||||
// Fatal error:
|
||||
exit(1, "FATAL: Prebuild initialization failed with error: \(error)")
|
||||
@@ -77,6 +78,10 @@ public class XCPrebuild {
|
||||
exit(0)
|
||||
}
|
||||
|
||||
let compilationHistoryOrganizer = CompilationHistoryFileOrganizer(
|
||||
context.compilationHistoryFile,
|
||||
fileManager: fileManager
|
||||
)
|
||||
do {
|
||||
let envFingerprint = try EnvironmentFingerprintGenerator(
|
||||
configuration: config,
|
||||
@@ -95,6 +100,7 @@ public class XCPrebuild {
|
||||
awsV4Signature = AWSV4Signature(
|
||||
secretKey: config.AWSSecretKey,
|
||||
accessKey: config.AWSAccessKey,
|
||||
securityToken: config.AWSSecurityToken,
|
||||
region: config.AWSRegion,
|
||||
service: config.AWSService,
|
||||
date: Date(timeIntervalSinceNow: 0)
|
||||
@@ -103,6 +109,7 @@ public class XCPrebuild {
|
||||
let networkClient = NetworkClientImpl(
|
||||
session: sessionFactory.build(),
|
||||
retries: config.downloadRetries,
|
||||
retryDelay: config.retryDelay,
|
||||
fileManager: fileManager,
|
||||
awsV4Signature: awsV4Signature
|
||||
)
|
||||
@@ -115,15 +122,15 @@ public class XCPrebuild {
|
||||
)
|
||||
let client: NetworkClient = config.disableHttpCache ? networkClient : cacheNetworkClient
|
||||
let remoteNetworkClient = RemoteNetworkClientImpl(client, urlBuilder)
|
||||
let envsRemapper = try StringDependenciesRemapperFactory().build(
|
||||
orderKeys: DependenciesMapping.rewrittenEnvs,
|
||||
let envsRemapper = try PathDependenciesRemapperFactory().build(
|
||||
orderKeys: DependenciesMapping.rewrittenEnvs + config.customRewriteEnvs,
|
||||
envs: env,
|
||||
customMappings: config.outOfBandMappings
|
||||
)
|
||||
var remappers: [DependenciesRemapper] = []
|
||||
if !config.disableVFSOverlay {
|
||||
// As PrebuildContext assumes file location and its filename (`all-product-headers.yaml`)
|
||||
// do not fail in case of a missing headers overlay file.
|
||||
// do not fail in case of a missing headers overlay file
|
||||
let overlayReader = JsonOverlayReader(
|
||||
context.overlayHeadersPath,
|
||||
mode: .bestEffort,
|
||||
@@ -145,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)
|
||||
@@ -155,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,
|
||||
@@ -187,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(
|
||||
@@ -195,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
|
||||
|
||||
+10
-3
@@ -33,20 +33,24 @@ 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["LDPLUSPLUS"] = wrappers.ldplusplus.path
|
||||
}
|
||||
|
||||
let existingSwiftFlags = result["OTHER_SWIFT_FLAGS"] as? String
|
||||
@@ -61,8 +65,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
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ extension IntegrateContext {
|
||||
swiftc: binariesDir.appendingPathComponent("xcswiftc"),
|
||||
libtool: binariesDir.appendingPathComponent("xclibtool"),
|
||||
ld: binariesDir.appendingPathComponent("xcld"),
|
||||
ldplusplus: binariesDir.appendingPathComponent("xcldplusplus"),
|
||||
prebuild: binariesDir.appendingPathComponent("xcprebuild"),
|
||||
postbuild: binariesDir.appendingPathComponent("xcpostbuild")
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -26,6 +26,7 @@ struct XCRCBinariesPaths {
|
||||
let swiftc: URL
|
||||
let libtool: 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,21 +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
|
||||
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 {
|
||||
@@ -165,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
|
||||
@@ -181,11 +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
|
||||
}
|
||||
|
||||
@@ -230,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]?
|
||||
@@ -243,11 +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 {
|
||||
@@ -275,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"
|
||||
@@ -288,11 +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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,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()
|
||||
@@ -337,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 []
|
||||
}
|
||||
}
|
||||
|
||||
+12
-8
@@ -19,24 +19,28 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
enum StringDependenciesRemapperFactoryError: Error {
|
||||
enum PathDependenciesRemapperFactoryError: Error {
|
||||
/// Remapping keys are duplicated and can lead to undetermined results
|
||||
case mappingKeyDuplication
|
||||
}
|
||||
|
||||
class StringDependenciesRemapperFactory {
|
||||
class PathDependenciesRemapperFactory {
|
||||
func build(
|
||||
orderKeys: [String],
|
||||
envs: [String: String],
|
||||
customMappings: [String: String]
|
||||
) throws -> StringDependenciesRemapper {
|
||||
let mappingMap = try envs.merging(customMappings) { envValue, outOfBandMapping in
|
||||
throw StringDependenciesRemapperFactoryError.mappingKeyDuplication
|
||||
let mappingMap = try envs.merging(customMappings) { _, _ in
|
||||
throw PathDependenciesRemapperFactoryError.mappingKeyDuplication
|
||||
}
|
||||
let mappingOrderKeys = orderKeys + customMappings.keys
|
||||
let mappings: [StringDependenciesRemapper.Mapping] = try mappingOrderKeys.map { key in
|
||||
let localValue: String = try mappingMap.readEnv(key: key)
|
||||
return StringDependenciesRemapper.Mapping(generic: "$(\(key))", local: localValue)
|
||||
let mappingOrderKeys = orderKeys + customMappings.keys
|
||||
let mappings: [StringDependenciesRemapper.Mapping] = mappingOrderKeys.compactMap { key in
|
||||
guard let localURL: URL = mappingMap.readEnv(key: key) else {
|
||||
debugLog("\(key) ENV to map a dependency is not defined")
|
||||
return nil
|
||||
}
|
||||
infoLog("Found url to remapp: \(localURL). Remapping: \(localURL.standardized.path)")
|
||||
return StringDependenciesRemapper.Mapping(generic: "$(\(key))", local: localURL.standardized.path)
|
||||
}
|
||||
return StringDependenciesRemapper(mappings: mappings)
|
||||
}
|
||||
@@ -42,7 +42,7 @@ class TargetDependenciesReader: DependenciesReader {
|
||||
let allURLs = try dirScanner.items(at: directory)
|
||||
let mergedDependencies = try allURLs.reduce(Set<String>()) { (prev: Set<String>, file) in
|
||||
// include only these .d files that either have corresponding .o file (incremental) or end
|
||||
// with '-master' (whole-module).
|
||||
// with '-master' (whole-module)
|
||||
// Otherwise .d is probably just a leftover from previous builds
|
||||
let correspondingOutputURL = file.deletingPathExtension().appendingPathExtension("o")
|
||||
let isDependencyFile = file.pathExtension == "d"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -22,34 +22,36 @@ import Foundation
|
||||
import os.log
|
||||
|
||||
|
||||
private var processTag: String = ""
|
||||
|
||||
public func exit(_ exitCode: Int32, _ message: String) -> Never {
|
||||
os_log("%{public}@", log: OSLog.default, type: .error, message)
|
||||
os_log("%{public}@%{public}@", log: OSLog.default, type: .error, processTag, message)
|
||||
printError(errorMessage: message)
|
||||
exit(exitCode)
|
||||
}
|
||||
|
||||
func defaultLog(_ message: String) {
|
||||
os_log("%{public}@", log: OSLog.default, type: .default, message)
|
||||
os_log("%{public}@%{public}@", log: OSLog.default, type: .default, processTag, message)
|
||||
}
|
||||
|
||||
func errorLog(_ message: String) {
|
||||
os_log("%{public}@", log: OSLog.default, type: .error, message)
|
||||
os_log("%{public}@%{public}@", log: OSLog.default, type: .error, processTag, message)
|
||||
}
|
||||
|
||||
func infoLog(_ message: String) {
|
||||
os_log("%{public}@", log: OSLog.default, type: .info, message)
|
||||
os_log("%{public}@%{public}@", log: OSLog.default, type: .info, processTag, message)
|
||||
}
|
||||
|
||||
func debugLog(_ message: String) {
|
||||
os_log("%{public}@", log: OSLog.default, type: .debug, message)
|
||||
os_log("%{public}@%{public}@", log: OSLog.default, type: .debug, processTag, message)
|
||||
}
|
||||
|
||||
func printError(errorMessage: String) {
|
||||
fputs("error: \(errorMessage)\n", stderr)
|
||||
fputs("error: \(processTag)\(errorMessage)\n", stderr)
|
||||
}
|
||||
|
||||
func printWarning(_ message: String) {
|
||||
print("warning: \(message)")
|
||||
print("warning: \(processTag)\(message)")
|
||||
}
|
||||
|
||||
/// Prints a message to the user. It shows in Xcode (if applies) or console output
|
||||
@@ -57,3 +59,7 @@ func printWarning(_ message: String) {
|
||||
func printToUser(_ message: String) {
|
||||
print("[RC] \(message)")
|
||||
}
|
||||
|
||||
func updateProcessTag(_ tag: String) {
|
||||
processTag = "(\(tag)) "
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
import Foundation
|
||||
|
||||
protocol MetaWriter {
|
||||
func write<T>(_ meta: T, locationDir : URL) throws -> URL where T : Meta
|
||||
func write<T>(_ meta: T, locationDir: URL) throws -> URL where T: Meta
|
||||
}
|
||||
|
||||
class JsonMetaWriter: MetaWriter {
|
||||
@@ -36,7 +36,7 @@ class JsonMetaWriter: MetaWriter {
|
||||
self.metaEncoder = encoder
|
||||
}
|
||||
|
||||
func write<T>(_ meta: T, locationDir : URL) throws -> URL where T : Meta {
|
||||
func write<T>(_ meta: T, locationDir: URL) throws -> URL where T: Meta {
|
||||
let metaURL = locationDir.appendingPathComponent(meta.fileKey).appendingPathExtension("json")
|
||||
let metaData = try metaEncoder.encode(meta)
|
||||
try fileWriter.write(toPath: metaURL.path, contents: metaData)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -24,8 +24,15 @@ enum EnvironmentError: Error {
|
||||
}
|
||||
|
||||
extension Dictionary where Key == String, Value == String {
|
||||
func readEnv(key: String) throws -> URL {
|
||||
func readEnv(key: String) -> URL? {
|
||||
guard let value = self[key].map(URL.init(fileURLWithPath:)) else {
|
||||
return nil
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func readEnv(key: String) throws -> URL {
|
||||
guard let value: URL = readEnv(key: key) else {
|
||||
throw EnvironmentError.missingEnv(key)
|
||||
}
|
||||
return value
|
||||
|
||||
@@ -21,7 +21,7 @@ import Foundation
|
||||
import XCRemoteCache
|
||||
|
||||
/// Wrapper for a `LD` program that copies the dynamic executable from a cached-downloaded location
|
||||
/// Fallbacks to a standard `clang` when the Ramote cache is not applicable (e.g. modified sources)
|
||||
/// Fallbacks to a standard `clang` when the Remote cache is not applicable (e.g. modified sources)
|
||||
public class XCLDMain {
|
||||
public func main() {
|
||||
let args = ProcessInfo().arguments
|
||||
@@ -48,7 +48,16 @@ public class XCLDMain {
|
||||
i += 1
|
||||
}
|
||||
guard let outputInput = output, let filelistInput = filelist, let dependencyInfoInput = dependencyInfo else {
|
||||
exit(1, "Missing 'output' argument. Args: \(args)")
|
||||
let ldCommand = "clang"
|
||||
print("Fallbacking to compilation using \(ldCommand).")
|
||||
|
||||
let args = ProcessInfo().arguments
|
||||
let paramList = [ldCommand] + args.dropFirst()
|
||||
let cargs = paramList.map { strdup($0) } + [nil]
|
||||
execvp(ldCommand, cargs)
|
||||
|
||||
/// C-function `execv` returns only when the command fails
|
||||
exit(1)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) 2021 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import Foundation
|
||||
import XCRemoteCache
|
||||
|
||||
/// Wrapper for a `LDPLUSPLUS` program that copies the dynamic executable from a cached-downloaded location
|
||||
/// Fallbacks to a standard `clang++` when the Remote cache is not applicable (e.g. modified sources)
|
||||
public class XCLDPlusPlusMain {
|
||||
public func main() {
|
||||
let args = ProcessInfo().arguments
|
||||
var output: String?
|
||||
var filelist: String?
|
||||
var dependencyInfo: String?
|
||||
var i = 0
|
||||
while i < args.count {
|
||||
switch args[i] {
|
||||
case "-o":
|
||||
output = args[i + 1]
|
||||
i += 1
|
||||
case "-filelist":
|
||||
filelist = args[i + 1]
|
||||
i += 1
|
||||
case "-dependency_info":
|
||||
// Skip following `-Xlinker` argument. Sample call:
|
||||
// `clang -dynamiclib ... -Xlinker -dependency_info -Xlinker /path/Target_dependency_info.dat`
|
||||
dependencyInfo = args[i + 2]
|
||||
i += 2
|
||||
default:
|
||||
break
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
guard let outputInput = output, let filelistInput = filelist, let dependencyInfoInput = dependencyInfo else {
|
||||
let ldCommand = "clang++"
|
||||
print("Fallbacking to compilation using \(ldCommand).")
|
||||
|
||||
let args = ProcessInfo().arguments
|
||||
let paramList = [ldCommand] + args.dropFirst()
|
||||
let cargs = paramList.map { strdup($0) } + [nil]
|
||||
execvp(ldCommand, cargs)
|
||||
|
||||
/// C-function `execv` returns only when the command fails
|
||||
exit(1)
|
||||
}
|
||||
|
||||
|
||||
// TODO: consider using `clang_command` from .rcinfo
|
||||
/// concrete clang path should be taken from the current toolchain
|
||||
let fallbackCommand = "clang++"
|
||||
XCCreateBinary(
|
||||
output: outputInput,
|
||||
filelist: filelistInput,
|
||||
dependencyInfo: dependencyInfoInput,
|
||||
fallbackCommand: fallbackCommand,
|
||||
stepDescription: "xcldplusplus"
|
||||
).run()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) 2021 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import XCRemoteCache
|
||||
|
||||
XCLDPlusPlusMain().main()
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) 2021 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
@testable import XCRemoteCache
|
||||
import XCTest
|
||||
|
||||
class XcodeProjBuildSettingsIntegrateAppenderTests: XCTestCase {
|
||||
private let rootURL: URL = "/root"
|
||||
private let binariesDir: URL = "/binaries"
|
||||
private var buildSettings: BuildSettings!
|
||||
private var binaries: XCRCBinariesPaths!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
buildSettings = BuildSettings()
|
||||
binaries = XCRCBinariesPaths(
|
||||
prepare: binariesDir.appendingPathComponent("xcprepare"),
|
||||
cc: binariesDir.appendingPathComponent("xccc"),
|
||||
swiftc: binariesDir.appendingPathComponent("xcswiftc"),
|
||||
libtool: binariesDir.appendingPathComponent("xclibtool"),
|
||||
ld: binariesDir.appendingPathComponent("xcld"),
|
||||
ldplusplus: binariesDir.appendingPathComponent("xcldplusplus"),
|
||||
prebuild: binariesDir.appendingPathComponent("xcprebuild"),
|
||||
postbuild: binariesDir.appendingPathComponent("xcpostbuild")
|
||||
)
|
||||
}
|
||||
|
||||
func testProducerSettingFakeSrcRoot() throws {
|
||||
let mode: Mode = .producer
|
||||
let fakeRootURL: URL = "/xxxxxxxxxxP"
|
||||
let appender = XcodeProjBuildSettingsIntegrateAppender(mode: mode, repoRoot: rootURL, fakeSrcRoot: fakeRootURL)
|
||||
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
|
||||
let resultURL = try XCTUnwrap(result["XCRC_FAKE_SRCROOT"] as? String)
|
||||
|
||||
XCTAssertEqual(resultURL, fakeRootURL.path)
|
||||
}
|
||||
|
||||
func testConsumerSettingFakeSrcRoot() throws {
|
||||
let mode: Mode = .consumer
|
||||
let fakeRootURL: URL = "/xxxxxxxxxxC"
|
||||
let appender = XcodeProjBuildSettingsIntegrateAppender(mode: mode, repoRoot: rootURL, fakeSrcRoot: fakeRootURL)
|
||||
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
|
||||
let resultURL: String = try XCTUnwrap(result["XCRC_FAKE_SRCROOT"] as? String)
|
||||
|
||||
XCTAssertEqual(resultURL, fakeRootURL.path)
|
||||
}
|
||||
|
||||
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
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
+28
-5
@@ -20,11 +20,12 @@
|
||||
@testable import XCRemoteCache
|
||||
import XCTest
|
||||
|
||||
class StringDependenciesRemapperFactoryTests: XCTestCase {
|
||||
private var factory: StringDependenciesRemapperFactory!
|
||||
class PathDependenciesRemapperFactoryTests: XCTestCase {
|
||||
private var factory: PathDependenciesRemapperFactory!
|
||||
|
||||
override func setUp() {
|
||||
factory = StringDependenciesRemapperFactory()
|
||||
super.setUp()
|
||||
factory = PathDependenciesRemapperFactory()
|
||||
}
|
||||
|
||||
func testMappingsFromEnvMaps() throws {
|
||||
@@ -38,8 +39,30 @@ class StringDependenciesRemapperFactoryTests: XCTestCase {
|
||||
XCTAssertEqual(localPaths, ["/tmp/root/some.swift"])
|
||||
}
|
||||
|
||||
func testInvalidMappingsFromEnvFails() throws {
|
||||
XCTAssertThrowsError(
|
||||
func testMappingsGenericWhenMappingHasParentDir() throws {
|
||||
let remapper = try factory.build(
|
||||
orderKeys: ["SRC_ROOT"],
|
||||
envs: ["SRC_ROOT": "/tmp/root/extra/.."],
|
||||
customMappings: [:]
|
||||
)
|
||||
|
||||
let localPaths = try remapper.replace(genericPaths: ["$(SRC_ROOT)/some.swift"])
|
||||
XCTAssertEqual(localPaths, ["/tmp/root/some.swift"])
|
||||
}
|
||||
|
||||
func testMappingsLocalWhenMappingHasParentDir() throws {
|
||||
let remapper = try factory.build(
|
||||
orderKeys: ["SRC_ROOT"],
|
||||
envs: ["SRC_ROOT": "/tmp/root/excessive/.."],
|
||||
customMappings: [:]
|
||||
)
|
||||
|
||||
let localPaths = try remapper.replace(localPaths: ["/tmp/root/some.swift"])
|
||||
XCTAssertEqual(localPaths, ["$(SRC_ROOT)/some.swift"])
|
||||
}
|
||||
|
||||
func testMissingEnvIsSkipped() throws {
|
||||
XCTAssertNoThrow(
|
||||
try factory.build(
|
||||
orderKeys: ["SRC_ROOT"],
|
||||
envs: ["NO_SRC_ROOT": ""],
|
||||
@@ -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
@@ -47,6 +47,16 @@ class DirAccessorFake: DirAccessor {
|
||||
}
|
||||
}
|
||||
|
||||
func recursiveItems(at dir: URL) throws -> [URL] {
|
||||
memory.compactMap { url, _ in
|
||||
// comparing paths to ignore dir or url's "isDir" property
|
||||
if url.deletingLastPathComponent().path.starts(with: dir.path) {
|
||||
return url
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func contents(atPath path: String) throws -> Data? {
|
||||
memory[URL(fileURLWithPath: path)]
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user