Compare commits
98 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f8c854d007 | |||
| 7fe04517e7 | |||
| 5029f9c73b | |||
| ed256234f1 | |||
| fd6e1da054 | |||
| 574129788c | |||
| 6a1335ea97 | |||
| be87c3779e | |||
| 2c23632732 | |||
| b048393eb0 | |||
| 8955cb2750 | |||
| afb1f9e531 | |||
| 53e7ddd34c | |||
| 344b1f13ca | |||
| bc13f58e73 | |||
| 47ad8a890b | |||
| 801920dc87 | |||
| 37e144c36a | |||
| f389887e37 | |||
| 587840ddbc | |||
| 42a56b4f5b | |||
| 6a98cb4127 | |||
| d5e95962b5 | |||
| b74e002415 | |||
| 9a34a99f0b | |||
| f4eed9e8aa | |||
| 811cc00f0c | |||
| 56d9c208a1 | |||
| 8dffbd4162 | |||
| 3853ce2bc2 | |||
| 148c99d2f5 | |||
| 65bf9156ec | |||
| 22484b4daf | |||
| 867bbb6265 | |||
| 532484c3ab | |||
| 338cbd141a | |||
| f15dd8f98d | |||
| ee31a3815f | |||
| 5749762b86 | |||
| f8757b6ee7 | |||
| c26aaf7d42 | |||
| d837f6e14b | |||
| 5528d507b0 | |||
| 352e72f44c | |||
| 1c67b79a7a | |||
| c7de203741 | |||
| b7e18916e6 | |||
| dfb4039404 | |||
| b28613a2ef | |||
| 1535b762bc | |||
| e6816846c3 | |||
| b89d98f411 | |||
| a0d3d1b0b9 | |||
| f432917505 | |||
| 5d297a4fb2 | |||
| b0d5f1660e | |||
| baea2de79a | |||
| d2803f4ad5 | |||
| ab017367b2 | |||
| 30cb648641 | |||
| 3330ca45f8 | |||
| d3193b15a8 | |||
| 7b14f2c9ff | |||
| 4933b454a5 | |||
| 79ffdce295 | |||
| 376e6a17c5 | |||
| a4d1849821 | |||
| 325fb07080 | |||
| a2afe62751 | |||
| bbaa374e12 | |||
| 39a259ff49 | |||
| a2b9cbf332 | |||
| 5710594bc4 | |||
| 7d123792b8 | |||
| 5856dbec77 | |||
| 600310f44b | |||
| aae2b3289c | |||
| 11eabdab3d | |||
| de24e609ef | |||
| 850983cbde | |||
| 0064335cc7 | |||
| 1ddadcb361 | |||
| 2f10c6a3a0 | |||
| c7cd649aab | |||
| 8201f7778b | |||
| 504393c3e3 | |||
| 82334dda04 | |||
| 1072979479 | |||
| d741b3f6df | |||
| a0f20b4da3 | |||
| f325b74796 | |||
| a50eae615c | |||
| 3c8f062e95 | |||
| 1cf685e197 | |||
| d2ba874079 | |||
| b439674378 | |||
| b1507b6e60 | |||
| c76c8a7672 |
@@ -15,7 +15,7 @@ jobs:
|
||||
macOS:
|
||||
runs-on: macos-12
|
||||
env:
|
||||
XCODE_VERSION: ${{ '13.3.1' }}
|
||||
XCODE_VERSION: ${{ '14.2' }}
|
||||
steps:
|
||||
- name: Select Xcode
|
||||
run: "sudo xcode-select -s /Applications/Xcode_$XCODE_VERSION.app"
|
||||
|
||||
@@ -9,7 +9,7 @@ jobs:
|
||||
docs:
|
||||
runs-on: macos-12
|
||||
env:
|
||||
XCODE_VERSION: ${{ '13.3.1' }}
|
||||
XCODE_VERSION: ${{ '14.2' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Select Xcode
|
||||
|
||||
@@ -8,7 +8,7 @@ jobs:
|
||||
name: Add macOS binaries to release
|
||||
runs-on: macos-12
|
||||
env:
|
||||
XCODE_VERSION: ${{ '13.3.1' }}
|
||||
XCODE_VERSION: ${{ '14.2' }}
|
||||
steps:
|
||||
- name: Select Xcode
|
||||
run: "sudo xcode-select -s /Applications/Xcode_$XCODE_VERSION.app"
|
||||
|
||||
@@ -51,8 +51,8 @@
|
||||
"repositoryURL": "https://github.com/tuist/XcodeProj.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "c75c3acc25460195cfd203a04dde165395bf00e0",
|
||||
"version": "8.7.1"
|
||||
"revision": "fae27b48bc14ff3fd9b02902e48c4665ce5a0793",
|
||||
"version": "8.9.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -16,7 +16,7 @@ let package = Package(
|
||||
.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.7.1"),
|
||||
.package(url: "https://github.com/tuist/XcodeProj.git", from: "8.9.0"),
|
||||
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
|
||||
],
|
||||
targets: [
|
||||
@@ -32,8 +32,20 @@ let package = Package(
|
||||
name: "xcswiftc",
|
||||
dependencies: ["XCRemoteCache"]
|
||||
),
|
||||
.target(
|
||||
name: "xcswift-frontend",
|
||||
dependencies: ["XCRemoteCache"]
|
||||
),
|
||||
.target(
|
||||
name: "xclibtoolSupport",
|
||||
dependencies: ["XCRemoteCache"]
|
||||
),
|
||||
.target(
|
||||
name: "xclibtool",
|
||||
dependencies: ["XCRemoteCache", "xclibtoolSupport"]
|
||||
),
|
||||
.target(
|
||||
name: "xclipo",
|
||||
dependencies: ["XCRemoteCache"]
|
||||
),
|
||||
.target(
|
||||
@@ -58,12 +70,26 @@ let package = Package(
|
||||
.target(
|
||||
// Wrapper target that builds all binaries but does nothing in runtime
|
||||
name: "Aggregator",
|
||||
dependencies: ["xcprebuild", "xcswiftc", "xclibtool", "xcpostbuild", "xcprepare", "xcld", "xcldplusplus"]
|
||||
dependencies: [
|
||||
"xcprebuild",
|
||||
"xcswiftc",
|
||||
"xcswift-frontend",
|
||||
"xclibtool",
|
||||
"xcpostbuild",
|
||||
"xcprepare",
|
||||
"xcld",
|
||||
"xcldplusplus",
|
||||
"xclipo",
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "XCRemoteCacheTests",
|
||||
dependencies: ["XCRemoteCache"],
|
||||
resources: [.copy("TestData")]
|
||||
),
|
||||
.testTarget(
|
||||
name: "xclibtoolSupportTests",
|
||||
dependencies: ["xclibtoolSupport"]
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -45,6 +45,7 @@ _XCRemoteCache is a remote cache tool for Xcode projects. It reuses target artif
|
||||
- [Limitations](#limitations)
|
||||
- [FAQ](#faq)
|
||||
- [Development](#development)
|
||||
- [Architectural Designs](#architectural-designs)
|
||||
- [Release](#release)
|
||||
* [Releasing CocoaPods plugin](#releasing-cocoapods-plugin)
|
||||
* [Building release package](#building-release-package)
|
||||
@@ -152,6 +153,7 @@ xcremotecache/xcprepare integrate --input <yourProject.xcodeproj> --mode consume
|
||||
| `--lldb-init` | LLDBInit mode. Appends to .lldbinit a command required for debugging. Supported values: 'none' (do not append to .lldbinit), 'user' (append to ~/.lldbinit) | `user` | ⬜️ |
|
||||
| `--fake-src-root` | An arbitrary source location shared between producers and consumers. Should be unique for a project. | `/xxxxxxxxxx` | ⬜️ |
|
||||
| `--output` | Save the project with integrated XCRemoteCache to a separate location. | N/A | ⬜️ |
|
||||
| `--sdks-exclude` | comma separated list of sdks to not integrate XCRemoteCache (e.g. "watchos*, watchsimulator*"). (Experimental) | `""` | ⬜️ |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -195,6 +197,7 @@ Configure Xcode targets that **should use** XCRemoteCache:
|
||||
* `CC` - `xccc_file` from your `.rcinfo` configuration (e.g. `xcremotecache/xccc`)
|
||||
* `SWIFT_EXEC` - location of `xcprepare` (e.g. `xcremotecache/xcswiftc`)
|
||||
* `LIBTOOL` - location of `xclibtool` (e.g. `xcremotecache/xclibtool`)
|
||||
* `LIPO` - location of `xclipo` (e.g. `xcremotecache/xclipo`)
|
||||
* `LD` - location of `xcld` (e.g. `xcremotecache/xcld`)
|
||||
* `LDPLUSPLUS` - location of `xcldplusplus` (e.g. `xcremotecache/xcldplusplus`)
|
||||
* `XCRC_PLATFORM_PREFERRED_ARCH` - `$(LINK_FILE_LIST_$(CURRENT_VARIANT)_$(PLATFORM_PREFERRED_ARCH):dir:standardizepath:file:default=arm64)`
|
||||
@@ -267,7 +270,7 @@ $ xcremotecache/xcprepare mark --configuration Debug --platform iphonesimulator
|
||||
|
||||
That command creates an empty file on a remote server which informs that for given sha, configuration, platform, Xcode versions etc. all artifacts are available.
|
||||
|
||||
_Note that for the `producer` mode, the prebuild build phase and `xccc`, `xcld`, `xcldplusplus`, `xclibtool` wrappers become no-op, so it is recommended to not add them for the `producer` mode._
|
||||
_Note that for the `producer` mode, the prebuild build phase and `xccc`, `xcld`, `xcldplusplus`, `xclibtool`, `xclipo` wrappers become no-op, so it is recommended to not add them for the `producer` mode._
|
||||
|
||||
##### 7. Generalize `-Swift.h` (Optional only if using static library with a bridging header with public `NS_ENUM` exposed from ObjC)
|
||||
|
||||
@@ -289,7 +292,7 @@ where
|
||||
|
||||
```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}"
|
||||
[ -f "${SCRIPT_INPUT_FILE_1}" ] && ditto "${SCRIPT_INPUT_FILE_1}" "${SCRIPT_OUTPUT_FILE_1}" || rm -f "${SCRIPT_OUTPUT_FILE_1}"
|
||||
```
|
||||
|
||||
where
|
||||
@@ -356,6 +359,8 @@ Note: This step is not required if at least one of these is true:
|
||||
| `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. | `[]` | ⬜️ |
|
||||
| `gracefully_handle_missing_common_sha` | If true, do not fail `prepare` if cannot find the most recent common commits with the primary branch. That might be useful on CI, where a shallow clone is used and cloning depth is not big enough to fetch a commit from a primary branch | `false` | ⬜️ |
|
||||
| `enable_swift_driver_integration` | Enable experimental integration with swift driver, added in Xcode 14 | `false` | ⬜️ |
|
||||
|
||||
## Backend cache server
|
||||
|
||||
@@ -465,6 +470,10 @@ Follow the [FAQ](docs/FAQ.md) page.
|
||||
|
||||
Follow the [Development](docs/Development.md) guide. It has all the information on how to get started.
|
||||
|
||||
## Architectural designs
|
||||
|
||||
Follow the [Architectural designs](docs/design/ArchitecturalDesigns.md) document that describes and documents XCRemoteCache designs and implementation details.
|
||||
|
||||
## Release
|
||||
|
||||
To release a version, in [Releases](https://github.com/spotify/XCRemoteCache/releases) draft a new release with `v0.3.0{-rc0}` tag format.
|
||||
|
||||
@@ -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', 'xcldplusplus']
|
||||
EXECUTABLE_NAMES = ['xclibtool', 'xcpostbuild', 'xcprebuild', 'xcprepare', 'xcswiftc', 'swiftc', 'xcswift-frontend', 'swift-frontend', 'xcld', 'xcldplusplus', 'xclipo']
|
||||
PROJECT_NAME = 'XCRemoteCache'
|
||||
|
||||
SWIFTLINT_ENABLED = true
|
||||
@@ -59,6 +59,10 @@ task :build, [:configuration, :arch, :sdks, :is_archive] do |task, args|
|
||||
|
||||
# Path of the executable looks like: `.build/(debug|release)/XCRemoteCache`
|
||||
build_path_base = File.join(DERIVED_DATA_DIR, args.configuration)
|
||||
# swift-frontent integration requires that the SWIFT_EXEC is `swiftc` so create
|
||||
# a symbolic link between swiftc->xcswiftc and swift-frontend->xcswift-frontend
|
||||
system("cd #{build_path_base} && ln -s xcswiftc swiftc")
|
||||
system("cd #{build_path_base} && ln -s xcswift-frontend swift-frontend")
|
||||
sdk_build_paths = EXECUTABLE_NAMES.map {|e| File.join(build_path_base, e)}
|
||||
|
||||
build_paths.push(sdk_build_paths)
|
||||
@@ -130,7 +134,9 @@ def create_release_zip(build_paths)
|
||||
# Create and move files into the release directory
|
||||
mkdir_p release_dir
|
||||
build_paths.each {|p|
|
||||
cp_r p, release_dir
|
||||
# -r for recursive
|
||||
# -P for copying symbolic link as is
|
||||
system("cp -rP #{p} #{release_dir}")
|
||||
}
|
||||
|
||||
output_artifact_basename = "#{PROJECT_NAME}.zip"
|
||||
@@ -139,7 +145,8 @@ def create_release_zip(build_paths)
|
||||
# -X: no extras (uid, gid, file times, ...)
|
||||
# -x: exclude .DS_Store
|
||||
# -r: recursive
|
||||
system("zip -X -x '*.DS_Store' -r #{output_artifact_basename} .") or abort "zip failure"
|
||||
# -y: to store symbolic links (used for swiftc -> xcswiftc)
|
||||
system("zip -X -x '*.DS_Store' -r -y #{output_artifact_basename} .") or abort "zip failure"
|
||||
# List contents of zip file
|
||||
system("unzip -l #{output_artifact_basename}") or abort "unzip failure"
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
enum ArtifactMetaUpdaterError: Error {
|
||||
/// The prebuild plugin execution was called but the local
|
||||
/// path to the artifact directory is still unknown
|
||||
/// Might happen that the artifact processor didn't invoke the updater's
|
||||
/// .process() after downloading/activating an artifact
|
||||
case artifactLocationIsUnknown
|
||||
}
|
||||
|
||||
/// Updates the meta file in an unzipped artifact directory, by placing an up-to-date
|
||||
/// and remapped meta file. Updating the meta in the artifact allows reusing existing
|
||||
/// artifacts it a new meta.json schema has been released to the meta format, while
|
||||
/// artifacts are still backward-compatible
|
||||
class ArtifactMetaUpdater: ArtifactProcessor {
|
||||
private var artifactLocation: URL?
|
||||
private let metaWriter: MetaWriter
|
||||
private let fileRemapper: FileDependenciesRemapper
|
||||
|
||||
init(
|
||||
fileRemapper: FileDependenciesRemapper,
|
||||
metaWriter: MetaWriter
|
||||
) {
|
||||
self.metaWriter = metaWriter
|
||||
self.fileRemapper = fileRemapper
|
||||
}
|
||||
|
||||
/// Remembers the artifact location, used later in the plugin
|
||||
/// - Parameter url: artifact's root directory
|
||||
func process(rawArtifact url: URL) throws {
|
||||
// Storing the location of the just downloaded/activated artifact
|
||||
// Note, the `url` location already includes a meta (generated by producer
|
||||
// while compiling and building an artifact)
|
||||
artifactLocation = url
|
||||
}
|
||||
|
||||
func process(localArtifact url: URL) throws {
|
||||
// No need to do anything in the postbuild
|
||||
}
|
||||
}
|
||||
|
||||
extension ArtifactMetaUpdater: ArtifactConsumerPrebuildPlugin {
|
||||
|
||||
/// Updates the meta json file in a local, unzipped, artifact location. It also remaps
|
||||
/// all paths so other steps (like actool or postbuild) don't have to do it again
|
||||
func run(meta: MainArtifactMeta) throws {
|
||||
guard let artifactLocation = artifactLocation else {
|
||||
throw ArtifactMetaUpdaterError.artifactLocationIsUnknown
|
||||
}
|
||||
let metaURL = try metaWriter.write(meta, locationDir: artifactLocation)
|
||||
try fileRemapper.remap(fromGeneric: metaURL)
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,8 @@ protocol ArtifactOrganizer {
|
||||
}
|
||||
|
||||
class ZipArtifactOrganizer: ArtifactOrganizer {
|
||||
static let activeArtifactLocation = "active"
|
||||
|
||||
private let cacheDir: URL
|
||||
// all processors that should "prepare" the unzipped raw artifact
|
||||
private let artifactProcessors: [ArtifactProcessor]
|
||||
@@ -63,7 +65,7 @@ class ZipArtifactOrganizer: ArtifactOrganizer {
|
||||
}
|
||||
|
||||
func getActiveArtifactLocation() -> URL {
|
||||
return cacheDir.appendingPathComponent("active")
|
||||
return cacheDir.appendingPathComponent(Self.self.activeArtifactLocation)
|
||||
}
|
||||
|
||||
func getActiveArtifactFilekey() throws -> String {
|
||||
@@ -90,20 +92,27 @@ class ZipArtifactOrganizer: ArtifactOrganizer {
|
||||
let destinationURL = artifact.deletingPathExtension()
|
||||
guard fileManager.fileExists(atPath: destinationURL.path) == false else {
|
||||
infoLog("Skipping artifact, already existing at \(destinationURL)")
|
||||
try runArtifactProcessors(artifactLocation: destinationURL)
|
||||
return destinationURL
|
||||
}
|
||||
// Uzipping to a temp file first to never leave the uncompleted zip in the final location
|
||||
// Unzipping to a temp file first to never leave the uncompleted zip in the final location
|
||||
// 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)
|
||||
try runArtifactProcessors(artifactLocation: destinationURL)
|
||||
return destinationURL
|
||||
}
|
||||
|
||||
/// Iterates all processor when an artifact has been just downloaded or reused from already downloaded
|
||||
/// and previously processed location
|
||||
private func runArtifactProcessors(artifactLocation: URL) throws {
|
||||
try artifactProcessors.forEach { processor in
|
||||
try processor.process(rawArtifact: artifactLocation)
|
||||
}
|
||||
}
|
||||
|
||||
func activate(extractedArtifact: URL) throws {
|
||||
let activeLocationURL = getActiveArtifactLocation()
|
||||
try fileManager.spt_forceSymbolicLink(at: activeLocationURL, withDestinationURL: extractedArtifact)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// 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
|
||||
|
||||
class FallbackXCLibtoolLogic: XCLibtoolLogic {
|
||||
private let fallbackCommand: String
|
||||
|
||||
init(fallbackCommand: String) {
|
||||
self.fallbackCommand = fallbackCommand
|
||||
}
|
||||
|
||||
func run() {
|
||||
let args = ProcessInfo().arguments
|
||||
let paramList = [fallbackCommand] + args.dropFirst()
|
||||
let cargs = paramList.map { strdup($0) } + [nil]
|
||||
execvp(fallbackCommand, cargs)
|
||||
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
@@ -19,31 +19,42 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
enum XCLibtoolCreateUniversalBinaryError: Error {
|
||||
enum XCCreateUniversalBinaryError: Error {
|
||||
/// Missing ar libraries that should constitute an universal build
|
||||
case missingInputLibrary
|
||||
}
|
||||
|
||||
/// Wrapper for `libtool` call for creating an universal binary
|
||||
class XCLibtoolCreateUniversalBinary: XCLibtoolLogic {
|
||||
/// Wrapper for `libtool`/`lipo` call for creating an universal binary
|
||||
class XCCreateUniversalBinary: XCLibtoolLogic {
|
||||
private let output: URL
|
||||
private let tempDir: URL
|
||||
private let firstInputURL: URL
|
||||
private let toolName: String
|
||||
private let fallbackCommand: String
|
||||
|
||||
init(output: String, inputs: [String]) throws {
|
||||
init(
|
||||
output: String,
|
||||
inputs: [String],
|
||||
toolName: String,
|
||||
fallbackCommand: String
|
||||
) throws {
|
||||
self.output = URL(fileURLWithPath: output)
|
||||
guard let firstInput = inputs.first else {
|
||||
throw XCLibtoolCreateUniversalBinaryError.missingInputLibrary
|
||||
throw XCCreateUniversalBinaryError.missingInputLibrary
|
||||
}
|
||||
let firstInputURL = URL(fileURLWithPath: firstInput)
|
||||
// inputs are place in $TARGET_TEMP_DIR/Objects-normal/$ARCH/Binary/$TARGET_NAME.a
|
||||
// TODO: find better (stable) technique to determine `$TARGET_TEMP_DIR`
|
||||
errorLog("\(firstInputURL.absoluteString)")
|
||||
|
||||
tempDir = firstInputURL
|
||||
.deletingLastPathComponent()
|
||||
.deletingLastPathComponent()
|
||||
.deletingLastPathComponent()
|
||||
.deletingLastPathComponent()
|
||||
self.firstInputURL = firstInputURL
|
||||
self.toolName = toolName
|
||||
self.fallbackCommand = fallbackCommand
|
||||
}
|
||||
|
||||
func run() {
|
||||
@@ -55,7 +66,7 @@ class XCLibtoolCreateUniversalBinary: XCLibtoolLogic {
|
||||
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileReader: fileManager)
|
||||
.readConfiguration()
|
||||
} catch {
|
||||
errorLog("Libtool initialization failed with error: \(error). Fallbacking to libtool")
|
||||
errorLog("\(toolName) initialization failed with error: \(error). Fallbacking to \(fallbackCommand)")
|
||||
fallbackToDefault()
|
||||
}
|
||||
|
||||
@@ -74,22 +85,21 @@ class XCLibtoolCreateUniversalBinary: XCLibtoolLogic {
|
||||
// that these are already an universal binary
|
||||
try fileManager.spt_forceLinkItem(at: firstInputURL, to: output)
|
||||
} catch {
|
||||
errorLog("Libtool failed with error: \(error). Fallbacking to libtool")
|
||||
errorLog("\(toolName) failed with error: \(error). Fallbacking to \(fallbackCommand)")
|
||||
do {
|
||||
try fileManager.removeItem(at: markerURL)
|
||||
fallbackToDefault()
|
||||
} catch {
|
||||
exit(1, "FATAL: libtool failed with error: \(error)")
|
||||
exit(1, "FATAL: \(fallbackCommand) failed with error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func fallbackToDefault() -> Never {
|
||||
let args = ProcessInfo().arguments
|
||||
let command = "libtool"
|
||||
let paramList = [command] + args.dropFirst()
|
||||
let paramList = [fallbackCommand] + args.dropFirst()
|
||||
let cargs = paramList.map { strdup($0) } + [nil]
|
||||
execvp(command, cargs)
|
||||
execvp(fallbackCommand, cargs)
|
||||
|
||||
/// C-function `execv` returns only when the command fails
|
||||
exit(1)
|
||||
@@ -20,11 +20,13 @@
|
||||
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
|
||||
case createUniversalBinary(output: String, inputs: [String])
|
||||
/// print the toolchain version
|
||||
case version
|
||||
}
|
||||
|
||||
public class XCLibtool {
|
||||
@@ -44,7 +46,14 @@ public class XCLibtool {
|
||||
stepDescription: "Libtool"
|
||||
)
|
||||
case .createUniversalBinary(let output, let inputs):
|
||||
logic = try XCLibtoolCreateUniversalBinary(output: output, inputs: inputs)
|
||||
logic = try XCCreateUniversalBinary(
|
||||
output: output,
|
||||
inputs: inputs,
|
||||
toolName: "Libtool",
|
||||
fallbackCommand: "libtool"
|
||||
)
|
||||
case .version:
|
||||
logic = FallbackXCLibtoolLogic(fallbackCommand: "libtool")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2023 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Wrapper for a `lipo` tool that creates a fat archive
|
||||
public class XCLipo {
|
||||
private let logic: XCLibtoolLogic
|
||||
|
||||
public init(
|
||||
output: String,
|
||||
inputs: [String],
|
||||
fallbackCommand: String,
|
||||
stepDescription: String
|
||||
) throws {
|
||||
errorLog("\(output)")
|
||||
errorLog("\(inputs.joined(separator: ","))")
|
||||
logic = try XCCreateUniversalBinary(
|
||||
output: output,
|
||||
inputs: inputs,
|
||||
toolName: stepDescription,
|
||||
fallbackCommand: fallbackCommand
|
||||
)
|
||||
}
|
||||
|
||||
/// Handles a `-create` action which is responsible to create a fat archive
|
||||
/// If remote cache can reuse artifacts from a remote cache, it just links any of input
|
||||
/// files to the destination (output) location because the binary in XCRC artifact already
|
||||
/// contains a fat library
|
||||
/// If a remote artifact cannot be reused, a fallback to the `lipo` command is performed
|
||||
public func run() {
|
||||
logic.run()
|
||||
}
|
||||
}
|
||||
@@ -87,6 +87,11 @@ public struct PostbuildContext {
|
||||
let irrelevantDependenciesPaths: [String]
|
||||
/// Location of public headers. Not always available (e.g. static libraries)
|
||||
var publicHeadersFolderPath: URL?
|
||||
/// XCRemoteCache is explicitly disabled
|
||||
let disabled: Bool
|
||||
/// The LLBUILD_BUILD_ID ENV that describes the compilation identifier
|
||||
/// it is used in the swift-frontend flow
|
||||
let llbuildIdLockFile: URL
|
||||
}
|
||||
|
||||
extension PostbuildContext {
|
||||
@@ -140,11 +145,17 @@ extension PostbuildContext {
|
||||
/// Note: The file has yaml extension, even it is in the json format
|
||||
overlayHeadersPath = targetTempDir.appendingPathComponent("all-product-headers.yaml")
|
||||
irrelevantDependenciesPaths = config.irrelevantDependenciesPaths
|
||||
let publicHeadersPath: String = try env.readEnv(key: "PUBLIC_HEADERS_FOLDER_PATH")
|
||||
if publicHeadersPath != "/usr/local/include" {
|
||||
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)
|
||||
}
|
||||
disabled = try env.readEnv(key: "XCRC_DISABLED") ?? false
|
||||
let llbuildId: String = try env.readEnv(key: "LLBUILD_BUILD_ID")
|
||||
llbuildIdLockFile = SwiftFrontendContext.buildLlbuildIdSharedLockUrl(
|
||||
llbuildId: llbuildId,
|
||||
tmpDir: targetTempDir
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ public class XCPostbuild {
|
||||
dependenciesWriter: FileDependenciesWriter.init,
|
||||
dependenciesReader: FileDependenciesReader.init,
|
||||
markerWriter: NoopMarkerWriter.init,
|
||||
llbuildLockFile: context.llbuildIdLockFile,
|
||||
fileManager: fileManager
|
||||
)
|
||||
|
||||
@@ -152,9 +153,14 @@ public class XCPostbuild {
|
||||
let fileReaderFactory: (URL) -> DependenciesReader = {
|
||||
FileDependenciesReader($0, accessor: fileManager)
|
||||
}
|
||||
let assetsFileDependenciesFactory: (URL) -> DependenciesReader = {
|
||||
AssetsFileDependenciesReader($0, dirAccessor: fileManager)
|
||||
}
|
||||
let dependenciesReader = TargetDependenciesReader(
|
||||
context.compilationTempDir,
|
||||
fileDependeciesReaderFactory: fileReaderFactory,
|
||||
compilationOutputDir: context.compilationTempDir,
|
||||
assetsCatalogOutputDir: context.targetTempDir,
|
||||
fileDependenciesReaderFactory: fileReaderFactory,
|
||||
assetsDependenciesReaderFactory: assetsFileDependenciesFactory,
|
||||
dirScanner: fileManager
|
||||
)
|
||||
var remappers: [DependenciesRemapper] = []
|
||||
@@ -274,7 +280,12 @@ public class XCPostbuild {
|
||||
)
|
||||
|
||||
// Trigger build completion
|
||||
if try modeController.isEnabled() {
|
||||
if context.disabled {
|
||||
infoLog("XCRC fully disabled for \(context.targetName), \(context.platform), \(context.configuration)")
|
||||
// Cutoff the process is disabled, but generate an "empty" list of dependencies
|
||||
try? modeController.disable()
|
||||
return
|
||||
} else if try modeController.isEnabled() {
|
||||
// Decorate .swiftmodule in the product dir with fingerprint(s) overrides from a cache artifact
|
||||
try postbuildAction.performBuildCompletion()
|
||||
} else if context.mode == .consumer {
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
import Foundation
|
||||
|
||||
enum PrebuildResult: Equatable {
|
||||
case disabled
|
||||
case incompatible
|
||||
case compatible(localDependencies: [URL])
|
||||
}
|
||||
@@ -57,6 +58,9 @@ class Prebuild {
|
||||
|
||||
// swiftlint:disable:next function_body_length
|
||||
public func perform() throws -> PrebuildResult {
|
||||
guard !context.disabled else {
|
||||
return .disabled
|
||||
}
|
||||
guard case .available(let commit) = context.remoteCommit else {
|
||||
return .incompatible
|
||||
}
|
||||
|
||||
@@ -46,6 +46,11 @@ public struct PrebuildContext {
|
||||
/// location of the json file that define virtual files system overlay
|
||||
/// (mappings of the virtual location file -> local file path)
|
||||
let overlayHeadersPath: URL
|
||||
/// XCRemoteCache is explicitly disabled
|
||||
let disabled: Bool
|
||||
/// The LLBUILD_BUILD_ID ENV that describes the compilation identifier
|
||||
/// it is used in the swift-frontend flow
|
||||
let llbuildIdLockFile: URL
|
||||
}
|
||||
|
||||
extension PrebuildContext {
|
||||
@@ -69,5 +74,11 @@ extension PrebuildContext {
|
||||
thinnedTargets = thinFocusedTargetsString?.split(separator: ",").map(String.init)
|
||||
/// Note: The file has yaml extension, even it is in the json format
|
||||
overlayHeadersPath = targetTempDir.appendingPathComponent("all-product-headers.yaml")
|
||||
disabled = try env.readEnv(key: "XCRC_DISABLED") ?? false
|
||||
let llbuildId: String = try env.readEnv(key: "LLBUILD_BUILD_ID")
|
||||
llbuildIdLockFile = SwiftFrontendContext.buildLlbuildIdSharedLockUrl(
|
||||
llbuildId: llbuildId,
|
||||
tmpDir: targetTempDir
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ public class XCPrebuild {
|
||||
dependenciesWriter: FileDependenciesWriter.init,
|
||||
dependenciesReader: FileDependenciesReader.init,
|
||||
markerWriter: lazyMarkerWriterFactory,
|
||||
llbuildLockFile: context.llbuildIdLockFile,
|
||||
fileManager: fileManager
|
||||
)
|
||||
|
||||
@@ -157,13 +158,17 @@ public class XCPrebuild {
|
||||
fileAccessor: fileManager
|
||||
)
|
||||
let artifactProcessor = UnzippedArtifactProcessor(fileRemapper: fileRemapper, dirScanner: fileManager)
|
||||
let metaUpdater = ArtifactMetaUpdater(
|
||||
fileRemapper: fileRemapper,
|
||||
metaWriter: JsonMetaWriter(fileWriter: fileManager, pretty: true)
|
||||
)
|
||||
let organizer = ZipArtifactOrganizer(
|
||||
targetTempDir: context.targetTempDir,
|
||||
artifactProcessors: [artifactProcessor],
|
||||
artifactProcessors: [artifactProcessor, metaUpdater],
|
||||
fileManager: fileManager
|
||||
)
|
||||
let metaReader = JsonMetaReader(fileAccessor: fileManager)
|
||||
var consumerPlugins: [ArtifactConsumerPrebuildPlugin] = []
|
||||
var consumerPlugins: [ArtifactConsumerPrebuildPlugin] = [metaUpdater]
|
||||
|
||||
if config.thinningEnabled {
|
||||
if context.moduleName == config.thinningTargetModuleName, let thinnedTarget = context.thinnedTargets {
|
||||
@@ -202,6 +207,9 @@ public class XCPrebuild {
|
||||
case .compatible(localDependencies: let dependencies):
|
||||
// TODO: pass `allowedInputFiles` observed in the build time
|
||||
try modeController.enable(allowedInputFiles: dependencies, dependencies: dependencies)
|
||||
case .disabled:
|
||||
infoLog("XCRemoteCache is explicitly disabled")
|
||||
try modeController.disable()
|
||||
}
|
||||
} catch {
|
||||
disableRemoteCache(
|
||||
|
||||
@@ -396,7 +396,8 @@ class TemplateBasedCCWrapperBuilder: CCWrapperBuilder {
|
||||
const char *output_arg_name = "-o";
|
||||
const char *serialize_diagnostics_arg_name = "--serialize-diagnostics";
|
||||
const char *language_mode_arg_name = "-x";
|
||||
const char *precompile_header_arg_value = "objective-c-header";
|
||||
const char *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)";
|
||||
@@ -464,7 +465,7 @@ class TemplateBasedCCWrapperBuilder: CCWrapperBuilder {
|
||||
clang_args[argc] = NULL;
|
||||
|
||||
// Verify mode. Even a target is cached, pch mode is not supported. Fallback to the local compilation
|
||||
if (language_mode != NULL && strcmp(language_mode, precompile_header_arg_value) == 0) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,11 @@ import Foundation
|
||||
|
||||
typealias BuildSettings = [String: Any]
|
||||
|
||||
struct BuildSettingsIntegrateAppenderOption: OptionSet {
|
||||
let rawValue: Int
|
||||
|
||||
static let disableSwiftDriverIntegration = BuildSettingsIntegrateAppenderOption(rawValue: 1 << 0)
|
||||
}
|
||||
// Manages Xcode build settings
|
||||
protocol BuildSettingsIntegrateAppender {
|
||||
/// Appends XCRemoteCache-specific build settings
|
||||
@@ -34,22 +39,43 @@ class XcodeProjBuildSettingsIntegrateAppender: BuildSettingsIntegrateAppender {
|
||||
private let mode: Mode
|
||||
private let repoRoot: URL
|
||||
private let fakeSrcRoot: URL
|
||||
private let sdksExclude: [String]
|
||||
private let options: BuildSettingsIntegrateAppenderOption
|
||||
|
||||
init(mode: Mode, repoRoot: URL, fakeSrcRoot: URL) {
|
||||
init(
|
||||
mode: Mode,
|
||||
repoRoot: URL,
|
||||
fakeSrcRoot: URL,
|
||||
sdksExclude: [String],
|
||||
options: BuildSettingsIntegrateAppenderOption
|
||||
) {
|
||||
self.mode = mode
|
||||
self.repoRoot = repoRoot
|
||||
self.fakeSrcRoot = fakeSrcRoot
|
||||
self.sdksExclude = sdksExclude
|
||||
self.options = options
|
||||
}
|
||||
|
||||
func appendToBuildSettings(buildSettings: BuildSettings, wrappers: XCRCBinariesPaths) -> BuildSettings {
|
||||
var result = buildSettings
|
||||
result["SWIFT_EXEC"] = wrappers.swiftc.path
|
||||
result["SWIFT_USE_INTEGRATED_DRIVER"] = "NO"
|
||||
setBuildSetting(buildSettings: &result, key: "SWIFT_EXEC", value: wrappers.swiftc.path )
|
||||
if options.contains(.disableSwiftDriverIntegration) {
|
||||
setBuildSetting(buildSettings: &result, key: "SWIFT_USE_INTEGRATED_DRIVER", value: "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
|
||||
setBuildSetting(buildSettings: &result, key: "CC", value: wrappers.cc.path )
|
||||
setBuildSetting(buildSettings: &result, key: "LD", value: wrappers.ld.path )
|
||||
// Setting LIBTOOL to '' breaks SwiftDriver intengration so resetting it to the original value
|
||||
// 'libtool' for all excluded configurations
|
||||
setBuildSetting(
|
||||
buildSettings: &result,
|
||||
key: "LIBTOOL",
|
||||
value: wrappers.libtool.path,
|
||||
excludedValue: "libtool"
|
||||
)
|
||||
setBuildSetting(buildSettings: &result, key: "LIPO", value: wrappers.lipo.path )
|
||||
setBuildSetting(buildSettings: &result, key: "LDPLUSPLUS", value: wrappers.ldplusplus.path )
|
||||
}
|
||||
|
||||
let existingSwiftFlags = result["OTHER_SWIFT_FLAGS"] as? String
|
||||
@@ -61,14 +87,36 @@ class XcodeProjBuildSettingsIntegrateAppender: BuildSettingsIntegrateAppender {
|
||||
swiftFlags.assignFlag(key: "debug-prefix-map", value: "\(repoRoot.path)=$(XCRC_FAKE_SRCROOT)")
|
||||
clangFlags.assignFlag(key: "debug-prefix-map", value: "\(repoRoot.path)=$(XCRC_FAKE_SRCROOT)")
|
||||
|
||||
result["OTHER_SWIFT_FLAGS"] = swiftFlags.settingValue
|
||||
result["OTHER_CFLAGS"] = clangFlags.settingValue
|
||||
setBuildSetting(buildSettings: &result, key: "OTHER_SWIFT_FLAGS", value: swiftFlags.settingValue )
|
||||
setBuildSetting(buildSettings: &result, key: "OTHER_CFLAGS", value: clangFlags.settingValue )
|
||||
|
||||
result["XCRC_FAKE_SRCROOT"] = "\(fakeSrcRoot.path)"
|
||||
result["XCRC_PLATFORM_PREFERRED_ARCH"] =
|
||||
setBuildSetting(buildSettings: &result, key: "XCRC_FAKE_SRCROOT", value: "\(fakeSrcRoot.path)" )
|
||||
setBuildSetting(buildSettings: &result, key: "XCRC_PLATFORM_PREFERRED_ARCH", value:
|
||||
"""
|
||||
$(LINK_FILE_LIST_$(CURRENT_VARIANT)_$(PLATFORM_PREFERRED_ARCH):dir:standardizepath:file:default=arm64)
|
||||
"""
|
||||
)
|
||||
|
||||
explicitlyDisableSDKs(buildSettings: &result)
|
||||
return result
|
||||
}
|
||||
|
||||
private func setBuildSetting(buildSettings: inout BuildSettings, key: String, value: String?, excludedValue: String = "") {
|
||||
buildSettings[key] = value
|
||||
guard value != nil else {
|
||||
// no need to exclude as the value will
|
||||
return
|
||||
}
|
||||
// Erase all overrides for a given sdk so a default toolchain is used
|
||||
for skippedSDK in sdksExclude {
|
||||
buildSettings["\(key)[sdk=\(skippedSDK)]"] = excludedValue
|
||||
}
|
||||
}
|
||||
|
||||
// For all exlcuded SDKs passes XCRC_DISABLED=TRUE, which will cut-off early the pre_build phase
|
||||
private func explicitlyDisableSDKs(buildSettings: inout BuildSettings) {
|
||||
for skippedSDK in sdksExclude {
|
||||
buildSettings["XCRC_DISABLED[sdk=\(skippedSDK)]"] = "YES"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,14 +27,14 @@ struct IntegrateContext {
|
||||
let configOverride: URL
|
||||
let fakeSrcRoot: URL
|
||||
let output: URL?
|
||||
let buildSettingsAppenderOptions: BuildSettingsIntegrateAppenderOption
|
||||
}
|
||||
|
||||
extension IntegrateContext {
|
||||
init(
|
||||
input: String,
|
||||
repoRootPath: String,
|
||||
config: XCRemoteCacheConfig,
|
||||
mode: Mode,
|
||||
configOverridePath: String,
|
||||
env: [String: String],
|
||||
binariesDir: URL,
|
||||
fakeSrcRoot: String,
|
||||
@@ -42,20 +42,29 @@ extension IntegrateContext {
|
||||
) throws {
|
||||
projectPath = URL(fileURLWithPath: input)
|
||||
let srcRoot = projectPath.deletingLastPathComponent()
|
||||
repoRoot = URL(fileURLWithPath: repoRootPath, relativeTo: srcRoot)
|
||||
repoRoot = URL(fileURLWithPath: config.repoRoot, relativeTo: srcRoot)
|
||||
self.mode = mode
|
||||
configOverride = URL(fileURLWithPath: configOverridePath, relativeTo: srcRoot)
|
||||
configOverride = URL(fileURLWithPath: config.extraConfigurationFile, relativeTo: srcRoot)
|
||||
output = outputPath.flatMap(URL.init(fileURLWithPath:))
|
||||
self.fakeSrcRoot = URL(fileURLWithPath: fakeSrcRoot)
|
||||
var swiftcBinaryName = "swiftc"
|
||||
var buildSettingsAppenderOptions: BuildSettingsIntegrateAppenderOption = []
|
||||
// Keep the legacy behaviour (supported in Xcode 14 and lower)
|
||||
if !config.enableSwiftDriverIntegration {
|
||||
buildSettingsAppenderOptions.insert(.disableSwiftDriverIntegration)
|
||||
swiftcBinaryName = "xcswiftc"
|
||||
}
|
||||
binaries = XCRCBinariesPaths(
|
||||
prepare: binariesDir.appendingPathComponent("xcprepare"),
|
||||
cc: binariesDir.appendingPathComponent("xccc"),
|
||||
swiftc: binariesDir.appendingPathComponent("xcswiftc"),
|
||||
swiftc: binariesDir.appendingPathComponent(swiftcBinaryName),
|
||||
libtool: binariesDir.appendingPathComponent("xclibtool"),
|
||||
lipo: binariesDir.appendingPathComponent("xclipo"),
|
||||
ld: binariesDir.appendingPathComponent("xcld"),
|
||||
ldplusplus: binariesDir.appendingPathComponent("xcldplusplus"),
|
||||
prebuild: binariesDir.appendingPathComponent("xcprebuild"),
|
||||
postbuild: binariesDir.appendingPathComponent("xcpostbuild")
|
||||
)
|
||||
self.buildSettingsAppenderOptions = buildSettingsAppenderOptions
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ public class XCIntegrate {
|
||||
private let consumerEligiblePlatforms: String
|
||||
private let lldbMode: LLDBInitMode
|
||||
private let fakeSrcRoot: String
|
||||
private let sdksExclude: String
|
||||
private let output: String?
|
||||
|
||||
public init(
|
||||
@@ -48,6 +49,7 @@ public class XCIntegrate {
|
||||
consumerEligiblePlatforms: String,
|
||||
lldbMode: LLDBInitMode,
|
||||
fakeSrcRoot: String,
|
||||
sdksExclude: String,
|
||||
output: String?
|
||||
) {
|
||||
projectPath = input
|
||||
@@ -61,6 +63,7 @@ public class XCIntegrate {
|
||||
self.consumerEligiblePlatforms = consumerEligiblePlatforms
|
||||
self.lldbMode = lldbMode
|
||||
self.fakeSrcRoot = fakeSrcRoot
|
||||
self.sdksExclude = sdksExclude
|
||||
self.output = output
|
||||
}
|
||||
|
||||
@@ -79,9 +82,8 @@ public class XCIntegrate {
|
||||
|
||||
let context = try IntegrateContext(
|
||||
input: projectPath,
|
||||
repoRootPath: config.repoRoot,
|
||||
config: config,
|
||||
mode: mode,
|
||||
configOverridePath: config.extraConfigurationFile,
|
||||
env: env,
|
||||
binariesDir: binariesDir,
|
||||
fakeSrcRoot: fakeSrcRoot,
|
||||
@@ -98,7 +100,9 @@ public class XCIntegrate {
|
||||
let buildSettingsAppender = XcodeProjBuildSettingsIntegrateAppender(
|
||||
mode: context.mode,
|
||||
repoRoot: context.repoRoot,
|
||||
fakeSrcRoot: context.fakeSrcRoot
|
||||
fakeSrcRoot: context.fakeSrcRoot,
|
||||
sdksExclude: sdksExclude.integrateArrayArguments,
|
||||
options: context.buildSettingsAppenderOptions
|
||||
)
|
||||
let lldbPatcher: LLDBInitPatcher
|
||||
switch lldbMode {
|
||||
|
||||
@@ -25,6 +25,7 @@ struct XCRCBinariesPaths {
|
||||
let cc: URL
|
||||
let swiftc: URL
|
||||
let libtool: URL
|
||||
let lipo: URL
|
||||
let ld: URL
|
||||
let ldplusplus: URL
|
||||
let prebuild: URL
|
||||
|
||||
@@ -77,7 +77,18 @@ class Prepare: PrepareLogic {
|
||||
guard fileAccessor.fileExists(atPath: PhaseCacheModeController.xcodeSelectLink.path) else {
|
||||
throw PrepareError.missingXcodeSelectDirectory
|
||||
}
|
||||
let commonSha = try gitClient.getCommonPrimarySha()
|
||||
|
||||
let commonSha: String
|
||||
do {
|
||||
commonSha = try gitClient.getCommonPrimarySha()
|
||||
} catch let GitClientError.noCommonShaWithPrimaryRepo(remoteName, error) {
|
||||
guard context.gracefullyHandleMissingCommonSha else {
|
||||
throw GitClientError.noCommonShaWithPrimaryRepo(remoteName: remoteName, error: error)
|
||||
}
|
||||
infoLog("Cannot find a common sha with the primary branch: \(error). Gracefully disabling remote cache")
|
||||
try disable()
|
||||
return .failed
|
||||
}
|
||||
|
||||
if context.offline {
|
||||
// Optimistically take first common sha
|
||||
|
||||
@@ -52,6 +52,8 @@ public struct PrepareContext {
|
||||
let cacheHealthPathProbeCount: Int
|
||||
/// clang wrapper output file
|
||||
let xcccCommand: URL
|
||||
/// gracefully disable remote cache for missing common sha with the primary branch
|
||||
let gracefullyHandleMissingCommonSha: Bool
|
||||
}
|
||||
|
||||
extension PrepareContext {
|
||||
@@ -77,5 +79,6 @@ extension PrepareContext {
|
||||
cacheAddresses = try config.cacheAddresses.map(URL.build)
|
||||
cacheHealthPath = config.cacheHealthPath
|
||||
cacheHealthPathProbeCount = config.cacheHealthPathProbeCount
|
||||
gracefullyHandleMissingCommonSha = config.gracefullyHandleMissingCommonSha
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,10 +31,12 @@ public struct PrepareMarkContext {
|
||||
let recommendedCacheAddress: URL
|
||||
/// All remote servers to mark
|
||||
let cacheAddresses: [URL]
|
||||
/// XCRemoteCache is explicitly disabled
|
||||
let disabled: Bool
|
||||
}
|
||||
|
||||
extension PrepareMarkContext {
|
||||
init(_ config: XCRemoteCacheConfig) throws {
|
||||
init(_ config: XCRemoteCacheConfig, env: [String: String]) throws {
|
||||
let sourceRoot = URL(fileURLWithPath: config.sourceRoot, isDirectory: true)
|
||||
repoRoot = URL(fileURLWithPath: config.repoRoot, relativeTo: sourceRoot)
|
||||
guard let address = URL(string: config.recommendedCacheAddress) else {
|
||||
@@ -43,5 +45,6 @@ extension PrepareMarkContext {
|
||||
}
|
||||
recommendedCacheAddress = address
|
||||
cacheAddresses = try config.cacheAddresses.map(URL.build)
|
||||
disabled = try env.readEnv(key: "XCRC_DISABLED") ?? false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,12 +47,17 @@ public class XCPrepareMark {
|
||||
let xcodeVersion: String
|
||||
do {
|
||||
config = try XCRemoteCacheConfigReader(env: env, fileReader: fileManager).readConfiguration()
|
||||
context = try PrepareMarkContext(config)
|
||||
context = try PrepareMarkContext(config, env: env)
|
||||
xcodeVersion = try xcode ?? XcodeProbeImpl(shell: shellGetStdout).read().buildVersion
|
||||
} catch {
|
||||
exit(1, "FATAL: Prepare initialization failed with error: \(error)")
|
||||
}
|
||||
|
||||
guard !context.disabled else {
|
||||
infoLog("XCRemoteCache explicitly disabled for marking.")
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let sessionFactory = DefaultURLSessionFactory(config: config)
|
||||
var awsV4Signature: AWSV4Signature?
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
// 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
|
||||
|
||||
enum SwiftFrontendArgInputError: Error, Equatable {
|
||||
// swift-frontend should either be compling or emiting a module
|
||||
case bothCompilationAndEmitAction
|
||||
// no .swift files have been passed as input files
|
||||
case noCompilationInputs
|
||||
// no -primary-file .swift files have been passed as input files
|
||||
case noPrimaryFileCompilationInputs
|
||||
// number of -emit-dependencies-path doesn't match compilation inputs
|
||||
case dependenciesOuputCountDoesntMatch(expected: Int, parsed: Int)
|
||||
// number of -serialize-diagnostics-path doesn't match compilation inputs
|
||||
case diagnosticsOuputCountDoesntMatch(expected: Int, parsed: Int)
|
||||
// number of -o doesn't match compilation inputs
|
||||
case outputsOuputCountDoesntMatch(expected: Int, parsed: Int)
|
||||
// number of -o for emit-module can be only 1
|
||||
case emitModulOuputCountIsNot1(parsed: Int)
|
||||
// number of -emit-dependencies-path for emit-module can be 0 or 1 (generate or not)
|
||||
case emitModuleDependenciesOuputCountIsHigherThan1(parsed: Int)
|
||||
// number of -serialize-diagnostics-path for emit-module can be 0 or 1 (generate or not)
|
||||
case emitModuleDiagnosticsOuputCountIsHigherThan1(parsed: Int)
|
||||
// emit-module requires -emit-objc-header-path
|
||||
case emitModuleMissingObjcHeaderPath
|
||||
// -target is required
|
||||
case emitMissingTarget
|
||||
// -moduleName is required
|
||||
case emitMissingModuleName
|
||||
}
|
||||
|
||||
public struct SwiftFrontendArgInput {
|
||||
let compile: Bool
|
||||
let emitModule: Bool
|
||||
let objcHeaderOutput: String?
|
||||
let moduleName: String?
|
||||
let target: String?
|
||||
let primaryInputPaths: [String]
|
||||
let inputPaths: [String]
|
||||
var outputPaths: [String]
|
||||
var dependenciesPaths: [String]
|
||||
// Extra params
|
||||
// Diagnostics are not supported yet in the XCRemoteCache (cached artifacts assumes no warnings)
|
||||
var diagnosticsPaths: [String]
|
||||
// Unsed for now:
|
||||
// .swiftsourceinfo and .swiftdoc will be placed next to the .swiftmodule
|
||||
let sourceInfoPath: String?
|
||||
let docPath: String?
|
||||
// Passed as -supplementary-output-file-map
|
||||
let supplementaryOutputFileMap: String?
|
||||
|
||||
/// Manual initializer implementation required to be public
|
||||
public init(
|
||||
compile: Bool,
|
||||
emitModule: Bool,
|
||||
objcHeaderOutput: String?,
|
||||
moduleName: String?,
|
||||
target: String?,
|
||||
primaryInputPaths: [String],
|
||||
inputPaths: [String],
|
||||
outputPaths: [String],
|
||||
dependenciesPaths: [String],
|
||||
diagnosticsPaths: [String],
|
||||
sourceInfoPath: String?,
|
||||
docPath: String?,
|
||||
supplementaryOutputFileMap: String?
|
||||
) {
|
||||
self.compile = compile
|
||||
self.emitModule = emitModule
|
||||
self.objcHeaderOutput = objcHeaderOutput
|
||||
self.moduleName = moduleName
|
||||
self.target = target
|
||||
self.primaryInputPaths = primaryInputPaths
|
||||
self.inputPaths = inputPaths
|
||||
self.outputPaths = outputPaths
|
||||
self.dependenciesPaths = dependenciesPaths
|
||||
self.diagnosticsPaths = diagnosticsPaths
|
||||
self.sourceInfoPath = sourceInfoPath
|
||||
self.docPath = docPath
|
||||
self.supplementaryOutputFileMap = supplementaryOutputFileMap
|
||||
}
|
||||
|
||||
private func generateForCompilation(
|
||||
config: XCRemoteCacheConfig,
|
||||
target: String,
|
||||
moduleName: String
|
||||
) throws -> SwiftcContext {
|
||||
let primaryInputsCount = primaryInputPaths.count
|
||||
|
||||
guard primaryInputsCount > 0 else {
|
||||
throw SwiftFrontendArgInputError.noPrimaryFileCompilationInputs
|
||||
}
|
||||
guard [primaryInputsCount, 0].contains(dependenciesPaths.count) else {
|
||||
throw SwiftFrontendArgInputError.dependenciesOuputCountDoesntMatch(
|
||||
expected: primaryInputsCount,
|
||||
parsed: dependenciesPaths.count
|
||||
)
|
||||
}
|
||||
guard [primaryInputsCount, 0].contains(diagnosticsPaths.count) else {
|
||||
throw SwiftFrontendArgInputError.diagnosticsOuputCountDoesntMatch(
|
||||
expected: primaryInputsCount,
|
||||
parsed: diagnosticsPaths.count
|
||||
)
|
||||
}
|
||||
guard outputPaths.count == primaryInputsCount else {
|
||||
throw SwiftFrontendArgInputError.outputsOuputCountDoesntMatch(
|
||||
expected: primaryInputsCount,
|
||||
parsed: outputPaths.count
|
||||
)
|
||||
}
|
||||
let primaryInputFilesURLs: [URL] = primaryInputPaths.map(URL.init(fileURLWithPath:))
|
||||
|
||||
let steps: SwiftcContext.SwiftcSteps = SwiftcContext.SwiftcSteps(
|
||||
compileFilesScope: .subset(primaryInputFilesURLs),
|
||||
emitModule: nil
|
||||
)
|
||||
|
||||
let compilationFilesInputs = buildCompilationFilesInputs(
|
||||
primaryInputsCount: primaryInputsCount,
|
||||
primaryInputFilesURLs: primaryInputFilesURLs
|
||||
)
|
||||
|
||||
return try .init(
|
||||
config: config,
|
||||
moduleName: moduleName,
|
||||
steps: steps,
|
||||
inputs: compilationFilesInputs,
|
||||
target: target,
|
||||
compilationFiles: .list(inputPaths),
|
||||
exampleWorkspaceFilePath: outputPaths[0]
|
||||
)
|
||||
}
|
||||
|
||||
private func buildCompilationFilesInputs(
|
||||
primaryInputsCount: Int,
|
||||
primaryInputFilesURLs: [URL]
|
||||
) -> SwiftcContext.CompilationFilesInputs {
|
||||
if let compimentaryFileMa = supplementaryOutputFileMap {
|
||||
return .supplementaryFileMap(compimentaryFileMa)
|
||||
} else {
|
||||
return .map((0..<primaryInputsCount).reduce(
|
||||
[String: SwiftFileCompilationInfo]()
|
||||
) { prev, i in
|
||||
var new = prev
|
||||
new[primaryInputPaths[i]] = SwiftFileCompilationInfo(
|
||||
file: primaryInputFilesURLs[i],
|
||||
dependencies: dependenciesPaths.get(i).map(URL.init(fileURLWithPath:)),
|
||||
object: outputPaths.get(i).map(URL.init(fileURLWithPath:)),
|
||||
// for now - swift-dependencies are not requested in the driver compilation mode
|
||||
swiftDependencies: nil
|
||||
)
|
||||
return new
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func generateForEmitModule(
|
||||
config: XCRemoteCacheConfig,
|
||||
target: String,
|
||||
moduleName: String
|
||||
) throws -> SwiftcContext {
|
||||
guard outputPaths.count == 1 else {
|
||||
throw SwiftFrontendArgInputError.emitModulOuputCountIsNot1(parsed: outputPaths.count)
|
||||
}
|
||||
guard let objcHeaderOutput = objcHeaderOutput else {
|
||||
throw SwiftFrontendArgInputError.emitModuleMissingObjcHeaderPath
|
||||
}
|
||||
guard diagnosticsPaths.count <= 1 else {
|
||||
throw SwiftFrontendArgInputError.emitModuleDiagnosticsOuputCountIsHigherThan1(
|
||||
parsed: diagnosticsPaths.count
|
||||
)
|
||||
}
|
||||
guard dependenciesPaths.count <= 1 else {
|
||||
throw SwiftFrontendArgInputError.emitModuleDependenciesOuputCountIsHigherThan1(
|
||||
parsed: dependenciesPaths.count
|
||||
)
|
||||
}
|
||||
|
||||
let steps: SwiftcContext.SwiftcSteps = SwiftcContext.SwiftcSteps(
|
||||
compileFilesScope: .none,
|
||||
emitModule: SwiftcContext.SwiftcStepEmitModule(
|
||||
objcHeaderOutput: URL(fileURLWithPath: objcHeaderOutput),
|
||||
modulePathOutput: URL(fileURLWithPath: outputPaths[0]),
|
||||
dependencies: dependenciesPaths.first.map(URL.init(fileURLWithPath:))
|
||||
)
|
||||
)
|
||||
return try .init(
|
||||
config: config,
|
||||
moduleName: moduleName,
|
||||
steps: steps,
|
||||
inputs: .map([:]),
|
||||
target: target,
|
||||
compilationFiles: .list(inputPaths),
|
||||
exampleWorkspaceFilePath: objcHeaderOutput
|
||||
)
|
||||
}
|
||||
|
||||
func generateSwiftcContext(config: XCRemoteCacheConfig) throws -> SwiftcContext {
|
||||
guard compile != emitModule else {
|
||||
throw SwiftFrontendArgInputError.bothCompilationAndEmitAction
|
||||
}
|
||||
let inputPathsCount = inputPaths.count
|
||||
guard inputPathsCount > 0 else {
|
||||
throw SwiftFrontendArgInputError.noCompilationInputs
|
||||
}
|
||||
guard let target = target else {
|
||||
throw SwiftFrontendArgInputError.emitMissingTarget
|
||||
}
|
||||
guard let moduleName = moduleName else {
|
||||
throw SwiftFrontendArgInputError.emitMissingModuleName
|
||||
}
|
||||
|
||||
if compile {
|
||||
return try generateForCompilation(
|
||||
config: config,
|
||||
target: target,
|
||||
moduleName: moduleName
|
||||
)
|
||||
} else {
|
||||
return try generateForEmitModule(
|
||||
config: config,
|
||||
target: target,
|
||||
moduleName: moduleName
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// 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
|
||||
|
||||
struct SwiftFrontendContext {
|
||||
/// File lock used for synchronizing multiple invocations
|
||||
let invocationLockFile: URL
|
||||
}
|
||||
|
||||
extension SwiftFrontendContext {
|
||||
init(_ swiftcContext: SwiftcContext, env: [String: String]) throws {
|
||||
/// The LLBUILD_BUILD_ID ENV that describes the entire (incl. parent's swiftc) bui;d
|
||||
let llbuildId: String = try env.readEnv(key: "LLBUILD_BUILD_ID")
|
||||
invocationLockFile = Self.self.buildLlbuildIdSharedLockUrl(
|
||||
llbuildId: llbuildId,
|
||||
tmpDir: swiftcContext.tempDir
|
||||
)
|
||||
}
|
||||
|
||||
/// Generate the filename to be used to synchronize multiple swift-frontend invocations
|
||||
/// The same file is used in prebuild, xcswift-frontend and postbuild (to clean it up)
|
||||
static func buildLlbuildIdSharedLockUrl(llbuildId: String, tmpDir: URL) -> URL {
|
||||
return tmpDir.appendingPathComponent(llbuildId).appendingPathExtension("lock")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
// 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
|
||||
|
||||
/// Manages the `swift-frontend` logic
|
||||
protocol SwiftFrontendOrchestrator {
|
||||
/// Executes the critical section according to the required order
|
||||
/// - Parameter criticalSection: the block that should be synchronized
|
||||
func run(criticalSection: () -> Void ) throws
|
||||
}
|
||||
|
||||
/// The default orchestrator that manages the order or swift-frontend invocations
|
||||
/// For emit-module (the "first" process) action, it locks a shared file between all swift-frontend invocations,
|
||||
/// verifies that the mocking can be done and continues the mocking/fall-backing along the lock release
|
||||
/// For the compilation action, tries to acquire a lock and waits until the "emit-module" makes a decision
|
||||
/// if the compilation should be skipped and a "mocking" should used instead
|
||||
class CommonSwiftFrontendOrchestrator {
|
||||
/// Content saved to the shared file
|
||||
/// Safe to use forced unwrapping
|
||||
private static let emitModuleContent = "done".data(using: .utf8)!
|
||||
|
||||
enum Action {
|
||||
case emitModule
|
||||
case compile
|
||||
}
|
||||
private let mode: SwiftcContext.SwiftcMode
|
||||
private let action: Action
|
||||
private let lockAccessor: ExclusiveFileAccessor
|
||||
private let maxLockTimeout: TimeInterval
|
||||
|
||||
init(
|
||||
mode: SwiftcContext.SwiftcMode,
|
||||
action: Action,
|
||||
lockAccessor: ExclusiveFileAccessor,
|
||||
maxLockTimeout: TimeInterval
|
||||
) {
|
||||
self.mode = mode
|
||||
self.action = action
|
||||
self.lockAccessor = lockAccessor
|
||||
self.maxLockTimeout = maxLockTimeout
|
||||
}
|
||||
|
||||
func run(criticalSection: () throws -> Void) throws {
|
||||
guard case .consumer(commit: .available) = mode else {
|
||||
// no need to lock anything - just allow fallbacking to the `swiftc or swift-frontend`
|
||||
// for a producer or a consumer where RC is disabled (we have already caught the
|
||||
// cache miss)
|
||||
try criticalSection()
|
||||
return
|
||||
}
|
||||
try executeMockAttemp(criticalSection: criticalSection)
|
||||
}
|
||||
|
||||
private func executeMockAttemp(criticalSection: () throws -> Void) throws {
|
||||
switch action {
|
||||
case .emitModule:
|
||||
try validateEmitModuleStep(criticalSection: criticalSection)
|
||||
case .compile:
|
||||
try waitForEmitModuleLock(criticalSection: criticalSection)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// For emit-module, wrap the critical section with the shared lock so other processes (compilation)
|
||||
/// have to wait until emit-module finishes
|
||||
/// Once the emit-module is done, the "magical" string is saved to the file and the lock is released
|
||||
///
|
||||
/// Note: The design of wrapping the entire "emit-module" has a small performance downside if inside
|
||||
/// the critical section, the code realizes that remote cache cannot be used
|
||||
/// (in practice - a new file has been added)
|
||||
/// None of compilation process (so with '-c' args) can continue until the entire emit-module logic finishes
|
||||
/// Because it is expected to happen not that often and emit-module is usually quite fast, this makes the
|
||||
/// implementation way simpler. If we ever want to optimize it, we should release the lock as early
|
||||
/// as we know, the remote cache cannot be used. Then all other compilation process (-c) can run
|
||||
/// in parallel with emit-module
|
||||
private func validateEmitModuleStep(criticalSection: () throws -> Void) throws {
|
||||
debugLog("starting the emit-module step: locking")
|
||||
try lockAccessor.exclusiveAccess { handle in
|
||||
debugLog("starting the emit-module step: locked")
|
||||
// writing to the file content proactively - incase the critical section never returns
|
||||
// (in case of a fallback to the local compilation), all awaiting swift-frontend processes
|
||||
// will be immediately unblocked
|
||||
handle.write(Self.self.emitModuleContent)
|
||||
try criticalSection()
|
||||
debugLog("lock file emit-module criticial end")
|
||||
}
|
||||
}
|
||||
|
||||
/// Locks a shared file in a loop until its content is non-empty - meaning the "parent" emit-module
|
||||
/// has already finished
|
||||
private func waitForEmitModuleLock(criticalSection: () throws -> Void) throws {
|
||||
// emit-module process should really quickly obtain a lock (it is always invoked
|
||||
// by Xcode as a first process)
|
||||
var executed = false
|
||||
let startingDate = Date()
|
||||
while !executed {
|
||||
debugLog("lock file compilation trying to acquire a lock ....")
|
||||
try lockAccessor.exclusiveAccess { handle in
|
||||
if !handle.availableData.isEmpty {
|
||||
// the file is not empty so the emit-module process is done with the "check"
|
||||
debugLog("swift-frontend lock file is unlocked for compilation")
|
||||
try criticalSection()
|
||||
executed = true
|
||||
} else {
|
||||
debugLog("swift-frontend lock file is not ready for compilation")
|
||||
}
|
||||
}
|
||||
// When a max locking time is achieved, execute anyway
|
||||
if !executed && Date().timeIntervalSince(startingDate) > self.maxLockTimeout {
|
||||
errorLog("""
|
||||
Executing command \(action) without lock synchronization. That may be cause by the\
|
||||
crashed or extremely long emit-module. Contact XCRemoteCache authors about this error.
|
||||
""")
|
||||
try criticalSection()
|
||||
executed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// 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
|
||||
|
||||
public class XCSwiftFrontend: XCSwiftAbstract<SwiftFrontendArgInput> {
|
||||
// don't lock individual compilation invocations for more than 10s
|
||||
private static let MaxLockingTimeout: TimeInterval = 10
|
||||
private let env: [String: String]
|
||||
|
||||
public init(
|
||||
command: String,
|
||||
inputArgs: SwiftFrontendArgInput,
|
||||
env: [String: String],
|
||||
dependenciesWriter: @escaping (URL, FileManager) -> DependenciesWriter,
|
||||
touchFactory: @escaping (URL, FileManager) -> Touch
|
||||
) throws {
|
||||
self.env = env
|
||||
super.init(
|
||||
command: command,
|
||||
inputArgs: inputArgs,
|
||||
dependenciesWriter: dependenciesWriter,
|
||||
touchFactory: touchFactory
|
||||
)
|
||||
}
|
||||
|
||||
override func buildContext() throws -> (XCRemoteCacheConfig, SwiftcContext) {
|
||||
let fileManager = FileManager.default
|
||||
let config: XCRemoteCacheConfig
|
||||
let context: SwiftcContext
|
||||
|
||||
let srcRoot: URL = URL(fileURLWithPath: fileManager.currentDirectoryPath)
|
||||
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileReader: fileManager)
|
||||
.readConfiguration()
|
||||
context = try SwiftcContext(config: config, input: inputArgs)
|
||||
// do not cache this context, as it is subject to change when
|
||||
// the emit-module finds that the cached artifact cannot be used
|
||||
return (config, context)
|
||||
}
|
||||
|
||||
override public func run() throws {
|
||||
do {
|
||||
let (_, context) = try buildContext()
|
||||
|
||||
let frontendContext = try SwiftFrontendContext(context, env: env)
|
||||
let sharedLock = ExclusiveFile(frontendContext.invocationLockFile, mode: .override)
|
||||
|
||||
let action: CommonSwiftFrontendOrchestrator.Action = inputArgs.emitModule ? .emitModule : .compile
|
||||
let swiftFrontendOrchestrator = CommonSwiftFrontendOrchestrator(
|
||||
mode: context.mode,
|
||||
action: action,
|
||||
lockAccessor: sharedLock,
|
||||
maxLockTimeout: Self.self.MaxLockingTimeout
|
||||
)
|
||||
|
||||
try swiftFrontendOrchestrator.run(criticalSection: super.run)
|
||||
} catch {
|
||||
// Splitting into 2 invocations as os_log truncates a massage
|
||||
defaultLog("Cannot correctly orchestrate the \(command) with params \(inputArgs)")
|
||||
defaultLog("Cannot correctly orchestrate error: \(error)")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// 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
|
||||
|
||||
/// Decides if an input to the compilation step should allow reusing the cached artifact
|
||||
protocol AllowedInputDeterminer {
|
||||
/// Decides if the input file is allowed to be compiled, even not specified in the dependency list
|
||||
func allowedNonDependencyInput(file: URL) -> Bool
|
||||
}
|
||||
|
||||
class FilenameBasedAllowedInputDeterminer: AllowedInputDeterminer {
|
||||
private let filenames: [String]
|
||||
|
||||
init(_ filenames: [String]) {
|
||||
self.filenames = filenames
|
||||
}
|
||||
|
||||
func allowedNonDependencyInput(file: URL) -> Bool {
|
||||
return filenames.contains(file.lastPathComponent)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// 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
|
||||
|
||||
/// Products generator that doesn't create any swiftmodule. It is used in the compilation swift-frontend mocking, where
|
||||
/// only individual .o files are created and not .swiftmodule of -Swift.h
|
||||
/// (which is part of swift-frontend -emit-module invocation)
|
||||
class NoopSwiftcProductsGenerator: SwiftcProductsGenerator {
|
||||
func generateFrom(
|
||||
artifactSwiftModuleFiles: [SwiftmoduleFileExtension: URL],
|
||||
artifactSwiftModuleObjCFile: URL
|
||||
) throws -> SwiftcProductsGeneratorOutput {
|
||||
infoLog("""
|
||||
Invoking module generation from NoopSwiftcProductsGenerator does nothing. \
|
||||
It might be a side-effect of a plugin asking to generate a module.
|
||||
""")
|
||||
// NoopSwiftcProductsGenerator is intended only for the swift-frontend
|
||||
let trivialURL = URL(fileURLWithPath: "/non-existing")
|
||||
return SwiftcProductsGeneratorOutput(swiftmoduleDir: trivialURL, objcHeaderFile: trivialURL)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// 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
|
||||
|
||||
class StaticSwiftcInputReader: SwiftcInputReader {
|
||||
private let moduleDependencies: URL?
|
||||
private let swiftDependencies: URL?
|
||||
private let compilationFiles: [SwiftFileCompilationInfo]
|
||||
|
||||
init(
|
||||
moduleDependencies: URL?,
|
||||
swiftDependencies: URL?,
|
||||
compilationFiles: [SwiftFileCompilationInfo]
|
||||
) {
|
||||
self.moduleDependencies = moduleDependencies
|
||||
self.swiftDependencies = swiftDependencies
|
||||
self.compilationFiles = compilationFiles
|
||||
}
|
||||
|
||||
func read() throws -> SwiftCompilationInfo {
|
||||
return .init(
|
||||
info: .init(
|
||||
dependencies: moduleDependencies,
|
||||
swiftDependencies: swiftDependencies
|
||||
),
|
||||
files: compilationFiles
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,7 @@ class Swiftc: SwiftcProtocol {
|
||||
private let dependenciesWriterFactory: (URL, FileManager) -> DependenciesWriter
|
||||
private let touchFactory: (URL, FileManager) -> Touch
|
||||
private let plugins: [SwiftcProductGenerationPlugin]
|
||||
private let allowedInputDeterminer: AllowedInputDeterminer
|
||||
|
||||
init(
|
||||
inputFileListReader: ListReader,
|
||||
@@ -70,7 +71,8 @@ class Swiftc: SwiftcProtocol {
|
||||
fileManager: FileManager,
|
||||
dependenciesWriterFactory: @escaping (URL, FileManager) -> DependenciesWriter,
|
||||
touchFactory: @escaping (URL, FileManager) -> Touch,
|
||||
plugins: [SwiftcProductGenerationPlugin]
|
||||
plugins: [SwiftcProductGenerationPlugin],
|
||||
allowedInputDeterminer: AllowedInputDeterminer
|
||||
) {
|
||||
self.inputFileListReader = inputFileListReader
|
||||
self.markerReader = markerReader
|
||||
@@ -84,6 +86,7 @@ class Swiftc: SwiftcProtocol {
|
||||
self.dependenciesWriterFactory = dependenciesWriterFactory
|
||||
self.touchFactory = touchFactory
|
||||
self.plugins = plugins
|
||||
self.allowedInputDeterminer = allowedInputDeterminer
|
||||
}
|
||||
|
||||
// swiftlint:disable:next function_body_length
|
||||
@@ -96,13 +99,17 @@ class Swiftc: SwiftcProtocol {
|
||||
|
||||
let inputFilesInputs = try inputFileListReader.listFilesURLs()
|
||||
let markerAllowedFiles = try markerReader.listFilesURLs()
|
||||
let allDependencies = Set(markerAllowedFiles + inputFilesInputs)
|
||||
let cachedDependenciesWriterFactory = CachedFileDependenciesWriterFactory(
|
||||
dependencies: markerAllowedFiles,
|
||||
dependencies: Array(allDependencies),
|
||||
fileManager: fileManager,
|
||||
writerFactory: dependenciesWriterFactory
|
||||
)
|
||||
// Verify all input files to be present in a marker fileList
|
||||
let disallowedInputs = try inputFilesInputs.filter { try !allowedFilesListScanner.contains($0) }
|
||||
let disallowedInputs = try inputFilesInputs.filter { file in
|
||||
try !allowedFilesListScanner.contains(file) &&
|
||||
!allowedInputDeterminer.allowedNonDependencyInput(file: file)
|
||||
}
|
||||
|
||||
if !disallowedInputs.isEmpty {
|
||||
// New file (disallowedFile) added without modifying the rest of the feature. Fallback to swiftc and
|
||||
@@ -132,7 +139,7 @@ class Swiftc: SwiftcProtocol {
|
||||
|
||||
// Read swiftmodule location from XCRemoteCache
|
||||
// arbitrary format swiftmodule/${arch}/${moduleName}.swift{module|doc|sourceinfo}
|
||||
let moduleName = context.modulePathOutput.deletingPathExtension().lastPathComponent
|
||||
let moduleName = context.moduleName
|
||||
let allCompilations = try inputFilesReader.read()
|
||||
let artifactSwiftmoduleDir = artifactLocation
|
||||
.appendingPathComponent("swiftmodule")
|
||||
@@ -145,20 +152,24 @@ class Swiftc: SwiftcProtocol {
|
||||
}
|
||||
)
|
||||
|
||||
// Build -Swift.h location from XCRemoteCache arbitrary format include/${arch}/${target}-Swift.h
|
||||
let artifactSwiftModuleObjCDir = artifactLocation
|
||||
.appendingPathComponent("include")
|
||||
.appendingPathComponent(context.arch)
|
||||
.appendingPathComponent(context.moduleName)
|
||||
// Move cached xxxx-Swift.h to the location passed in arglist
|
||||
// Alternatively, artifactSwiftModuleObjCFile could be built as a first .h file in artifactSwiftModuleObjCDir
|
||||
let artifactSwiftModuleObjCFile = artifactSwiftModuleObjCDir
|
||||
.appendingPathComponent(context.objcHeaderOutput.lastPathComponent)
|
||||
// emit module (if requested)
|
||||
if let emitModule = context.steps.emitModule {
|
||||
// Build -Swift.h location from XCRemoteCache arbitrary format include/${arch}/${target}-Swift.h
|
||||
let artifactSwiftModuleObjCDir = artifactLocation
|
||||
.appendingPathComponent("include")
|
||||
.appendingPathComponent(context.arch)
|
||||
.appendingPathComponent(context.moduleName)
|
||||
// Move cached xxxx-Swift.h to the location passed in arglist
|
||||
// Alternatively, artifactSwiftModuleObjCFile could be built as a first .h
|
||||
// file in artifactSwiftModuleObjCDir
|
||||
let artifactSwiftModuleObjCFile = artifactSwiftModuleObjCDir
|
||||
.appendingPathComponent(emitModule.objcHeaderOutput.lastPathComponent)
|
||||
|
||||
_ = try productsGenerator.generateFrom(
|
||||
artifactSwiftModuleFiles: artifactSwiftmoduleFiles,
|
||||
artifactSwiftModuleObjCFile: artifactSwiftModuleObjCFile
|
||||
)
|
||||
_ = try productsGenerator.generateFrom(
|
||||
artifactSwiftModuleFiles: artifactSwiftmoduleFiles,
|
||||
artifactSwiftModuleObjCFile: artifactSwiftModuleObjCFile
|
||||
)
|
||||
}
|
||||
|
||||
try plugins.forEach {
|
||||
try $0.generate(for: allCompilations)
|
||||
@@ -176,8 +187,10 @@ class Swiftc: SwiftcProtocol {
|
||||
try cachedDependenciesWriterFactory.generate(output: individualDeps)
|
||||
}
|
||||
}
|
||||
// Save .d for the entire module
|
||||
try cachedDependenciesWriterFactory.generate(output: allCompilations.info.swiftDependencies)
|
||||
// Save .d for the entire module (might not be required in the `swift-frontend -c` mode)
|
||||
if let swiftDependencies = allCompilations.info.swiftDependencies {
|
||||
try cachedDependenciesWriterFactory.generate(output: swiftDependencies)
|
||||
}
|
||||
// Generate .d file with all deps in the "-master.d" (e.g. for WMO)
|
||||
if let wmoDeps = allCompilations.info.dependencies {
|
||||
try cachedDependenciesWriterFactory.generate(output: wmoDeps)
|
||||
|
||||
@@ -20,6 +20,53 @@
|
||||
import Foundation
|
||||
|
||||
public struct SwiftcContext {
|
||||
/// Describes the action if the module emit should happen
|
||||
/// that generates .swiftmodule and/or -Swift.h
|
||||
public struct SwiftcStepEmitModule: Equatable {
|
||||
// where the -Swift.h should be placed
|
||||
let objcHeaderOutput: URL
|
||||
// where should the .swiftmodule be placed
|
||||
let modulePathOutput: URL
|
||||
// might be passed as an explicit argument in the swiftc
|
||||
// -emit-dependencies-path
|
||||
let dependencies: URL?
|
||||
}
|
||||
|
||||
/// Which files (from the list of all files in the module)
|
||||
/// should be compiled in this process
|
||||
public enum SwiftcStepCompileFilesScope: Equatable {
|
||||
/// used if only emit module should be done
|
||||
case none
|
||||
case all
|
||||
case subset([URL])
|
||||
}
|
||||
|
||||
/// Describes which steps should be done as a part of this process
|
||||
public struct SwiftcSteps: Equatable {
|
||||
/// which files should be compiled
|
||||
let compileFilesScope: SwiftcStepCompileFilesScope
|
||||
/// if a module should be generated
|
||||
let emitModule: SwiftcStepEmitModule?
|
||||
}
|
||||
|
||||
/// Defines how a list of input files (*.swift) is passed to the invocation
|
||||
public enum CompilationFilesSource: Equatable {
|
||||
/// defined in a separate file (via @/.../*.SwiftFileList)
|
||||
case fileList(String)
|
||||
/// explicitly passed a list of files
|
||||
case list([String])
|
||||
}
|
||||
|
||||
/// Defines how a list of output files (*.d, *.o etc.) is passed to the invocation
|
||||
public enum CompilationFilesInputs: Equatable {
|
||||
/// defined in a separate file (via -output-file-map)
|
||||
case fileMap(String)
|
||||
/// defined in a separate file (via -supplementary-output-file-map)
|
||||
case supplementaryFileMap(String)
|
||||
/// explicitly passed in the invocation
|
||||
case map([String: SwiftFileCompilationInfo])
|
||||
}
|
||||
|
||||
enum SwiftcMode: Equatable {
|
||||
case producer
|
||||
/// Commit sha of the commit to use during remote cache
|
||||
@@ -28,14 +75,13 @@ public struct SwiftcContext {
|
||||
case producerFast
|
||||
}
|
||||
|
||||
let objcHeaderOutput: URL
|
||||
let steps: SwiftcSteps
|
||||
let moduleName: String
|
||||
let modulePathOutput: URL
|
||||
/// File that defines output files locations (.d, .swiftmodule etc.)
|
||||
let filemap: URL
|
||||
/// A source that defines output files locations (.d, .swiftmodule etc.)
|
||||
let inputs: CompilationFilesInputs
|
||||
let target: String
|
||||
/// File that contains input files for the swift module compilation
|
||||
let fileList: URL
|
||||
/// A source that contains all input files for the swift module compilation
|
||||
let compilationFiles: CompilationFilesSource
|
||||
let tempDir: URL
|
||||
let arch: String
|
||||
let prebuildDependenciesPath: String
|
||||
@@ -43,29 +89,29 @@ public struct SwiftcContext {
|
||||
/// File that stores all compilation invocation arguments
|
||||
let invocationHistoryFile: URL
|
||||
|
||||
|
||||
public init(
|
||||
config: XCRemoteCacheConfig,
|
||||
objcHeaderOutput: String,
|
||||
moduleName: String,
|
||||
modulePathOutput: String,
|
||||
filemap: String,
|
||||
steps: SwiftcSteps,
|
||||
inputs: CompilationFilesInputs,
|
||||
target: String,
|
||||
fileList: String
|
||||
compilationFiles: CompilationFilesSource,
|
||||
/// any workspace file path - all other intermediate files for this compilation
|
||||
/// are placed next to it. This path is used to infer the arch and TARGET_TEMP_DIR
|
||||
exampleWorkspaceFilePath: String
|
||||
) throws {
|
||||
self.objcHeaderOutput = URL(fileURLWithPath: objcHeaderOutput)
|
||||
self.moduleName = moduleName
|
||||
self.modulePathOutput = URL(fileURLWithPath: modulePathOutput)
|
||||
self.filemap = URL(fileURLWithPath: filemap)
|
||||
self.steps = steps
|
||||
self.inputs = inputs
|
||||
self.target = target
|
||||
self.fileList = URL(fileURLWithPath: fileList)
|
||||
// modulePathOutput is place in $TARGET_TEMP_DIR/Objects-normal/$ARCH/$TARGET_NAME.swiftmodule
|
||||
self.compilationFiles = compilationFiles
|
||||
// exampleWorkspaceFilePath has a format $TARGET_TEMP_DIR/Objects-normal/$ARCH/some.file
|
||||
// That may be subject to change for other Xcode versions
|
||||
tempDir = URL(fileURLWithPath: modulePathOutput)
|
||||
tempDir = URL(fileURLWithPath: exampleWorkspaceFilePath)
|
||||
.deletingLastPathComponent()
|
||||
.deletingLastPathComponent()
|
||||
.deletingLastPathComponent()
|
||||
arch = URL(fileURLWithPath: modulePathOutput).deletingLastPathComponent().lastPathComponent
|
||||
arch = URL(fileURLWithPath: exampleWorkspaceFilePath).deletingLastPathComponent().lastPathComponent
|
||||
|
||||
let srcRoot: URL = URL(fileURLWithPath: config.sourceRoot)
|
||||
let remoteCommitLocation = URL(fileURLWithPath: config.remoteCommitFile, relativeTo: srcRoot)
|
||||
@@ -92,14 +138,32 @@ public struct SwiftcContext {
|
||||
config: XCRemoteCacheConfig,
|
||||
input: SwiftcArgInput
|
||||
) throws {
|
||||
let steps = SwiftcSteps(
|
||||
compileFilesScope: .all,
|
||||
emitModule: SwiftcStepEmitModule(
|
||||
objcHeaderOutput: URL(fileURLWithPath: (input.objcHeaderOutput)),
|
||||
modulePathOutput: URL(fileURLWithPath: input.modulePathOutput),
|
||||
// in `swiftc`, .d dependencies are pass in the output filemap
|
||||
dependencies: nil
|
||||
)
|
||||
)
|
||||
let inputs = CompilationFilesInputs.fileMap(input.filemap)
|
||||
let compilationFiles = CompilationFilesSource.fileList(input.fileList)
|
||||
try self.init(
|
||||
config: config,
|
||||
objcHeaderOutput: input.objcHeaderOutput,
|
||||
moduleName: input.moduleName,
|
||||
modulePathOutput: input.modulePathOutput,
|
||||
filemap: input.filemap,
|
||||
steps: steps,
|
||||
inputs: inputs,
|
||||
target: input.target,
|
||||
fileList: input.fileList
|
||||
compilationFiles: compilationFiles,
|
||||
exampleWorkspaceFilePath: input.modulePathOutput
|
||||
)
|
||||
}
|
||||
|
||||
init(
|
||||
config: XCRemoteCacheConfig,
|
||||
input: SwiftFrontendArgInput
|
||||
) throws {
|
||||
self = try input.generateSwiftcContext(config: config)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,16 @@
|
||||
// under the License.
|
||||
|
||||
import Foundation
|
||||
import Yams
|
||||
|
||||
/// Errors with reading swiftc inputs
|
||||
enum SwiftcInputReaderError: Error {
|
||||
case readingFailed
|
||||
case invalidFormat
|
||||
/// The file is not in the yaml format
|
||||
case invalidYamlFormat
|
||||
/// The yaml string contains illegal characters
|
||||
case invalidYamlString
|
||||
case missingField(String)
|
||||
}
|
||||
|
||||
@@ -45,10 +50,11 @@ struct SwiftCompilationInfo: Encodable, Equatable {
|
||||
struct SwiftModuleCompilationInfo: Encodable, Equatable {
|
||||
// not present for incremental builds
|
||||
let dependencies: URL?
|
||||
let swiftDependencies: URL
|
||||
// might be nil for the swift-frontend '-c' invocation
|
||||
let swiftDependencies: URL?
|
||||
}
|
||||
|
||||
struct SwiftFileCompilationInfo: Encodable, Equatable {
|
||||
public struct SwiftFileCompilationInfo: Encodable, Hashable {
|
||||
let file: URL
|
||||
// not present for WMO builds
|
||||
let dependencies: URL?
|
||||
@@ -60,11 +66,18 @@ struct SwiftFileCompilationInfo: Encodable, Equatable {
|
||||
|
||||
class SwiftcFilemapInputEditor: SwiftcInputReader, SwiftcInputWriter {
|
||||
|
||||
enum Format {
|
||||
case json
|
||||
case yaml
|
||||
}
|
||||
|
||||
private let file: URL
|
||||
private let fileFormat: Format
|
||||
private let fileManager: FileManager
|
||||
|
||||
init(_ file: URL, fileManager: FileManager) {
|
||||
init(_ file: URL, fileFormat: Format, fileManager: FileManager) {
|
||||
self.file = file
|
||||
self.fileFormat = fileFormat
|
||||
self.fileManager = fileManager
|
||||
}
|
||||
|
||||
@@ -72,7 +85,7 @@ class SwiftcFilemapInputEditor: SwiftcInputReader, SwiftcInputWriter {
|
||||
guard let content = fileManager.contents(atPath: file.path) else {
|
||||
throw SwiftcInputReaderError.readingFailed
|
||||
}
|
||||
guard let representation = try JSONSerialization.jsonObject(with: content, options: []) as? [String: Any] else {
|
||||
guard let representation = try decodeFile(content: content) else {
|
||||
throw SwiftcInputReaderError.invalidFormat
|
||||
}
|
||||
return try SwiftCompilationInfo(from: representation)
|
||||
@@ -82,11 +95,23 @@ class SwiftcFilemapInputEditor: SwiftcInputReader, SwiftcInputWriter {
|
||||
let data = try JSONSerialization.data(withJSONObject: info.dump(), options: [.prettyPrinted])
|
||||
fileManager.createFile(atPath: file.path, contents: data, attributes: nil)
|
||||
}
|
||||
|
||||
private func decodeFile(content: Data) throws -> [String: Any]? {
|
||||
switch fileFormat {
|
||||
case .json:
|
||||
return try JSONSerialization.jsonObject(with: content, options: []) as? [String: Any]
|
||||
case .yaml:
|
||||
guard let stringContent = String(data: content, encoding: .utf8) else {
|
||||
throw SwiftcInputReaderError.invalidYamlString
|
||||
}
|
||||
return try Yams.load(yaml: stringContent) as? [String: Any]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SwiftCompilationInfo {
|
||||
init(from object: [String: Any]) throws {
|
||||
info = try SwiftModuleCompilationInfo(from: object[""])
|
||||
info = try SwiftModuleCompilationInfo(from: object["", default: [:]])
|
||||
files = try object.reduce([]) { prev, new in
|
||||
let (key, value) = new
|
||||
if key.isEmpty {
|
||||
@@ -111,14 +136,14 @@ extension SwiftModuleCompilationInfo {
|
||||
guard let dict = object as? [String: String] else {
|
||||
throw SwiftcInputReaderError.invalidFormat
|
||||
}
|
||||
swiftDependencies = try dict.readURL(key: "swift-dependencies")
|
||||
swiftDependencies = dict.readURL(key: "swift-dependencies")
|
||||
dependencies = dict.readURL(key: "dependencies")
|
||||
}
|
||||
|
||||
func dump() -> [String: String] {
|
||||
return [
|
||||
"dependencies": dependencies?.path,
|
||||
"swift-dependencies": swiftDependencies.path,
|
||||
"swift-dependencies": swiftDependencies?.path,
|
||||
].compactMapValues { $0 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,10 @@ class SwiftcOrchestrator {
|
||||
private let mode: SwiftcContext.SwiftcMode
|
||||
// swiftc command that should be called to generate artifacts
|
||||
private let swiftcCommand: String
|
||||
private let objcHeaderOutput: URL
|
||||
private let moduleOutput: URL
|
||||
// Might be nil if invoking from frontend compilation: `swift-frontend -c`
|
||||
private let objcHeaderOutput: URL?
|
||||
// Might be nil if invoking from frontend compilation: `swift-frontend -c`
|
||||
private let moduleOutput: URL?
|
||||
private let arch: String
|
||||
private let artifactBuilder: ArtifactSwiftProductsBuilder
|
||||
private let shellOut: ShellOut
|
||||
@@ -39,8 +41,8 @@ class SwiftcOrchestrator {
|
||||
mode: SwiftcContext.SwiftcMode,
|
||||
swiftc: SwiftcProtocol,
|
||||
swiftcCommand: String,
|
||||
objcHeaderOutput: URL,
|
||||
moduleOutput: URL,
|
||||
objcHeaderOutput: URL?,
|
||||
moduleOutput: URL?,
|
||||
arch: String,
|
||||
artifactBuilder: ArtifactSwiftProductsBuilder,
|
||||
producerFallbackCommandProcessors: [ShellCommandsProcessor],
|
||||
@@ -128,10 +130,14 @@ class SwiftcOrchestrator {
|
||||
try processor.applyArgsRewrite(args)
|
||||
}
|
||||
try fallbackToDefaultAndWait(command: swiftcCommand, args: swiftcArgs)
|
||||
// move generated .h to the location where artifact creator expects it
|
||||
try artifactBuilder.includeObjCHeaderToTheArtifact(arch: arch, headerURL: objcHeaderOutput)
|
||||
// move generated .swiftmodule to the location where artifact creator expects it
|
||||
try artifactBuilder.includeModuleDefinitionsToTheArtifact(arch: arch, moduleURL: moduleOutput)
|
||||
if let objcHeaderOutput = objcHeaderOutput {
|
||||
// move generated .h to the location where artifact creator expects it
|
||||
try artifactBuilder.includeObjCHeaderToTheArtifact(arch: arch, headerURL: objcHeaderOutput)
|
||||
}
|
||||
if let moduleOutput = moduleOutput {
|
||||
// move generated .swiftmodule to the location where artifact creator expects it
|
||||
try artifactBuilder.includeModuleDefinitionsToTheArtifact(arch: arch, moduleURL: moduleOutput)
|
||||
}
|
||||
|
||||
try producerFallbackCommandProcessors.forEach {
|
||||
try $0.postCommandProcessing()
|
||||
|
||||
@@ -45,15 +45,15 @@ public struct SwiftcArgInput {
|
||||
}
|
||||
}
|
||||
|
||||
public class XCSwiftc {
|
||||
private let command: String
|
||||
private let inputArgs: SwiftcArgInput
|
||||
public class XCSwiftAbstract<InputArgs> {
|
||||
let command: String
|
||||
let inputArgs: InputArgs
|
||||
private let dependenciesWriterFactory: (URL, FileManager) -> DependenciesWriter
|
||||
private let touchFactory: (URL, FileManager) -> Touch
|
||||
|
||||
public init(
|
||||
command: String,
|
||||
inputArgs: SwiftcArgInput,
|
||||
inputArgs: InputArgs,
|
||||
dependenciesWriter: @escaping (URL, FileManager) -> DependenciesWriter,
|
||||
touchFactory: @escaping (URL, FileManager) -> Touch
|
||||
) {
|
||||
@@ -63,26 +63,52 @@ public class XCSwiftc {
|
||||
self.touchFactory = touchFactory
|
||||
}
|
||||
|
||||
func buildContext() throws -> (XCRemoteCacheConfig, SwiftcContext) {
|
||||
fatalError("Need to override in \(Self.self)")
|
||||
}
|
||||
|
||||
// swiftlint:disable:next function_body_length
|
||||
public func run() {
|
||||
public func run() throws {
|
||||
let fileManager = FileManager.default
|
||||
let config: XCRemoteCacheConfig
|
||||
let context: SwiftcContext
|
||||
do {
|
||||
let srcRoot: URL = URL(fileURLWithPath: fileManager.currentDirectoryPath)
|
||||
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileReader: fileManager)
|
||||
.readConfiguration()
|
||||
context = try SwiftcContext(config: config, input: inputArgs)
|
||||
} catch {
|
||||
exit(1, "FATAL: Swiftc initialization failed with error: \(error)")
|
||||
}
|
||||
let (config, context) = try buildContext()
|
||||
|
||||
let swiftcCommand = config.swiftcCommand
|
||||
let markerURL = context.tempDir.appendingPathComponent(config.modeMarkerPath)
|
||||
let markerReader = FileMarkerReader(markerURL, fileManager: fileManager)
|
||||
let markerWriter = FileMarkerWriter(markerURL, fileAccessor: fileManager)
|
||||
|
||||
let inputReader = SwiftcFilemapInputEditor(context.filemap, fileManager: fileManager)
|
||||
let fileListEditor = FileListEditor(context.fileList, fileManager: fileManager)
|
||||
let inputReader: SwiftcInputReader
|
||||
switch context.inputs {
|
||||
case .fileMap(let path):
|
||||
inputReader = SwiftcFilemapInputEditor(
|
||||
URL(fileURLWithPath: path),
|
||||
fileFormat: .json,
|
||||
fileManager: fileManager
|
||||
)
|
||||
case .supplementaryFileMap(let path):
|
||||
// Supplementary file map is endoded in the yaml file (contraty to
|
||||
// the standard filemap, which is in json)
|
||||
inputReader = SwiftcFilemapInputEditor(
|
||||
URL(fileURLWithPath: path),
|
||||
fileFormat: .yaml,
|
||||
fileManager: fileManager
|
||||
)
|
||||
case .map(let map):
|
||||
// static - passed via the arguments list
|
||||
inputReader = StaticSwiftcInputReader(
|
||||
moduleDependencies: context.steps.emitModule?.dependencies,
|
||||
// with Xcode 14, inputs via cmd are only used for compilations
|
||||
swiftDependencies: nil,
|
||||
compilationFiles: Array(map.values)
|
||||
)
|
||||
}
|
||||
let fileListReader: ListReader
|
||||
switch context.compilationFiles {
|
||||
case .fileList(let path):
|
||||
fileListReader = FileListEditor(URL(fileURLWithPath: path), fileManager: fileManager)
|
||||
case .list(let paths):
|
||||
fileListReader = StaticFileListReader(list: paths.map(URL.init(fileURLWithPath:)))
|
||||
}
|
||||
let artifactOrganizer = ZipArtifactOrganizer(
|
||||
targetTempDir: context.tempDir,
|
||||
// xcswiftc doesn't call artifact preprocessing
|
||||
@@ -101,11 +127,20 @@ public class XCSwiftc {
|
||||
moduleName: context.moduleName,
|
||||
fileManager: fileManager
|
||||
)
|
||||
let productsGenerator = DiskSwiftcProductsGenerator(
|
||||
modulePathOutput: context.modulePathOutput,
|
||||
objcHeaderOutput: context.objcHeaderOutput,
|
||||
diskCopier: HardLinkDiskCopier(fileManager: fileManager)
|
||||
)
|
||||
let productsGenerator: SwiftcProductsGenerator
|
||||
if let emitModule = context.steps.emitModule {
|
||||
productsGenerator = DiskSwiftcProductsGenerator(
|
||||
modulePathOutput: emitModule.modulePathOutput,
|
||||
objcHeaderOutput: emitModule.objcHeaderOutput,
|
||||
diskCopier: HardLinkDiskCopier(fileManager: fileManager)
|
||||
)
|
||||
} else {
|
||||
// If the module was not requested for this proces (compiling files only)
|
||||
// do nothing, when someone (e.g. a plugin) asks for the products generation
|
||||
// This generation will happend in a separate process, where the module
|
||||
// generation is requested
|
||||
productsGenerator = NoopSwiftcProductsGenerator()
|
||||
}
|
||||
let allInvocationsStorage = ExistingFileStorage(
|
||||
storageFile: context.invocationHistoryFile,
|
||||
command: swiftcCommand
|
||||
@@ -117,9 +152,12 @@ public class XCSwiftc {
|
||||
retrieveIgnoredCommands: [swiftcCommand]
|
||||
)
|
||||
let shellOut = ProcessShellOut()
|
||||
// Always allow an input file from the actool generation step
|
||||
// As of Xcode15, the filename is confirmed to be static
|
||||
let allowedInputDeterminer = FilenameBasedAllowedInputDeterminer(["GeneratedAssetSymbols.swift"])
|
||||
|
||||
let swiftc = Swiftc(
|
||||
inputFileListReader: fileListEditor,
|
||||
inputFileListReader: fileListReader,
|
||||
markerReader: markerReader,
|
||||
allowedFilesListScanner: allowedFilesListScanner,
|
||||
artifactOrganizer: artifactOrganizer,
|
||||
@@ -130,24 +168,35 @@ public class XCSwiftc {
|
||||
fileManager: fileManager,
|
||||
dependenciesWriterFactory: dependenciesWriterFactory,
|
||||
touchFactory: touchFactory,
|
||||
plugins: []
|
||||
plugins: [],
|
||||
allowedInputDeterminer: allowedInputDeterminer
|
||||
)
|
||||
let orchestrator = SwiftcOrchestrator(
|
||||
mode: context.mode,
|
||||
swiftc: swiftc,
|
||||
swiftcCommand: swiftcCommand,
|
||||
objcHeaderOutput: context.objcHeaderOutput,
|
||||
moduleOutput: context.modulePathOutput,
|
||||
objcHeaderOutput: context.steps.emitModule?.objcHeaderOutput,
|
||||
moduleOutput: context.steps.emitModule?.modulePathOutput,
|
||||
arch: context.arch,
|
||||
artifactBuilder: artifactBuilder,
|
||||
producerFallbackCommandProcessors: [],
|
||||
invocationStorage: invocationStorage,
|
||||
shellOut: shellOut
|
||||
)
|
||||
do {
|
||||
try orchestrator.run()
|
||||
} catch {
|
||||
exit(1, "Swiftc failed with error: \(error)")
|
||||
}
|
||||
try orchestrator.run()
|
||||
}
|
||||
}
|
||||
|
||||
public class XCSwiftc: XCSwiftAbstract<SwiftcArgInput> {
|
||||
override func buildContext() throws -> (XCRemoteCacheConfig, SwiftcContext) {
|
||||
let fileReader = FileManager.default
|
||||
let config: XCRemoteCacheConfig
|
||||
let context: SwiftcContext
|
||||
let srcRoot: URL = URL(fileURLWithPath: fileReader.currentDirectoryPath)
|
||||
config = try XCRemoteCacheConfigReader(srcRootPath: srcRoot.path, fileReader: fileReader)
|
||||
.readConfiguration()
|
||||
context = try SwiftcContext(config: config, input: inputArgs)
|
||||
|
||||
return (config, context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
// swiftlint:disable file_length
|
||||
|
||||
import Foundation
|
||||
import Yams
|
||||
|
||||
@@ -57,6 +59,8 @@ public struct XCRemoteCacheConfig: Encodable {
|
||||
var clangCommand: String = "clang"
|
||||
/// Command for a standard Swift compilation (swiftc)
|
||||
var swiftcCommand: String = "swiftc"
|
||||
/// Command for a standard Swift frontend compilation (swift-frontend)
|
||||
var swiftFrontendCommand: String = "swift-frontend"
|
||||
/// Path of the primary repository that produces cache artifacts
|
||||
var primaryRepo: String = ""
|
||||
/// Main (primary) branch that produces cache artifacts (default to 'master')
|
||||
@@ -148,6 +152,11 @@ public struct XCRemoteCacheConfig: Encodable {
|
||||
/// Note: The regex can match either partially or fully the filepath, e.g. `\\.modulemap$` will exclude
|
||||
/// all `.modulemap` files
|
||||
var irrelevantDependenciesPaths: [String] = []
|
||||
/// If true, do not fail `prepare` if cannot find the most recent common commits with the primary branch
|
||||
/// That might useful on CI, where a shallow clone is used
|
||||
var gracefullyHandleMissingCommonSha: Bool = false
|
||||
/// Enable experimental integration with swift driver, added in Xcode 14
|
||||
var enableSwiftDriverIntegration: Bool = false
|
||||
}
|
||||
|
||||
extension XCRemoteCacheConfig {
|
||||
@@ -206,6 +215,9 @@ extension XCRemoteCacheConfig {
|
||||
merge.disableVFSOverlay = scheme.disableVFSOverlay ?? disableVFSOverlay
|
||||
merge.customRewriteEnvs = scheme.customRewriteEnvs ?? customRewriteEnvs
|
||||
merge.irrelevantDependenciesPaths = scheme.irrelevantDependenciesPaths ?? irrelevantDependenciesPaths
|
||||
merge.gracefullyHandleMissingCommonSha =
|
||||
scheme.gracefullyHandleMissingCommonSha ?? gracefullyHandleMissingCommonSha
|
||||
merge.enableSwiftDriverIntegration = scheme.enableSwiftDriverIntegration ?? enableSwiftDriverIntegration
|
||||
return merge
|
||||
}
|
||||
|
||||
@@ -273,6 +285,8 @@ struct ConfigFileScheme: Decodable {
|
||||
let disableVFSOverlay: Bool?
|
||||
let customRewriteEnvs: [String]?
|
||||
let irrelevantDependenciesPaths: [String]?
|
||||
let gracefullyHandleMissingCommonSha: Bool?
|
||||
let enableSwiftDriverIntegration: Bool?
|
||||
|
||||
// Yams library doesn't support encoding strategy, see https://github.com/jpsim/Yams/issues/84
|
||||
enum CodingKeys: String, CodingKey {
|
||||
@@ -323,6 +337,8 @@ struct ConfigFileScheme: Decodable {
|
||||
case disableVFSOverlay = "disable_vfs_overlay"
|
||||
case customRewriteEnvs = "custom_rewrite_envs"
|
||||
case irrelevantDependenciesPaths = "irrelevant_dependencies_paths"
|
||||
case gracefullyHandleMissingCommonSha = "gracefully_handle_missing_common_sha"
|
||||
case enableSwiftDriverIntegration = "enable_swift_driver_integration"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
// 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
|
||||
|
||||
/// Parser for `assetcatalog_dependencies` file: an output of the `actool`
|
||||
/// that lists all dependencies of this command
|
||||
class AssetsFileDependenciesReader: DependenciesReader {
|
||||
private let file: URL
|
||||
private let dirAccessor: DirAccessor
|
||||
|
||||
public init(_ file: URL, dirAccessor: DirAccessor) {
|
||||
self.file = file
|
||||
self.dirAccessor = dirAccessor
|
||||
}
|
||||
|
||||
public func findDependencies() throws -> [String] {
|
||||
return try Array(findAllDependencies())
|
||||
}
|
||||
|
||||
public func findInputs() throws -> [String] {
|
||||
// XCRemoteCache doesn't use it yet
|
||||
exit(1, "TODO: implement")
|
||||
}
|
||||
|
||||
public func readFilesAndDependencies() throws -> [String: [String]] {
|
||||
return try ["": findAllDependencies()]
|
||||
}
|
||||
|
||||
private func findAllDependencies() throws -> [String] {
|
||||
let fileData = try getFileData()
|
||||
// all dependency files are separated by the \0 byte
|
||||
// each path has a file type prefix:
|
||||
// 0x10 - directory
|
||||
// 0x40 - file
|
||||
// We only care about dirs, as *.xcassets is a folder
|
||||
let pathDatas = fileData.split(separator: 0x0)
|
||||
let paths = pathDatas
|
||||
.filter { !$0.isEmpty && $0.first == 0x10 }
|
||||
.map { String(data: $0.dropFirst(), encoding: .utf8)! }
|
||||
.map(URL.init(fileURLWithPath:))
|
||||
let xcassetsPaths = paths.filter { path in
|
||||
path.pathExtension == "xcassets"
|
||||
}
|
||||
return try xcassetsPaths.flatMap { try findAssetsContentJsons(xcasset: $0) }
|
||||
}
|
||||
|
||||
private func findAssetsContentJsons(xcasset: URL) throws -> [String] {
|
||||
return try dirAccessor.recursiveItems(at: xcasset).filter { url in
|
||||
url.lastPathComponent == "Contents.json"
|
||||
}.map(\.path)
|
||||
}
|
||||
|
||||
private func getFileData() throws -> Data {
|
||||
guard let fileData = try dirAccessor.contents(atPath: file.path) else {
|
||||
throw DependenciesReaderError.readingError
|
||||
}
|
||||
return fileData
|
||||
}
|
||||
|
||||
}
|
||||
@@ -48,6 +48,7 @@ class PhaseCacheModeController: CacheModeController {
|
||||
private let dependenciesWriter: DependenciesWriter
|
||||
private let dependenciesReader: DependenciesReader
|
||||
private let markerWriter: MarkerWriter
|
||||
private let llbuildLockFile: URL
|
||||
private let fileManager: FileManager
|
||||
|
||||
init(
|
||||
@@ -59,6 +60,7 @@ class PhaseCacheModeController: CacheModeController {
|
||||
dependenciesWriter: (URL, FileManager) -> DependenciesWriter,
|
||||
dependenciesReader: (URL, FileManager) -> DependenciesReader,
|
||||
markerWriter: (URL, FileManager) -> MarkerWriter,
|
||||
llbuildLockFile: URL,
|
||||
fileManager: FileManager
|
||||
) {
|
||||
|
||||
@@ -69,10 +71,12 @@ class PhaseCacheModeController: CacheModeController {
|
||||
let discoveryURL = tempDir.appendingPathComponent(phaseDependencyPath)
|
||||
self.dependenciesWriter = dependenciesWriter(discoveryURL, fileManager)
|
||||
self.dependenciesReader = dependenciesReader(discoveryURL, fileManager)
|
||||
self.llbuildLockFile = llbuildLockFile
|
||||
self.markerWriter = markerWriter(modeMarker, fileManager)
|
||||
}
|
||||
|
||||
func enable(allowedInputFiles: [URL], dependencies: [URL]) throws {
|
||||
try cleanupLlBuildLock()
|
||||
// marker file contains filepaths that contribute to the build products
|
||||
// and should invalidate all other target steps (swiftc,libtool etc.)
|
||||
let targetSensitiveFiles = dependencies + [modeMarker, Self.xcodeSelectLink]
|
||||
@@ -84,6 +88,7 @@ class PhaseCacheModeController: CacheModeController {
|
||||
}
|
||||
|
||||
func disable() throws {
|
||||
try cleanupLlBuildLock()
|
||||
guard !forceCached else {
|
||||
throw PhaseCacheModeControllerError.cannotUseRemoteCacheForForcedCacheMode
|
||||
}
|
||||
@@ -110,8 +115,24 @@ class PhaseCacheModeController: CacheModeController {
|
||||
} catch {
|
||||
// Gracefully don't disable a cache
|
||||
// That may happen if building a target for the first time
|
||||
errorLog("Couldn't verify if should disable RC for \(commitValue).")
|
||||
debugLog("Couldn't verify if should disable RC for \(commitValue).")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// cleanup the build lock file (if exists) as the very last step of this controller
|
||||
// this is just a non-critical cleanup step to not leave {{LLBUILD_BUILD_ID}}.lock
|
||||
// files in $TARGET_TEMP_DIR. It is expected that both prebuild and postbuild will
|
||||
// invoke it, to ensure:
|
||||
// - swift-frontend synchronization is done per-target build
|
||||
// - no .lock leftover files
|
||||
private func cleanupLlBuildLock() throws {
|
||||
if fileManager.fileExists(atPath: llbuildLockFile.path) {
|
||||
do {
|
||||
try fileManager.removeItem(at: llbuildLockFile)
|
||||
} catch {
|
||||
printWarning("Removing llbuild lock at \(llbuildLockFile.path) failed. Error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,12 +22,12 @@ import Foundation
|
||||
/// Reads a list of files from a marker file
|
||||
class FileMarkerReader: ListReader {
|
||||
private let file: URL
|
||||
private let fileManager: FileManager
|
||||
private let fileReader: FileReader
|
||||
private var cachedFiles: [URL]?
|
||||
|
||||
init(_ file: URL, fileManager: FileManager) {
|
||||
init(_ file: URL, fileManager: FileReader) {
|
||||
self.file = file
|
||||
self.fileManager = fileManager
|
||||
self.fileReader = fileManager
|
||||
}
|
||||
|
||||
func listFilesURLs() throws -> [URL] {
|
||||
@@ -45,6 +45,6 @@ class FileMarkerReader: ListReader {
|
||||
}
|
||||
|
||||
func canRead() -> Bool {
|
||||
return fileManager.fileExists(atPath: file.path)
|
||||
return fileReader.fileExists(atPath: file.path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// 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
|
||||
|
||||
class StaticFileListReader: ListReader {
|
||||
private let list: [URL]
|
||||
|
||||
init(list: [URL]) {
|
||||
self.list = list
|
||||
}
|
||||
|
||||
func listFilesURLs() throws -> [URL] {
|
||||
list
|
||||
}
|
||||
|
||||
func canRead() -> Bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -21,26 +21,35 @@ import Foundation
|
||||
|
||||
/// Reads and aggregates all compilation dependencies from a single directory
|
||||
class TargetDependenciesReader: DependenciesReader {
|
||||
private let directory: URL
|
||||
// As of Xcode15, the filename is static
|
||||
private static let assetsDependenciesFilename = "assetcatalog_dependencies"
|
||||
private let compilationDirectory: URL
|
||||
private let assetsCatalogOutputDir: URL
|
||||
private let dirScanner: DirScanner
|
||||
private let fileDependeciesReaderFactory: (URL) -> DependenciesReader
|
||||
private let fileDependenciesReaderFactory: (URL) -> DependenciesReader
|
||||
private let assetsDependenciesReaderFactory: (URL) -> DependenciesReader
|
||||
|
||||
public init(
|
||||
_ directory: URL,
|
||||
fileDependeciesReaderFactory: @escaping (URL) -> DependenciesReader,
|
||||
compilationOutputDir: URL,
|
||||
assetsCatalogOutputDir: URL,
|
||||
fileDependenciesReaderFactory: @escaping (URL) -> DependenciesReader,
|
||||
assetsDependenciesReaderFactory: @escaping (URL) -> DependenciesReader,
|
||||
dirScanner: DirScanner
|
||||
) {
|
||||
self.directory = directory
|
||||
self.compilationDirectory = compilationOutputDir
|
||||
self.assetsCatalogOutputDir = assetsCatalogOutputDir
|
||||
self.dirScanner = dirScanner
|
||||
self.fileDependeciesReaderFactory = fileDependeciesReaderFactory
|
||||
self.fileDependenciesReaderFactory = fileDependenciesReaderFactory
|
||||
self.assetsDependenciesReaderFactory = assetsDependenciesReaderFactory
|
||||
}
|
||||
|
||||
// Optimized way of finding dependencies only for files that have corresponding .o file on a disk
|
||||
// includes also inputs to the `actool` assets generator
|
||||
public func findDependencies() throws -> [String] {
|
||||
// Not calling `readFilesAndDependencies` as it may unnecessary call expensive `findDependencies()` for
|
||||
// files that eventually will not be considered
|
||||
let allURLs = try dirScanner.items(at: directory)
|
||||
let mergedDependencies = try allURLs.reduce(Set<String>()) { (prev: Set<String>, file) in
|
||||
let allCompilationOutputURLs = try dirScanner.items(at: compilationDirectory)
|
||||
var mergedDependencies = try allCompilationOutputURLs.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)
|
||||
// Otherwise .d is probably just a leftover from previous builds
|
||||
@@ -53,20 +62,33 @@ class TargetDependenciesReader: DependenciesReader {
|
||||
return prev
|
||||
}
|
||||
|
||||
return try prev.union(fileDependeciesReaderFactory(file).findDependencies())
|
||||
return try prev.union(fileDependenciesReaderFactory(file).findDependencies())
|
||||
}
|
||||
// include also dependencies from optional assets compilation (`actool`)
|
||||
try mergedDependencies.formUnion(findAssetsCatalogDependencies())
|
||||
return Array(mergedDependencies).sorted()
|
||||
}
|
||||
|
||||
// finds all assets compilation's dependencies, which are always appended to the list of
|
||||
// files to compare on the consumer side (in the fingerprint comparison)
|
||||
private func findAssetsCatalogDependencies() throws -> Set<String> {
|
||||
let expectedAssetsDepsFile = assetsCatalogOutputDir
|
||||
.appendingPathComponent(Self.assetsDependenciesFilename)
|
||||
guard try dirScanner.itemType(atPath: expectedAssetsDepsFile.path) == .file else {
|
||||
return []
|
||||
}
|
||||
return try Set(assetsDependenciesReaderFactory(expectedAssetsDepsFile).findDependencies())
|
||||
}
|
||||
|
||||
public func findInputs() throws -> [String] {
|
||||
fatalError("TODO: implement")
|
||||
}
|
||||
|
||||
public func readFilesAndDependencies() throws -> [String: [String]] {
|
||||
let allURLs = try dirScanner.items(at: directory)
|
||||
let allURLs = try dirScanner.items(at: compilationDirectory)
|
||||
return try allURLs.reduce([String: [String]]()) { prev, file in
|
||||
var new = prev
|
||||
new[file.path] = try fileDependeciesReaderFactory(file).findDependencies()
|
||||
new[file.path] = try fileDependenciesReaderFactory(file).findDependencies()
|
||||
return new
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ enum RemoteCommitInfo: Equatable {
|
||||
extension RemoteCommitInfo {
|
||||
init(_ commit: String?) {
|
||||
switch commit {
|
||||
case .some(let value) where !value.isEmpty :
|
||||
case .some(let value) where !value.isEmpty:
|
||||
self = .available(commit: value)
|
||||
default:
|
||||
self = .unavailable
|
||||
|
||||
@@ -34,7 +34,15 @@ struct CanonicalRequest {
|
||||
if url.path.isEmpty {
|
||||
path = "/"
|
||||
} else {
|
||||
path = url.path
|
||||
if let escapedPath = url.path.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) {
|
||||
path = escapedPath
|
||||
} else {
|
||||
path = "/"
|
||||
printWarning("""
|
||||
Escaping the path \(url.path) failed and a placeholder is used instead. \
|
||||
Make sure the path doesn't contain invalid characters.
|
||||
""")
|
||||
}
|
||||
}
|
||||
return
|
||||
"\(httpMethod)\n" +
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// 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
|
||||
|
||||
public extension Array {
|
||||
func get(_ i: Index) -> Element? {
|
||||
guard count > i else {
|
||||
return nil
|
||||
}
|
||||
return self[i]
|
||||
}
|
||||
}
|
||||
@@ -55,4 +55,11 @@ extension Dictionary where Key == String, Value == String {
|
||||
}
|
||||
return value == "YES"
|
||||
}
|
||||
|
||||
func readEnv(key: String) throws -> Bool? {
|
||||
guard let value = self[key] else {
|
||||
return nil
|
||||
}
|
||||
return value == "YES"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: Array(args.dropFirst()))
|
||||
try XCLibtool(mode).run()
|
||||
} catch {
|
||||
exit(1, "Failed with: \(error). Args: \(args)")
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
// 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 asksForVersion = false
|
||||
var i = 0
|
||||
while i < args.count {
|
||||
switch args[i] {
|
||||
case "-V":
|
||||
asksForVersion = true
|
||||
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 args[i].starts(with: "/") && ["", "a"].contains(URL(string: args[i])?.pathExtension):
|
||||
// Assume always absolute paths to the library
|
||||
// Support for static frameworks (no extension) and static libraries (.a)
|
||||
inputLibraries.append(input)
|
||||
default:
|
||||
break
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
if asksForVersion {
|
||||
return .version
|
||||
}
|
||||
guard let outputInput = output else {
|
||||
throw XCLibtoolHelperError.missingOutput
|
||||
}
|
||||
|
||||
let mode: XCLibtoolMode
|
||||
if let filelistInput = filelist, let dependencyInfoInput = dependencyInfo {
|
||||
// libtool is creating a library
|
||||
mode = .createLibrary(output: outputInput, filelist: filelistInput, dependencyInfo: dependencyInfoInput)
|
||||
} else if !inputLibraries.isEmpty {
|
||||
// multiple input libraries suggest creating an universal binary
|
||||
mode = .createUniversalBinary(output: outputInput, inputs: inputLibraries)
|
||||
} else {
|
||||
// unknown mode
|
||||
throw XCLibtoolHelperError.unsupportedMode
|
||||
}
|
||||
return mode
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) 2023 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import Foundation
|
||||
import XCRemoteCache
|
||||
|
||||
/// Wrapper for a `lipo` program that links any of input binaries to the destination paths
|
||||
/// Fallbacks to a standard `lipo` when the Ramote cache is not applicable (e.g. modified sources)
|
||||
public class XCLipoMain {
|
||||
public init() { }
|
||||
|
||||
public func main() {
|
||||
let args = ProcessInfo().arguments
|
||||
var output: String?
|
||||
var create = false
|
||||
var inputs: [String] = []
|
||||
|
||||
var i = 1
|
||||
while i < args.count {
|
||||
switch args[i] {
|
||||
case "-output":
|
||||
output = args[i + 1]
|
||||
i += 1
|
||||
case "-create":
|
||||
create = true
|
||||
default:
|
||||
inputs.append(args[i])
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
let lipoCommand = "lipo"
|
||||
guard let output = output, !inputs.isEmpty, create else {
|
||||
print("Fallbacking to compilation using \(lipoCommand).")
|
||||
|
||||
let args = ProcessInfo().arguments
|
||||
let paramList = [lipoCommand] + args.dropFirst()
|
||||
let cargs = paramList.map { strdup($0) } + [nil]
|
||||
execvp(lipoCommand, cargs)
|
||||
|
||||
/// C-function `execv` returns only when the command fails
|
||||
exit(1)
|
||||
}
|
||||
|
||||
do {
|
||||
try XCLipo(
|
||||
output: output,
|
||||
inputs: inputs,
|
||||
fallbackCommand: lipoCommand,
|
||||
stepDescription: "xclipo"
|
||||
).run()
|
||||
} catch {
|
||||
exit(1, "Failed with: \(error). Args: \(args)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) 2023 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import XCRemoteCache
|
||||
|
||||
XCLipoMain().main()
|
||||
@@ -229,6 +229,11 @@ struct XCPrepareMain: ParsableCommand {
|
||||
)
|
||||
var fakeSrcRoot: String
|
||||
|
||||
@Option(name: .customLong("sdks-exclude"), default: "", help: """
|
||||
comma separated list of sdks to not integrate XCRemoteCache (e.g. "watchos*, watchsimulator*")
|
||||
""", transform: nonEmptyString)
|
||||
var sdksExclude: String
|
||||
|
||||
|
||||
func run() throws {
|
||||
XCIntegrate(
|
||||
@@ -243,6 +248,7 @@ struct XCPrepareMain: ParsableCommand {
|
||||
consumerEligiblePlatforms: consumerEligiblePlatforms,
|
||||
lldbMode: lldbInit,
|
||||
fakeSrcRoot: fakeSrcRoot,
|
||||
sdksExclude: sdksExclude,
|
||||
output: output
|
||||
).main()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
// Copyright (c) 2023 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import Foundation
|
||||
import XCRemoteCache
|
||||
|
||||
/// Wrapper for a `swift-frontend` that skips compilation and
|
||||
/// produces empty output files (.o). Just like in xcswiftc, compilation dependencies
|
||||
/// (.d) files are copied from the prebuild marker file which includes all relevant files
|
||||
/// Fallbacks to a standard `swift-frontend` when the
|
||||
/// ramote cache is not applicable (e.g. modified sources)
|
||||
public class XCSwiftcFrontendMain {
|
||||
// swiftlint:disable:next function_body_length cyclomatic_complexity
|
||||
public func main() {
|
||||
let env = ProcessInfo.processInfo.environment
|
||||
// Do not invoke raw swift-frontend because that would lead to the infinite loop
|
||||
// swift-frontent -> xcswift-frontent -> swift-frontent
|
||||
//
|
||||
// Note: Returning the `swiftc` executaion here because it is possible to pass all arguments
|
||||
// from swift-frontend to `swiftc` and swiftc will be able to redirect to swift-frontend
|
||||
// (because the first argument is `-frontend`). If that is not a case (might change in
|
||||
// future swift compiler versions), invoke swift-frontend from the Xcode, but that introduces
|
||||
// a limitation that disallows custom toolchains in Xcode:
|
||||
// $DEVELOPER_DIR/Toolchains/XcodeDefault.xctoolchain/usr/bin/{ ProcessInfo().processName}
|
||||
let command = "swiftc"
|
||||
let args = ProcessInfo().arguments
|
||||
var compile = false
|
||||
var emitModule = false
|
||||
var objcHeaderOutput: String?
|
||||
var moduleName: String?
|
||||
var target: String?
|
||||
var inputPaths: [String] = []
|
||||
var primaryInputPaths: [String] = []
|
||||
var outputPaths: [String] = []
|
||||
var dependenciesPaths: [String] = []
|
||||
var diagnosticsPaths: [String] = []
|
||||
var sourceInfoPath: String?
|
||||
var docPath: String?
|
||||
var supplementaryOutputFileMap: String?
|
||||
|
||||
for i in 0..<args.count {
|
||||
let arg = args[i]
|
||||
switch arg {
|
||||
case "-c":
|
||||
compile = true
|
||||
case "-emit-module":
|
||||
emitModule = true
|
||||
case "-o":
|
||||
outputPaths.append(args[i + 1])
|
||||
case "-emit-objc-header-path":
|
||||
objcHeaderOutput = args[i + 1]
|
||||
case "-module-name":
|
||||
moduleName = args[i + 1]
|
||||
case "-target":
|
||||
target = args[i + 1]
|
||||
case "-serialize-diagnostics-path":
|
||||
// .dia
|
||||
diagnosticsPaths.append(args[i + 1])
|
||||
case "-emit-dependencies-path":
|
||||
// .d
|
||||
dependenciesPaths.append(args[i + 1])
|
||||
case "-emit-module-source-info-path":
|
||||
// .swiftsourceinfo
|
||||
sourceInfoPath = args[i + 1]
|
||||
case "-emit-module-doc-path":
|
||||
// .swiftdoc
|
||||
docPath = args[i + 1]
|
||||
case "-primary-file":
|
||||
// .swift
|
||||
primaryInputPaths.append(args[i + 1])
|
||||
case "-supplementary-output-file-map":
|
||||
supplementaryOutputFileMap = args[i + 1]
|
||||
default:
|
||||
if arg.hasSuffix(".swift") {
|
||||
inputPaths.append(arg)
|
||||
}
|
||||
}
|
||||
}
|
||||
// support either emitModule (the preflight step) or compilation
|
||||
// all other types of invocations (like -print-target-info) should be
|
||||
// automatically redirected to the original swift-frontend
|
||||
let argInput = SwiftFrontendArgInput(
|
||||
compile: compile,
|
||||
emitModule: emitModule,
|
||||
objcHeaderOutput: objcHeaderOutput,
|
||||
moduleName: moduleName,
|
||||
target: target,
|
||||
primaryInputPaths: primaryInputPaths,
|
||||
inputPaths: inputPaths,
|
||||
outputPaths: outputPaths,
|
||||
dependenciesPaths: dependenciesPaths,
|
||||
diagnosticsPaths: diagnosticsPaths,
|
||||
sourceInfoPath: sourceInfoPath,
|
||||
docPath: docPath,
|
||||
supplementaryOutputFileMap: supplementaryOutputFileMap
|
||||
)
|
||||
// swift-frontend is first invoked with some "probing" args like
|
||||
// -print-target-info
|
||||
guard emitModule != compile else {
|
||||
runFallback(envs: env)
|
||||
}
|
||||
|
||||
do {
|
||||
let frontend = try XCSwiftFrontend(
|
||||
command: command,
|
||||
inputArgs: argInput,
|
||||
env: env,
|
||||
dependenciesWriter: FileDependenciesWriter.init,
|
||||
touchFactory: FileTouch.init)
|
||||
try frontend.run()
|
||||
} catch {
|
||||
runFallback(envs: env)
|
||||
}
|
||||
}
|
||||
|
||||
private func runFallback(envs env: [String: String]) -> Never {
|
||||
// DEVELOPER_DIR is always set by Xcode
|
||||
let developerDir = env["DEVELOPER_DIR"]!
|
||||
// limitation: always using the Xcode's toolchain, otherwise
|
||||
// there will be a loop for invoking swift-frontend wrapper from XCRemoteCache
|
||||
// Cause: for injecting into the swift driver pipeline, Xcode looks for
|
||||
// an executable with the name `swift-frontend` that is placed in the same
|
||||
// dir as `SWIFT_EXEC`'s `swiftc` wrapper
|
||||
let swiftFrontendCommand = "\(developerDir)/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-frontend"
|
||||
|
||||
let args = ProcessInfo().arguments
|
||||
let paramList = [swiftFrontendCommand] + args.dropFirst()
|
||||
let cargs = paramList.map { strdup($0) } + [nil]
|
||||
execvp(swiftFrontendCommand, cargs)
|
||||
|
||||
/// C-function `execv` returns only when the command fails
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) 2023 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import XCRemoteCache
|
||||
|
||||
XCSwiftcFrontendMain().main()
|
||||
@@ -60,30 +60,37 @@ public class XCSwiftcMain {
|
||||
let targetInputInput = target,
|
||||
let swiftFileListInput = swiftFileList
|
||||
else {
|
||||
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)
|
||||
executeFallback()
|
||||
}
|
||||
let swiftcArgsInput = SwiftcArgInput(
|
||||
objcHeaderOutput: objcHeaderOutputInput,
|
||||
moduleName: moduleNameInput,
|
||||
modulePathOutput: modulePathOutputInput,
|
||||
filemap: filemapInput,
|
||||
target: targetInputInput,
|
||||
fileList: swiftFileListInput
|
||||
)
|
||||
XCSwiftc(
|
||||
command: command,
|
||||
inputArgs: swiftcArgsInput,
|
||||
dependenciesWriter: FileDependenciesWriter.init,
|
||||
touchFactory: FileTouch.init
|
||||
).run()
|
||||
do {
|
||||
let swiftcArgsInput = SwiftcArgInput(
|
||||
objcHeaderOutput: objcHeaderOutputInput,
|
||||
moduleName: moduleNameInput,
|
||||
modulePathOutput: modulePathOutputInput,
|
||||
filemap: filemapInput,
|
||||
target: targetInputInput,
|
||||
fileList: swiftFileListInput
|
||||
)
|
||||
try XCSwiftc(
|
||||
command: command,
|
||||
inputArgs: swiftcArgsInput,
|
||||
dependenciesWriter: FileDependenciesWriter.init,
|
||||
touchFactory: FileTouch.init
|
||||
).run()
|
||||
} catch {
|
||||
executeFallback()
|
||||
}
|
||||
}
|
||||
private func executeFallback() -> Never {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
// 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.
|
||||
|
||||
@testable import XCRemoteCache
|
||||
import XCTest
|
||||
|
||||
class ArtifactMetaUpdaterTests: XCTestCase {
|
||||
private let accessorFake = FileAccessorFake(mode: .normal)
|
||||
private var metaWriter: MetaWriter!
|
||||
private var fileRemapper: FileDependenciesRemapper!
|
||||
private var updater: ArtifactMetaUpdater!
|
||||
private let sampleMeta = MainArtifactMeta(
|
||||
dependencies: [],
|
||||
fileKey: "abc",
|
||||
rawFingerprint: "",
|
||||
generationCommit: "",
|
||||
targetName: "",
|
||||
configuration: "",
|
||||
platform: "",
|
||||
xcode: "",
|
||||
inputs: ["$(BASE)/myFile.swift"],
|
||||
pluginsKeys: [:]
|
||||
)
|
||||
|
||||
override func setUp() async throws {
|
||||
metaWriter = JsonMetaWriter(
|
||||
fileWriter: accessorFake,
|
||||
pretty: true
|
||||
)
|
||||
fileRemapper = TextFileDependenciesRemapper(
|
||||
remapper: StringDependenciesRemapper(
|
||||
mappings: [
|
||||
.init(generic: "$(BASE)", local: "/base")
|
||||
]
|
||||
),
|
||||
fileAccessor: accessorFake
|
||||
)
|
||||
updater = ArtifactMetaUpdater(
|
||||
fileRemapper: fileRemapper,
|
||||
metaWriter: metaWriter
|
||||
)
|
||||
}
|
||||
|
||||
func testStoresInTheRawArtifact() throws {
|
||||
try updater.process(rawArtifact: "/artifact")
|
||||
try updater.run(meta: sampleMeta)
|
||||
|
||||
XCTAssertTrue(accessorFake.fileExists(atPath: "/artifact/abc.json"))
|
||||
}
|
||||
|
||||
func testRewirtesMetaPaths() throws {
|
||||
try updater.process(rawArtifact: "/artifact")
|
||||
try updater.run(meta: sampleMeta)
|
||||
|
||||
let diskMetaData = try XCTUnwrap(accessorFake.contents(atPath: "/artifact/abc.json"))
|
||||
let diskMeta = try JSONDecoder().decode(MainArtifactMeta.self, from: diskMetaData)
|
||||
XCTAssertEqual(diskMeta.inputs, ["/base/myFile.swift"])
|
||||
}
|
||||
|
||||
func testFailsIfProcessorTriggerIsNotCalledBeforeRunningAPlugin() throws {
|
||||
XCTAssertThrowsError(try updater.run(meta: sampleMeta)) { error in
|
||||
switch error {
|
||||
case ArtifactMetaUpdaterError.artifactLocationIsUnknown: break
|
||||
default:
|
||||
XCTFail("Not expected error")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,4 +156,39 @@ class ZipArtifactOrganizerTests: XCTestCase {
|
||||
|
||||
XCTAssertEqual(fileKey, expectedFileKey)
|
||||
}
|
||||
|
||||
func testPrepareRunsProcessorsForAlreadyExistingArtifacts() throws {
|
||||
let zipURL = try prepareZipFile(content: "Magic", fileName: "content.txt")
|
||||
let artifactURL = zipURL.deletingPathExtension()
|
||||
let processor = DestroyerArtifactProcessor(fileManager)
|
||||
let organizer: ZipArtifactOrganizer = ZipArtifactOrganizer(
|
||||
targetTempDir: workingDirectory,
|
||||
artifactProcessors: [processor],
|
||||
fileManager: fileManager
|
||||
)
|
||||
try fileManager.createDirectory(
|
||||
at: artifactURL,
|
||||
withIntermediateDirectories: true
|
||||
)
|
||||
|
||||
let preparedArtifact = try organizer.prepare(artifact: zipURL)
|
||||
|
||||
XCTAssertFalse(fileManager.fileExists(atPath: preparedArtifact.path))
|
||||
|
||||
}
|
||||
|
||||
func testPrepareRunsProcessorsForNewlyUnzippedArtifacts() throws {
|
||||
let zipURL = try prepareZipFile(content: "Magic", fileName: "content.txt")
|
||||
let processor = DestroyerArtifactProcessor(fileManager)
|
||||
let organizer: ZipArtifactOrganizer = ZipArtifactOrganizer(
|
||||
targetTempDir: workingDirectory,
|
||||
artifactProcessors: [processor],
|
||||
fileManager: fileManager
|
||||
)
|
||||
|
||||
let preparedArtifact = try organizer.prepare(artifact: zipURL)
|
||||
|
||||
XCTAssertFalse(fileManager.fileExists(atPath: preparedArtifact.path))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ class PostbuildContextTests: FileXCTestCase {
|
||||
"TARGET_TEMP_DIR": "TARGET_TEMP_DIR",
|
||||
"DERIVED_FILE_DIR": "DERIVED_FILE_DIR",
|
||||
"ARCHS": "x86_64",
|
||||
"OBJECT_FILE_DIR_normal": "/OBJECT_FILE_DIR_normal" ,
|
||||
"OBJECT_FILE_DIR_normal": "/OBJECT_FILE_DIR_normal",
|
||||
"CONFIGURATION": "CONFIGURATION",
|
||||
"PLATFORM_NAME": "PLATFORM_NAME",
|
||||
"XCODE_PRODUCT_BUILD_VERSION": "XCODE_PRODUCT_BUILD_VERSION",
|
||||
@@ -45,6 +45,7 @@ class PostbuildContextTests: FileXCTestCase {
|
||||
"DERIVED_SOURCES_DIR": "DERIVED_SOURCES_DIR",
|
||||
"CURRENT_VARIANT": "normal",
|
||||
"PUBLIC_HEADERS_FOLDER_PATH": "/usr/local/include",
|
||||
"LLBUILD_BUILD_ID": "1",
|
||||
]
|
||||
|
||||
override func setUpWithError() throws {
|
||||
@@ -150,4 +151,53 @@ class PostbuildContextTests: FileXCTestCase {
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func testDisabledEnvIsFalseByDefault() throws {
|
||||
var envs = Self.SampleEnvs
|
||||
envs.removeValue(forKey: "XCRC_DISABLED")
|
||||
|
||||
let context = try PostbuildContext(config, env: envs)
|
||||
|
||||
XCTAssertFalse(context.disabled)
|
||||
}
|
||||
|
||||
func testDisabledIsTrueForYesEnv() throws {
|
||||
var envs = Self.SampleEnvs
|
||||
envs["XCRC_DISABLED"] = "YES"
|
||||
|
||||
let context = try PostbuildContext(config, env: envs)
|
||||
|
||||
XCTAssertTrue(context.disabled)
|
||||
}
|
||||
|
||||
func testDisabledIsFalseForNonYesEnv() throws {
|
||||
var envs = Self.SampleEnvs
|
||||
envs["XCRC_DISABLED"] = "NO"
|
||||
|
||||
let context = try PostbuildContext(config, env: envs)
|
||||
|
||||
XCTAssertFalse(context.disabled)
|
||||
}
|
||||
|
||||
func testFailsIfLlBuildIdEnvIsMissing() throws {
|
||||
var envs = Self.SampleEnvs
|
||||
envs.removeValue(forKey: "LLBUILD_BUILD_ID")
|
||||
|
||||
XCTAssertThrowsError(try PostbuildContext(config, env: envs))
|
||||
}
|
||||
|
||||
func testBuildsLockValidFileUrl() throws {
|
||||
let context = try PostbuildContext(config, env: Self.SampleEnvs)
|
||||
|
||||
XCTAssertEqual(context.llbuildIdLockFile, "TARGET_TEMP_DIR/1.lock")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,9 @@ class PostbuildTests: FileXCTestCase {
|
||||
modeMarkerPath: "",
|
||||
overlayHeadersPath: "",
|
||||
irrelevantDependenciesPaths: [],
|
||||
publicHeadersFolderPath: nil
|
||||
publicHeadersFolderPath: nil,
|
||||
disabled: false,
|
||||
llbuildIdLockFile: "/file"
|
||||
)
|
||||
private var network = RemoteNetworkClientImpl(
|
||||
NetworkClientFake(fileManager: .default),
|
||||
|
||||
@@ -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.
|
||||
|
||||
@testable import XCRemoteCache
|
||||
import XCTest
|
||||
|
||||
class PrebuildContextTests: FileXCTestCase {
|
||||
private var config: XCRemoteCacheConfig!
|
||||
private var remoteCommitFile: URL!
|
||||
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",
|
||||
"PLATFORM_NAME": "PLATFORM_NAME",
|
||||
"XCODE_PRODUCT_BUILD_VERSION": "XCODE_PRODUCT_BUILD_VERSION",
|
||||
"TARGET_BUILD_DIR": "TARGET_BUILD_DIR",
|
||||
"PRODUCT_MODULE_NAME": "PRODUCT_MODULE_NAME",
|
||||
"EXECUTABLE_PATH": "EXECUTABLE_PATH",
|
||||
"SRCROOT": "SRCROOT",
|
||||
"DEVELOPER_DIR": "DEVELOPER_DIR",
|
||||
"MACH_O_TYPE": "MACH_O_TYPE",
|
||||
"DWARF_DSYM_FILE_SHOULD_ACCOMPANY_PRODUCT": "DWARF_DSYM_FILE_SHOULD_ACCOMPANY_PRODUCT",
|
||||
"DWARF_DSYM_FOLDER_PATH": "DWARF_DSYM_FOLDER_PATH",
|
||||
"DWARF_DSYM_FILE_NAME": "DWARF_DSYM_FILE_NAME",
|
||||
"BUILT_PRODUCTS_DIR": "BUILT_PRODUCTS_DIR",
|
||||
"DERIVED_SOURCES_DIR": "DERIVED_SOURCES_DIR",
|
||||
"CURRENT_VARIANT": "normal",
|
||||
"PUBLIC_HEADERS_FOLDER_PATH": "/usr/local/include",
|
||||
"LLBUILD_BUILD_ID": "1",
|
||||
]
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
let workingDir = try prepareTempDir()
|
||||
remoteCommitFile = workingDir.appendingPathComponent("arc.rc")
|
||||
_ = workingDir.appendingPathComponent("mpo")
|
||||
config = XCRemoteCacheConfig(remoteCommitFile: remoteCommitFile.path, sourceRoot: workingDir.path)
|
||||
config.recommendedCacheAddress = "http://test.com"
|
||||
}
|
||||
|
||||
func testFailsIfLlBuildIdEnvIsMissing() throws {
|
||||
var envs = Self.SampleEnvs
|
||||
envs.removeValue(forKey: "LLBUILD_BUILD_ID")
|
||||
|
||||
XCTAssertThrowsError(try PrebuildContext(config, env: envs))
|
||||
}
|
||||
|
||||
func testBuildsLockValidFileUrl() throws {
|
||||
let context = try PrebuildContext(config, env: Self.SampleEnvs)
|
||||
|
||||
XCTAssertEqual(context.llbuildIdLockFile, "TARGET_TEMP_DIR/1.lock")
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
@testable import XCRemoteCache
|
||||
import XCTest
|
||||
|
||||
// swiftlint:disable file_length
|
||||
// swiftlint:disable:next type_body_length
|
||||
class PrebuildTests: FileXCTestCase {
|
||||
|
||||
@@ -52,6 +53,13 @@ class PrebuildTests: FileXCTestCase {
|
||||
remoteNetwork = RemoteNetworkClientImpl(network, URLBuilderFake(remoteCacheURL))
|
||||
remapper = DependenciesRemapperFake(baseURL: URL(fileURLWithPath: "/"))
|
||||
metaReader = JsonMetaReader(fileAccessor: FileManager.default)
|
||||
setupNonCachedContext()
|
||||
setupCachedContext()
|
||||
organizer = ArtifactOrganizerFake(artifactRoot: artifactsRoot, unzippedExtension: "unzip")
|
||||
globalCacheSwitcher = InMemoryGlobalCacheSwitcher()
|
||||
}
|
||||
|
||||
private func setupNonCachedContext() {
|
||||
contextNonCached = PrebuildContext(
|
||||
targetTempDir: sampleURL,
|
||||
productsDir: sampleURL,
|
||||
@@ -63,8 +71,13 @@ class PrebuildTests: FileXCTestCase {
|
||||
compilationHistoryFile: compilationHistory,
|
||||
turnOffRemoteCacheOnFirstTimeout: true,
|
||||
targetName: "",
|
||||
overlayHeadersPath: ""
|
||||
overlayHeadersPath: "",
|
||||
disabled: false,
|
||||
llbuildIdLockFile: "/tmp/lock"
|
||||
)
|
||||
}
|
||||
|
||||
private func setupCachedContext() {
|
||||
contextCached = PrebuildContext(
|
||||
targetTempDir: sampleURL,
|
||||
productsDir: sampleURL,
|
||||
@@ -76,10 +89,10 @@ class PrebuildTests: FileXCTestCase {
|
||||
compilationHistoryFile: compilationHistory,
|
||||
turnOffRemoteCacheOnFirstTimeout: true,
|
||||
targetName: "",
|
||||
overlayHeadersPath: ""
|
||||
overlayHeadersPath: "",
|
||||
disabled: false,
|
||||
llbuildIdLockFile: "/tmp/lock"
|
||||
)
|
||||
organizer = ArtifactOrganizerFake(artifactRoot: artifactsRoot, unzippedExtension: "unzip")
|
||||
globalCacheSwitcher = InMemoryGlobalCacheSwitcher()
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
@@ -241,7 +254,9 @@ class PrebuildTests: FileXCTestCase {
|
||||
compilationHistoryFile: compilationHistory,
|
||||
turnOffRemoteCacheOnFirstTimeout: true,
|
||||
targetName: "",
|
||||
overlayHeadersPath: ""
|
||||
overlayHeadersPath: "",
|
||||
disabled: false,
|
||||
llbuildIdLockFile: "/tmp/lock"
|
||||
)
|
||||
|
||||
let prebuild = Prebuild(
|
||||
@@ -272,7 +287,9 @@ class PrebuildTests: FileXCTestCase {
|
||||
compilationHistoryFile: compilationHistory,
|
||||
turnOffRemoteCacheOnFirstTimeout: true,
|
||||
targetName: "",
|
||||
overlayHeadersPath: ""
|
||||
overlayHeadersPath: "",
|
||||
disabled: false,
|
||||
llbuildIdLockFile: "/tmp/lock"
|
||||
)
|
||||
metaContent = try generateMeta(fingerprint: generator.generate(), filekey: "1")
|
||||
let downloadedArtifactPackage = artifactsRoot.appendingPathComponent("1")
|
||||
@@ -335,7 +352,9 @@ class PrebuildTests: FileXCTestCase {
|
||||
compilationHistoryFile: compilationHistory,
|
||||
turnOffRemoteCacheOnFirstTimeout: false,
|
||||
targetName: "",
|
||||
overlayHeadersPath: ""
|
||||
overlayHeadersPath: "",
|
||||
disabled: false,
|
||||
llbuildIdLockFile: "/tmp/lock"
|
||||
)
|
||||
try globalCacheSwitcher.enable(sha: "1")
|
||||
let prebuild = Prebuild(
|
||||
@@ -353,4 +372,35 @@ class PrebuildTests: FileXCTestCase {
|
||||
|
||||
XCTAssertEqual(globalCacheSwitcher.state, .enabled(sha: "1"))
|
||||
}
|
||||
|
||||
func testReturnsDisabledIfXCRCExplicitlyDisabled() throws {
|
||||
contextNonCached = PrebuildContext(
|
||||
targetTempDir: sampleURL,
|
||||
productsDir: sampleURL,
|
||||
moduleName: nil,
|
||||
remoteCommit: .unavailable,
|
||||
remoteCommitLocation: sampleURL,
|
||||
recommendedCacheAddress: sampleURL,
|
||||
forceCached: false,
|
||||
compilationHistoryFile: compilationHistory,
|
||||
turnOffRemoteCacheOnFirstTimeout: true,
|
||||
targetName: "",
|
||||
overlayHeadersPath: "",
|
||||
disabled: true,
|
||||
llbuildIdLockFile: "/tmp/lock"
|
||||
)
|
||||
|
||||
let prebuild = Prebuild(
|
||||
context: contextNonCached,
|
||||
networkClient: remoteNetwork,
|
||||
remapper: remapper,
|
||||
fingerprintAccumulator: generator,
|
||||
artifactsOrganizer: organizer,
|
||||
globalCacheSwitcher: globalCacheSwitcher,
|
||||
metaReader: metaReader,
|
||||
artifactConsumerPrebuildPlugins: []
|
||||
)
|
||||
|
||||
XCTAssertEqual(try prebuild.perform(), .disabled)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// 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.
|
||||
|
||||
@testable import XCRemoteCache
|
||||
import XCTest
|
||||
|
||||
class IntegrateTests: FileXCTestCase {
|
||||
private var config: XCRemoteCacheConfig!
|
||||
private var remoteCommitFile: URL!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
let workingDir = try prepareTempDir()
|
||||
remoteCommitFile = workingDir.appendingPathComponent("arc.rc")
|
||||
_ = workingDir.appendingPathComponent("mpo")
|
||||
config = XCRemoteCacheConfig(remoteCommitFile: remoteCommitFile.path, sourceRoot: workingDir.path)
|
||||
config.recommendedCacheAddress = "http://test.com"
|
||||
}
|
||||
|
||||
|
||||
func tesFallbacksToNoDriverByDefault() throws {
|
||||
let context = try IntegrateContext(
|
||||
input: "project.xcodeproj",
|
||||
config: config,
|
||||
mode: .producer,
|
||||
env: [:],
|
||||
binariesDir: "/binaries",
|
||||
fakeSrcRoot: "/src",
|
||||
outputPath: "/output"
|
||||
)
|
||||
|
||||
XCTAssertEqual(context.buildSettingsAppenderOptions, [.disableSwiftDriverIntegration])
|
||||
XCTAssertEqual(context.binaries.swiftc, "/binaries/xcswiftc")
|
||||
}
|
||||
|
||||
func testEnablesDriverOnRequest() throws {
|
||||
config.enableSwiftDriverIntegration = true
|
||||
let context = try IntegrateContext(
|
||||
input: "project.xcodeproj",
|
||||
config: config,
|
||||
mode: .producer,
|
||||
env: [:],
|
||||
binariesDir: "/binaries",
|
||||
fakeSrcRoot: "/src",
|
||||
outputPath: "/output"
|
||||
)
|
||||
|
||||
XCTAssertEqual(context.buildSettingsAppenderOptions, [])
|
||||
XCTAssertEqual(context.binaries.swiftc, "/binaries/swiftc")
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ class XcodeProjBuildSettingsIntegrateAppenderTests: XCTestCase {
|
||||
cc: binariesDir.appendingPathComponent("xccc"),
|
||||
swiftc: binariesDir.appendingPathComponent("xcswiftc"),
|
||||
libtool: binariesDir.appendingPathComponent("xclibtool"),
|
||||
lipo: binariesDir.appendingPathComponent("lipo"),
|
||||
ld: binariesDir.appendingPathComponent("xcld"),
|
||||
ldplusplus: binariesDir.appendingPathComponent("xcldplusplus"),
|
||||
prebuild: binariesDir.appendingPathComponent("xcprebuild"),
|
||||
@@ -44,7 +45,13 @@ class XcodeProjBuildSettingsIntegrateAppenderTests: XCTestCase {
|
||||
func testProducerSettingFakeSrcRoot() throws {
|
||||
let mode: Mode = .producer
|
||||
let fakeRootURL: URL = "/xxxxxxxxxxP"
|
||||
let appender = XcodeProjBuildSettingsIntegrateAppender(mode: mode, repoRoot: rootURL, fakeSrcRoot: fakeRootURL)
|
||||
let appender = XcodeProjBuildSettingsIntegrateAppender(
|
||||
mode: mode,
|
||||
repoRoot: rootURL,
|
||||
fakeSrcRoot: fakeRootURL,
|
||||
sdksExclude: [],
|
||||
options: []
|
||||
)
|
||||
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
|
||||
let resultURL = try XCTUnwrap(result["XCRC_FAKE_SRCROOT"] as? String)
|
||||
|
||||
@@ -54,10 +61,126 @@ class XcodeProjBuildSettingsIntegrateAppenderTests: XCTestCase {
|
||||
func testConsumerSettingFakeSrcRoot() throws {
|
||||
let mode: Mode = .consumer
|
||||
let fakeRootURL: URL = "/xxxxxxxxxxC"
|
||||
let appender = XcodeProjBuildSettingsIntegrateAppender(mode: mode, repoRoot: rootURL, fakeSrcRoot: fakeRootURL)
|
||||
let appender = XcodeProjBuildSettingsIntegrateAppender(
|
||||
mode: mode,
|
||||
repoRoot: rootURL,
|
||||
fakeSrcRoot: fakeRootURL,
|
||||
sdksExclude: [],
|
||||
options: []
|
||||
)
|
||||
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,
|
||||
sdksExclude: [],
|
||||
options: []
|
||||
)
|
||||
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
|
||||
let ldPlusPlus: String = try XCTUnwrap(result["LDPLUSPLUS"] as? String)
|
||||
|
||||
XCTAssertEqual(ldPlusPlus, binaries.ldplusplus.path)
|
||||
}
|
||||
|
||||
func testSinglesdksExcludeIsAppended() throws {
|
||||
let mode: Mode = .consumer
|
||||
let appender = XcodeProjBuildSettingsIntegrateAppender(
|
||||
mode: mode,
|
||||
repoRoot: rootURL,
|
||||
fakeSrcRoot: "/",
|
||||
sdksExclude: ["watchOS*"],
|
||||
options: []
|
||||
)
|
||||
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
|
||||
let ldPlusPlusWatchOS: String = try XCTUnwrap(result["LDPLUSPLUS[sdk=watchOS*]"] as? String)
|
||||
|
||||
XCTAssertEqual(ldPlusPlusWatchOS, "")
|
||||
}
|
||||
|
||||
func testLibtoolIsSetForExcludedSdks() throws {
|
||||
let mode: Mode = .consumer
|
||||
let appender = XcodeProjBuildSettingsIntegrateAppender(
|
||||
mode: mode,
|
||||
repoRoot: rootURL,
|
||||
fakeSrcRoot: "/",
|
||||
sdksExclude: ["watchOS*"],
|
||||
options: []
|
||||
)
|
||||
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
|
||||
let libtoolWatchOS: String = try XCTUnwrap(result["LIBTOOL[sdk=watchOS*]"] as? String)
|
||||
|
||||
XCTAssertEqual(libtoolWatchOS, "libtool")
|
||||
}
|
||||
|
||||
func testMultiplesdksExcludeAreAppended() throws {
|
||||
let mode: Mode = .consumer
|
||||
let appender = XcodeProjBuildSettingsIntegrateAppender(
|
||||
mode: mode,
|
||||
repoRoot: rootURL,
|
||||
fakeSrcRoot: "/",
|
||||
sdksExclude: ["watchOS*", "watchsimulator*"],
|
||||
options: []
|
||||
)
|
||||
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
|
||||
let ldPlusPlusWatchOS: String = try XCTUnwrap(result["LDPLUSPLUS[sdk=watchOS*]"] as? String)
|
||||
let ldPlusPlusWatchSimulator: String = try XCTUnwrap(result["LDPLUSPLUS[sdk=watchsimulator*]"] as? String)
|
||||
|
||||
XCTAssertEqual(ldPlusPlusWatchOS, "")
|
||||
XCTAssertEqual(ldPlusPlusWatchSimulator, "")
|
||||
}
|
||||
|
||||
func testAddsDisabledFlagForExcludedSDKs() throws {
|
||||
let mode: Mode = .consumer
|
||||
let appender = XcodeProjBuildSettingsIntegrateAppender(
|
||||
mode: mode,
|
||||
repoRoot: rootURL,
|
||||
fakeSrcRoot: "/",
|
||||
sdksExclude: ["watchOS*", "watchsimulator*"],
|
||||
options: []
|
||||
)
|
||||
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
|
||||
let disabledWatchOS: String = try XCTUnwrap(result["XCRC_DISABLED[sdk=watchOS*]"] as? String)
|
||||
let disabledWatchSimulator: String = try XCTUnwrap(result["XCRC_DISABLED[sdk=watchsimulator*]"] as? String)
|
||||
|
||||
XCTAssertEqual(disabledWatchOS, "YES")
|
||||
XCTAssertEqual(disabledWatchSimulator, "YES")
|
||||
}
|
||||
|
||||
func testExcludesSwiftFrontendIntegrationForSpecificOption() throws {
|
||||
let mode: Mode = .consumer
|
||||
let appender = XcodeProjBuildSettingsIntegrateAppender(
|
||||
mode: mode,
|
||||
repoRoot: rootURL,
|
||||
fakeSrcRoot: "/",
|
||||
sdksExclude: [],
|
||||
options: [.disableSwiftDriverIntegration]
|
||||
)
|
||||
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
|
||||
let useSwiftIntegrationDriver: String = try XCTUnwrap(result["SWIFT_USE_INTEGRATED_DRIVER"] as? String)
|
||||
|
||||
XCTAssertEqual(useSwiftIntegrationDriver, "NO")
|
||||
}
|
||||
|
||||
func testDoesntExcludesSwiftFrontendIntegrationForEmptyOptions() throws {
|
||||
let mode: Mode = .consumer
|
||||
let appender = XcodeProjBuildSettingsIntegrateAppender(
|
||||
mode: mode,
|
||||
repoRoot: rootURL,
|
||||
fakeSrcRoot: "/",
|
||||
sdksExclude: [],
|
||||
options: []
|
||||
)
|
||||
let result = appender.appendToBuildSettings(buildSettings: buildSettings, wrappers: binaries)
|
||||
let useSwiftIntegrationDriver: String? = result["SWIFT_USE_INTEGRATED_DRIVER"] as? String
|
||||
|
||||
XCTAssertNil(useSwiftIntegrationDriver)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ class PrepareMarkContextTests: XCTestCase {
|
||||
let repoPath = "/AbsolutePath"
|
||||
config.repoRoot = repoPath
|
||||
|
||||
let context = try PrepareMarkContext(config)
|
||||
let context = try PrepareMarkContext(config, env: [:])
|
||||
|
||||
XCTAssertEqual(context.repoRoot.path, repoPath)
|
||||
}
|
||||
@@ -43,8 +43,20 @@ class PrepareMarkContextTests: XCTestCase {
|
||||
let repoPath = "."
|
||||
config.repoRoot = repoPath
|
||||
|
||||
let context = try PrepareMarkContext(config)
|
||||
let context = try PrepareMarkContext(config, env: [:])
|
||||
|
||||
XCTAssertEqual(context.repoRoot.path, "/Root")
|
||||
}
|
||||
|
||||
func testDisabledIsFalseByDefault() throws {
|
||||
let context = try PrepareMarkContext(config, env: [:])
|
||||
|
||||
XCTAssertFalse(context.disabled)
|
||||
}
|
||||
|
||||
func testDisabledIsTrueForYes() throws {
|
||||
let context = try PrepareMarkContext(config, env: ["XCRC_DISABLED": "YES"])
|
||||
|
||||
XCTAssertTrue(context.disabled)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,4 +89,66 @@ class PrepareTests: XCTestCase {
|
||||
|
||||
XCTAssertEqual(result, .preparedFor(sha: .init(sha: "2", age: 0), recommendedCacheAddress: remoteURL))
|
||||
}
|
||||
|
||||
func testFailsForMissingCommonShaWhenConfiguredToGrefullyDisable() throws {
|
||||
git = GitClientFake(shaHistory: [], primaryBranchIndex: 0)
|
||||
config.gracefullyHandleMissingCommonSha = true
|
||||
|
||||
let prepare = Prepare(
|
||||
context: try PrepareContext(config, offline: false),
|
||||
gitClient: git,
|
||||
networkClients: [],
|
||||
ccBuilder: CCWrapperBuilderFake(),
|
||||
fileAccessor: FileManager.default,
|
||||
globalCacheSwitcher: globalCacheSwitcher,
|
||||
cacheInvalidator: CacheInvalidatorFake()
|
||||
)
|
||||
|
||||
let result = try prepare.prepare()
|
||||
|
||||
XCTAssertEqual(result, .failed)
|
||||
}
|
||||
|
||||
func testDisablesGlobalCacheForMissingCommonShaWhenConfiguredToGrefullyDisable() throws {
|
||||
git = GitClientFake(shaHistory: [], primaryBranchIndex: 0)
|
||||
config.gracefullyHandleMissingCommonSha = true
|
||||
try globalCacheSwitcher.enable(sha: "starting_state")
|
||||
|
||||
let prepare = Prepare(
|
||||
context: try PrepareContext(config, offline: false),
|
||||
gitClient: git,
|
||||
networkClients: [],
|
||||
ccBuilder: CCWrapperBuilderFake(),
|
||||
fileAccessor: FileManager.default,
|
||||
globalCacheSwitcher: globalCacheSwitcher,
|
||||
cacheInvalidator: CacheInvalidatorFake()
|
||||
)
|
||||
|
||||
_ = try prepare.prepare()
|
||||
|
||||
XCTAssertEqual(globalCacheSwitcher.state, .disabled)
|
||||
}
|
||||
|
||||
func testThrowsForMissingCommonSha() throws {
|
||||
git = GitClientFake(shaHistory: [], primaryBranchIndex: 0)
|
||||
config.gracefullyHandleMissingCommonSha = false
|
||||
|
||||
let prepare = Prepare(
|
||||
context: try PrepareContext(config, offline: false),
|
||||
gitClient: git,
|
||||
networkClients: [],
|
||||
ccBuilder: CCWrapperBuilderFake(),
|
||||
fileAccessor: FileManager.default,
|
||||
globalCacheSwitcher: globalCacheSwitcher,
|
||||
cacheInvalidator: CacheInvalidatorFake()
|
||||
)
|
||||
|
||||
XCTAssertThrowsError(try prepare.prepare()) { error in
|
||||
switch error {
|
||||
case GitClientError.noCommonShaWithPrimaryRepo: break
|
||||
default:
|
||||
XCTFail("Not expected error")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
// 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.
|
||||
|
||||
@testable import XCRemoteCache
|
||||
import XCTest
|
||||
|
||||
// swiftlint:disable type_body_length
|
||||
class SwiftFrontendArgInputTests: FileXCTestCase {
|
||||
private var compile: Bool = true
|
||||
private var emitModule: Bool = false
|
||||
private var objcHeaderOutput: String?
|
||||
private var moduleName: String?
|
||||
private var target: String?
|
||||
private var primaryInputPaths: [String] = []
|
||||
private var inputPaths: [String] = []
|
||||
private var outputPaths: [String] = []
|
||||
private var dependenciesPaths: [String] = []
|
||||
private var diagnosticsPaths: [String] = []
|
||||
private var sourceInfoPath: String?
|
||||
private var docPath: String?
|
||||
private var supplementaryOutputFileMap: String?
|
||||
|
||||
private var config: XCRemoteCacheConfig!
|
||||
private var input: SwiftFrontendArgInput!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
let workingDir = try prepareTempDir()
|
||||
let remoteCommitFile = workingDir.appendingPathComponent("arc.rc")
|
||||
config = XCRemoteCacheConfig(remoteCommitFile: remoteCommitFile.path, sourceRoot: workingDir.path)
|
||||
config.recommendedCacheAddress = "http://test.com"
|
||||
|
||||
buildInput()
|
||||
}
|
||||
|
||||
private func buildInput() {
|
||||
input = SwiftFrontendArgInput(
|
||||
compile: compile,
|
||||
emitModule: emitModule,
|
||||
objcHeaderOutput: objcHeaderOutput,
|
||||
moduleName: moduleName,
|
||||
target: target,
|
||||
primaryInputPaths: primaryInputPaths,
|
||||
inputPaths: inputPaths,
|
||||
outputPaths: outputPaths,
|
||||
dependenciesPaths: dependenciesPaths,
|
||||
diagnosticsPaths: diagnosticsPaths,
|
||||
sourceInfoPath: sourceInfoPath,
|
||||
docPath: docPath,
|
||||
supplementaryOutputFileMap: supplementaryOutputFileMap)
|
||||
}
|
||||
|
||||
private func assertGenerationError(_ expectedError: SwiftFrontendArgInputError) {
|
||||
XCTAssertThrowsError(try input.generateSwiftcContext(config: config)) { error in
|
||||
guard let e = error as? SwiftFrontendArgInputError else {
|
||||
XCTFail("Received invalid error \(error). Expected: \(expectedError)")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(e, expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func testFailsForNoStep() throws {
|
||||
compile = false
|
||||
emitModule = false
|
||||
buildInput()
|
||||
|
||||
assertGenerationError(SwiftFrontendArgInputError.bothCompilationAndEmitAction)
|
||||
}
|
||||
|
||||
func testFailsIfNoCompilationFiles() throws {
|
||||
buildInput()
|
||||
|
||||
assertGenerationError(SwiftFrontendArgInputError.noCompilationInputs)
|
||||
}
|
||||
|
||||
func testFailsIfNoTarget() throws {
|
||||
inputPaths = ["/file1"]
|
||||
buildInput()
|
||||
|
||||
assertGenerationError(SwiftFrontendArgInputError.emitMissingTarget)
|
||||
}
|
||||
|
||||
func testFailsIfNoModuleName() throws {
|
||||
inputPaths = ["/file1"]
|
||||
target = "Target"
|
||||
buildInput()
|
||||
|
||||
assertGenerationError(SwiftFrontendArgInputError.emitMissingModuleName)
|
||||
}
|
||||
|
||||
func testFailsIfNoCompileHasNoPrimaryInputs() throws {
|
||||
inputPaths = ["/file1"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
buildInput()
|
||||
|
||||
assertGenerationError(SwiftFrontendArgInputError.noPrimaryFileCompilationInputs)
|
||||
}
|
||||
|
||||
func testFailsIfDependenciesAreMissing() throws {
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
primaryInputPaths = ["/file1", "/file2"]
|
||||
dependenciesPaths = ["/file1.d"]
|
||||
buildInput()
|
||||
|
||||
assertGenerationError(SwiftFrontendArgInputError.dependenciesOuputCountDoesntMatch(expected: 2, parsed: 1))
|
||||
}
|
||||
|
||||
func testDoesntFailForMissingDependenciesIfNoDependencies() throws {
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
primaryInputPaths = ["/file1", "/file2"]
|
||||
dependenciesPaths = []
|
||||
buildInput()
|
||||
|
||||
assertGenerationError(SwiftFrontendArgInputError.outputsOuputCountDoesntMatch(expected: 2, parsed: 0))
|
||||
}
|
||||
|
||||
func testFailsIfDiagnosticsAreMissing() throws {
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
primaryInputPaths = ["/file1", "/file2"]
|
||||
diagnosticsPaths = ["/file1.d"]
|
||||
buildInput()
|
||||
|
||||
assertGenerationError(SwiftFrontendArgInputError.diagnosticsOuputCountDoesntMatch(expected: 2, parsed: 1))
|
||||
}
|
||||
|
||||
func testDoesntFailForMissingDdiagnosticsIfNoDiagnostics() throws {
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
primaryInputPaths = ["/file1", "/file2"]
|
||||
diagnosticsPaths = []
|
||||
buildInput()
|
||||
|
||||
assertGenerationError(SwiftFrontendArgInputError.outputsOuputCountDoesntMatch(expected: 2, parsed: 0))
|
||||
}
|
||||
|
||||
func testFailsIfOutputsAreMissing() throws {
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
primaryInputPaths = ["/file1", "/file2"]
|
||||
outputPaths = ["/file1.o"]
|
||||
buildInput()
|
||||
|
||||
assertGenerationError(SwiftFrontendArgInputError.outputsOuputCountDoesntMatch(expected: 2, parsed: 1))
|
||||
}
|
||||
|
||||
func testSetsCompilationSubsetForCompilation() throws {
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
primaryInputPaths = ["/file1"]
|
||||
outputPaths = ["/file1.o"]
|
||||
buildInput()
|
||||
|
||||
let context = try input.generateSwiftcContext(config: config)
|
||||
|
||||
XCTAssertEqual(context.steps, .init(
|
||||
compileFilesScope: .subset(["/file1"]),
|
||||
emitModule: .none
|
||||
))
|
||||
}
|
||||
|
||||
func testBuildCompilationFilesInputs() throws {
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
primaryInputPaths = ["/file1"]
|
||||
outputPaths = ["/file1.o"]
|
||||
dependenciesPaths = ["/file1.d"]
|
||||
buildInput()
|
||||
|
||||
let context = try input.generateSwiftcContext(config: config)
|
||||
|
||||
XCTAssertEqual(context.inputs, .map([
|
||||
"/file1": SwiftFileCompilationInfo(
|
||||
file: "/file1",
|
||||
dependencies: "/file1.d",
|
||||
object: "/file1.o",
|
||||
swiftDependencies: nil),
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
func testRecognizesArchFromOuputFirstPaths() throws {
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
primaryInputPaths = ["/file1"]
|
||||
outputPaths = ["/TARGET_TEMP_DIR/Object-normal/arm64/file1.o"]
|
||||
dependenciesPaths = ["/file1.d"]
|
||||
buildInput()
|
||||
|
||||
let context = try input.generateSwiftcContext(config: config)
|
||||
|
||||
XCTAssertEqual(context.arch, "arm64")
|
||||
}
|
||||
|
||||
func testPassesExtraParams() throws {
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
primaryInputPaths = ["/file1"]
|
||||
outputPaths = ["/file1.o"]
|
||||
dependenciesPaths = ["/file1.d"]
|
||||
buildInput()
|
||||
|
||||
let context = try input.generateSwiftcContext(config: config)
|
||||
|
||||
XCTAssertEqual(context.moduleName, "Module")
|
||||
XCTAssertEqual(context.target, "Target")
|
||||
XCTAssertEqual(context.compilationFiles, .list(inputPaths))
|
||||
XCTAssertEqual(context.mode, .consumer(commit: .unavailable))
|
||||
}
|
||||
|
||||
func testEmitModuleFailsForMissingOutput() throws {
|
||||
emitModule = true
|
||||
compile = false
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
outputPaths = []
|
||||
buildInput()
|
||||
|
||||
assertGenerationError(SwiftFrontendArgInputError.emitModulOuputCountIsNot1(parsed: 0))
|
||||
}
|
||||
|
||||
func testEmitModuleFailsForMissingObjcHeader() throws {
|
||||
emitModule = true
|
||||
compile = false
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
outputPaths = ["/Module.swiftmodule"]
|
||||
buildInput()
|
||||
|
||||
assertGenerationError(SwiftFrontendArgInputError.emitModuleMissingObjcHeaderPath)
|
||||
}
|
||||
|
||||
func testEmitModuleFailsForExcessiveDiagnostics() throws {
|
||||
emitModule = true
|
||||
compile = false
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
outputPaths = ["/Module.swiftmodule"]
|
||||
objcHeaderOutput = "/file-Swift.h"
|
||||
diagnosticsPaths = ["/file.diag", "/file2.diag"]
|
||||
buildInput()
|
||||
|
||||
assertGenerationError(SwiftFrontendArgInputError.emitModuleDiagnosticsOuputCountIsHigherThan1(parsed: 2))
|
||||
}
|
||||
|
||||
func testEmitModuleFailsForExcessiveDependencies() throws {
|
||||
emitModule = true
|
||||
compile = false
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
outputPaths = ["/Module.swiftmodule"]
|
||||
objcHeaderOutput = "/file-Swift.h"
|
||||
dependenciesPaths = ["/file.d", "/file2.d"]
|
||||
buildInput()
|
||||
|
||||
assertGenerationError(SwiftFrontendArgInputError.emitModuleDependenciesOuputCountIsHigherThan1(parsed: 2))
|
||||
}
|
||||
|
||||
func testEmitModuleSetsStep() throws {
|
||||
emitModule = true
|
||||
compile = false
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
outputPaths = ["/Module.swiftmodule"]
|
||||
objcHeaderOutput = "/file-Swift.h"
|
||||
diagnosticsPaths = ["/file.dia"]
|
||||
dependenciesPaths = ["/file.d"]
|
||||
buildInput()
|
||||
|
||||
let context = try input.generateSwiftcContext(config: config)
|
||||
|
||||
XCTAssertEqual(context.steps, .init(
|
||||
compileFilesScope: .none,
|
||||
emitModule: .init(
|
||||
objcHeaderOutput: "/file-Swift.h",
|
||||
modulePathOutput: "/Module.swiftmodule",
|
||||
dependencies: "/file.d"))
|
||||
)
|
||||
}
|
||||
|
||||
func testEmitModuleSetsAllIntpus() throws {
|
||||
emitModule = true
|
||||
compile = false
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
outputPaths = ["/Module.swiftmodule"]
|
||||
objcHeaderOutput = "/file-Swift.h"
|
||||
buildInput()
|
||||
|
||||
let context = try input.generateSwiftcContext(config: config)
|
||||
|
||||
XCTAssertEqual(context.compilationFiles, .list(inputPaths))
|
||||
}
|
||||
|
||||
func testEmitModuleRecognizesArchFromObjCHeader() throws {
|
||||
emitModule = true
|
||||
compile = false
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
outputPaths = ["file.swiftmodule"]
|
||||
objcHeaderOutput = "/TARGET_TEMP_DIR/Object-normal/arm64/file-Swift.h"
|
||||
buildInput()
|
||||
|
||||
let context = try input.generateSwiftcContext(config: config)
|
||||
|
||||
XCTAssertEqual(context.arch, "arm64")
|
||||
}
|
||||
|
||||
func testEmitModulePassesExtraParams() throws {
|
||||
emitModule = true
|
||||
compile = false
|
||||
inputPaths = ["/file1", "/file2", "/file3"]
|
||||
target = "Target"
|
||||
moduleName = "Module"
|
||||
outputPaths = ["/Module.swiftmodule"]
|
||||
objcHeaderOutput = "/file-Swift.h"
|
||||
buildInput()
|
||||
|
||||
let context = try input.generateSwiftcContext(config: config)
|
||||
|
||||
XCTAssertEqual(context.moduleName, "Module")
|
||||
XCTAssertEqual(context.target, "Target")
|
||||
XCTAssertEqual(context.compilationFiles, .list(inputPaths))
|
||||
XCTAssertEqual(context.mode, .consumer(commit: .unavailable))
|
||||
}
|
||||
}
|
||||
// swiftlint:enable type_body_length
|
||||
@@ -0,0 +1,56 @@
|
||||
// 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.
|
||||
|
||||
@testable import XCRemoteCache
|
||||
import XCTest
|
||||
|
||||
|
||||
class SwiftFrontendContextTests: XCTestCase {
|
||||
|
||||
private var swiftcContext: SwiftcContext!
|
||||
private let targetTempDir: URL = "/temp"
|
||||
|
||||
override func setUp() async throws {
|
||||
swiftcContext = try SwiftcContext(
|
||||
config: .init(sourceRoot: ""),
|
||||
input: .init(
|
||||
objcHeaderOutput: "",
|
||||
moduleName: "",
|
||||
modulePathOutput: "\(targetTempDir.path)/Objects-normal/$ARCH/some.file",
|
||||
filemap: "",
|
||||
target: "",
|
||||
fileList: ""
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testBuildsBuildLockInTargetTemp() throws {
|
||||
let env = [
|
||||
"LLBUILD_BUILD_ID": "1",
|
||||
]
|
||||
|
||||
let frontendContext = try SwiftFrontendContext(swiftcContext, env: env)
|
||||
|
||||
XCTAssertEqual(frontendContext.invocationLockFile, targetTempDir.appendingPathComponent("1.lock"))
|
||||
}
|
||||
|
||||
func testInitializerFailsIfLlBuildIdIsMissingInEnv() throws {
|
||||
XCTAssertThrowsError(try SwiftFrontendContext(swiftcContext, env: [:]))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
// 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.
|
||||
|
||||
@testable import XCRemoteCache
|
||||
import XCTest
|
||||
|
||||
|
||||
final class SwiftFrontendOrchestratorTests: FileXCTestCase {
|
||||
private let prohibitedAccessor = DisallowedExclusiveFileAccessor()
|
||||
private var nonEmptyFile: URL!
|
||||
private var emptyFile: URL!
|
||||
private let maxLocking: TimeInterval = 10
|
||||
|
||||
override func setUp() async throws {
|
||||
nonEmptyFile = try prepareTempDir().appendingPathComponent("lock.lock")
|
||||
try fileManager.write(toPath: nonEmptyFile.path, contents: "done".data(using: .utf8))
|
||||
emptyFile = try prepareTempDir().appendingPathComponent("lock_empty.lock")
|
||||
try fileManager.write(toPath: emptyFile.path, contents: .init())
|
||||
}
|
||||
|
||||
func testRunsCriticalSectionImmediatelyForProducer() throws {
|
||||
let orchestrator = CommonSwiftFrontendOrchestrator(
|
||||
mode: .producer,
|
||||
action: .compile,
|
||||
lockAccessor: prohibitedAccessor,
|
||||
maxLockTimeout: maxLocking
|
||||
)
|
||||
|
||||
var invoked = false
|
||||
try orchestrator.run {
|
||||
invoked = true
|
||||
}
|
||||
XCTAssertTrue(invoked)
|
||||
}
|
||||
|
||||
func testRunsCriticalSectionImmediatelyForDisabledConsumer() throws {
|
||||
let orchestrator = CommonSwiftFrontendOrchestrator(
|
||||
mode: .consumer(commit: .unavailable),
|
||||
action: .compile,
|
||||
lockAccessor: prohibitedAccessor,
|
||||
maxLockTimeout: maxLocking
|
||||
)
|
||||
|
||||
var invoked = false
|
||||
try orchestrator.run {
|
||||
invoked = true
|
||||
}
|
||||
XCTAssertTrue(invoked)
|
||||
}
|
||||
|
||||
func testRunsEmitModuleLogicInExclusiveLock() throws {
|
||||
let lock = FakeExclusiveFileAccessor()
|
||||
let orchestrator = CommonSwiftFrontendOrchestrator(
|
||||
mode: .consumer(commit: .available(commit: "")),
|
||||
action: .emitModule,
|
||||
lockAccessor: lock,
|
||||
maxLockTimeout: maxLocking
|
||||
)
|
||||
|
||||
var invoked = false
|
||||
try orchestrator.run {
|
||||
XCTAssertTrue(lock.isLocked)
|
||||
invoked = true
|
||||
}
|
||||
XCTAssertTrue(invoked)
|
||||
}
|
||||
|
||||
func testCompilationInvokesCriticalSectionOnlyForNonEmptyLockFile() throws {
|
||||
let lock = FakeExclusiveFileAccessor(pattern: [.empty, .nonEmptyForRead(nonEmptyFile)])
|
||||
let orchestrator = CommonSwiftFrontendOrchestrator(
|
||||
mode: .consumer(commit: .available(commit: "")),
|
||||
action: .compile,
|
||||
lockAccessor: lock,
|
||||
maxLockTimeout: maxLocking
|
||||
)
|
||||
|
||||
var invoked = false
|
||||
try orchestrator.run {
|
||||
XCTAssertTrue(lock.isLocked)
|
||||
invoked = true
|
||||
}
|
||||
XCTAssertTrue(invoked)
|
||||
}
|
||||
|
||||
func testExecutesActionWithoutLockIfLockingFileIsEmptyForALongTime() throws {
|
||||
let lock = FakeExclusiveFileAccessor(pattern: [])
|
||||
let orchestrator = CommonSwiftFrontendOrchestrator(
|
||||
mode: .consumer(commit: .available(commit: "")),
|
||||
action: .compile,
|
||||
lockAccessor: lock,
|
||||
maxLockTimeout: 0
|
||||
)
|
||||
|
||||
var invoked = false
|
||||
try orchestrator.run {
|
||||
XCTAssertFalse(lock.isLocked)
|
||||
invoked = true
|
||||
}
|
||||
XCTAssertTrue(invoked)
|
||||
}
|
||||
|
||||
func testExecutesCriticalSectionAfterWriting() throws {
|
||||
let lock = FakeExclusiveFileAccessor(pattern: [.nonEmptyForWrite(emptyFile)])
|
||||
let orchestrator = CommonSwiftFrontendOrchestrator(
|
||||
mode: .consumer(commit: .available(commit: "")),
|
||||
action: .emitModule,
|
||||
lockAccessor: lock,
|
||||
maxLockTimeout: 0
|
||||
)
|
||||
|
||||
var invoked = false
|
||||
try orchestrator.run {
|
||||
XCTAssertEqual(fileManager.contents(atPath: emptyFile.path), "done".data(using: .utf8))
|
||||
invoked = true
|
||||
}
|
||||
XCTAssertTrue(invoked)
|
||||
}
|
||||
}
|
||||
@@ -25,20 +25,25 @@ class SwiftcContextTests: FileXCTestCase {
|
||||
private var config: XCRemoteCacheConfig!
|
||||
private var input: SwiftcArgInput!
|
||||
private var remoteCommitFile: URL!
|
||||
private var modulePathOutput: URL!
|
||||
private var fileMapUrl: URL!
|
||||
private var fileListUrl: URL!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
let workingDir = try prepareTempDir()
|
||||
remoteCommitFile = workingDir.appendingPathComponent("arc.rc")
|
||||
let modulePathOutput = workingDir.appendingPathComponent("mpo")
|
||||
modulePathOutput = workingDir.appendingPathComponent("mpo")
|
||||
fileMapUrl = workingDir.appendingPathComponent("filemap")
|
||||
fileListUrl = workingDir.appendingPathComponent("filelist")
|
||||
config = XCRemoteCacheConfig(remoteCommitFile: remoteCommitFile.path, sourceRoot: workingDir.path)
|
||||
input = SwiftcArgInput(
|
||||
objcHeaderOutput: "Target-Swift.h",
|
||||
moduleName: "",
|
||||
moduleName: "Module",
|
||||
modulePathOutput: modulePathOutput.path,
|
||||
filemap: "",
|
||||
filemap: fileMapUrl.path,
|
||||
target: "",
|
||||
fileList: ""
|
||||
fileList: fileListUrl.path
|
||||
)
|
||||
try fileManager.write(toPath: remoteCommitFile.path, contents: "123".data(using: .utf8))
|
||||
}
|
||||
@@ -77,4 +82,29 @@ class SwiftcContextTests: FileXCTestCase {
|
||||
|
||||
XCTAssertEqual(context.mode, .producer)
|
||||
}
|
||||
|
||||
func testStepsContainEmitingModuleAndAllCompilationScope() throws {
|
||||
let context = try SwiftcContext(config: config, input: input)
|
||||
|
||||
XCTAssertEqual(context.steps, .init(
|
||||
compileFilesScope: .all,
|
||||
emitModule: .init(
|
||||
objcHeaderOutput: "Target-Swift.h",
|
||||
modulePathOutput: modulePathOutput,
|
||||
dependencies: nil)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testReadsInputsFromFileMap() throws {
|
||||
let context = try SwiftcContext(config: config, input: input)
|
||||
|
||||
XCTAssertEqual(context.inputs, .fileMap(fileMapUrl.path))
|
||||
}
|
||||
|
||||
func testReadsCompilationFilesFromFileList() throws {
|
||||
let context = try SwiftcContext(config: config, input: input)
|
||||
|
||||
XCTAssertEqual(context.compilationFiles, .fileList(fileListUrl.path))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,19 +34,21 @@ class SwiftcFilemapInputEditorTests: FileXCTestCase {
|
||||
)
|
||||
private let sampleInfoContentData = #"{"":{"swift-dependencies":"/"}}"#.data(using: .utf8)!
|
||||
private var inputFile: URL!
|
||||
private var editor: SwiftcFilemapInputEditor!
|
||||
private var editorJson: SwiftcFilemapInputEditor!
|
||||
private var editorYaml: SwiftcFilemapInputEditor!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
try prepareTempDir()
|
||||
inputFile = workingDirectory!.appendingPathComponent("swift.json")
|
||||
editor = SwiftcFilemapInputEditor(inputFile, fileManager: fileManager)
|
||||
editorJson = SwiftcFilemapInputEditor(inputFile, fileFormat: .json, fileManager: fileManager)
|
||||
editorYaml = SwiftcFilemapInputEditor(inputFile, fileFormat: .yaml, fileManager: fileManager)
|
||||
}
|
||||
|
||||
func testReading() throws {
|
||||
try fileManager.spt_writeToFile(atPath: inputFile.path, contents: sampleInfoContentData)
|
||||
|
||||
let readInfo = try editor.read()
|
||||
let readInfo = try editorJson.read()
|
||||
|
||||
XCTAssertEqual(readInfo, sampleInfo)
|
||||
}
|
||||
@@ -80,13 +82,13 @@ class SwiftcFilemapInputEditorTests: FileXCTestCase {
|
||||
])
|
||||
try fileManager.spt_writeToFile(atPath: inputFile.path, contents: infoContentData)
|
||||
|
||||
let readInfo = try editor.read()
|
||||
let readInfo = try editorJson.read()
|
||||
|
||||
XCTAssertEqual(readInfo, expectedInfo)
|
||||
}
|
||||
|
||||
func testWritingSavesContent() throws {
|
||||
try editor.write(sampleInfo)
|
||||
try editorJson.write(sampleInfo)
|
||||
|
||||
let savedContent = try Data(contentsOf: inputFile)
|
||||
let content = try JSONSerialization.jsonObject(with: savedContent, options: []) as? [String: Any]
|
||||
@@ -108,7 +110,7 @@ class SwiftcFilemapInputEditorTests: FileXCTestCase {
|
||||
),
|
||||
])
|
||||
|
||||
try editor.write(extendedInfo)
|
||||
try editorJson.write(extendedInfo)
|
||||
|
||||
let savedContent = try Data(contentsOf: inputFile)
|
||||
let content = try JSONSerialization.jsonObject(with: savedContent, options: []) as? [String: Any]
|
||||
@@ -119,12 +121,50 @@ class SwiftcFilemapInputEditorTests: FileXCTestCase {
|
||||
func testModifyingFileCompilationInfo() throws {
|
||||
try fileManager.spt_writeToFile(atPath: inputFile.path, contents: sampleInfoContentData)
|
||||
|
||||
let originalInfo = try editor.read()
|
||||
let originalInfo = try editorJson.read()
|
||||
var modifiedInfo = originalInfo
|
||||
modifiedInfo.files = [file]
|
||||
try editor.write(modifiedInfo)
|
||||
let finalInfo = try editor.read()
|
||||
try editorJson.write(modifiedInfo)
|
||||
let finalInfo = try editorJson.read()
|
||||
|
||||
XCTAssertEqual(finalInfo, modifiedInfo)
|
||||
}
|
||||
|
||||
func testReadingSupplementaryInfoWithOptionalProperties() throws {
|
||||
let infoContentData = #"""
|
||||
"/file1.swift":
|
||||
swift-dependencies: "/file1.swiftdeps"
|
||||
dependencies: "/file1.d"
|
||||
"/file2.swift":
|
||||
dependencies: "/file2.d"
|
||||
object: "/file2.o"
|
||||
swift-dependencies: "/file2.swiftdeps"
|
||||
"""#.data(using: .utf8)!
|
||||
let expectedInfo = SwiftCompilationInfo(
|
||||
info: SwiftModuleCompilationInfo(
|
||||
dependencies: nil,
|
||||
swiftDependencies: nil
|
||||
),
|
||||
files: [
|
||||
SwiftFileCompilationInfo(
|
||||
file: "/file1.swift",
|
||||
dependencies: "/file1.d",
|
||||
object: nil,
|
||||
swiftDependencies: "/file1.swiftdeps"
|
||||
),
|
||||
SwiftFileCompilationInfo(
|
||||
file: "/file2.swift",
|
||||
dependencies: "/file2.d",
|
||||
object: "/file2.o",
|
||||
swiftDependencies: "/file2.swiftdeps"
|
||||
),
|
||||
])
|
||||
try fileManager.spt_writeToFile(atPath: inputFile.path, contents: infoContentData)
|
||||
|
||||
let readInfo = try editorYaml.read()
|
||||
|
||||
// `files` order doesn't match
|
||||
XCTAssertEqual(readInfo.info, expectedInfo.info)
|
||||
XCTAssertEqual(Set(readInfo.files), Set(expectedInfo.files))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,4 +231,45 @@ class SwiftcOrchestratorTests: XCTestCase {
|
||||
XCTAssertEqual(artifactBuilder.addedObjCHeaders, [:])
|
||||
}
|
||||
|
||||
|
||||
func testNotSetObjCHeaderIsNotCreated() throws {
|
||||
let swiftc = SwiftcMock(mockingResult: .success)
|
||||
let orchestrator = SwiftcOrchestrator(
|
||||
mode: .producer,
|
||||
swiftc: swiftc,
|
||||
swiftcCommand: "",
|
||||
objcHeaderOutput: nil,
|
||||
moduleOutput: moduleOutputURL,
|
||||
arch: "arch",
|
||||
artifactBuilder: artifactBuilder,
|
||||
producerFallbackCommandProcessors: [],
|
||||
invocationStorage: invocationStorage,
|
||||
shellOut: shellOutSpy
|
||||
)
|
||||
|
||||
try orchestrator.run()
|
||||
|
||||
XCTAssertEqual(artifactBuilder.addedObjCHeaders, [:])
|
||||
}
|
||||
|
||||
func testNotSetModuleOutputIsNotCreated() throws {
|
||||
let swiftc = SwiftcMock(mockingResult: .success)
|
||||
let orchestrator = SwiftcOrchestrator(
|
||||
mode: .producer,
|
||||
swiftc: swiftc,
|
||||
swiftcCommand: "",
|
||||
objcHeaderOutput: objcHeaderURL,
|
||||
moduleOutput: nil,
|
||||
arch: "arch",
|
||||
artifactBuilder: artifactBuilder,
|
||||
producerFallbackCommandProcessors: [],
|
||||
invocationStorage: invocationStorage,
|
||||
shellOut: shellOutSpy
|
||||
)
|
||||
|
||||
try orchestrator.run()
|
||||
|
||||
XCTAssertEqual(artifactBuilder.addedModuleDefinitions, [:])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ class SwiftcTests: FileXCTestCase {
|
||||
private var workingDir: URL!
|
||||
private var remoteCommitLocation: URL!
|
||||
private let sampleRemoteCommit = "bdb321"
|
||||
private var allowedInputDeterminer = FilenameBasedAllowedInputDeterminer([])
|
||||
|
||||
|
||||
override func setUpWithError() throws {
|
||||
@@ -62,7 +63,7 @@ class SwiftcTests: FileXCTestCase {
|
||||
|
||||
input = SwiftcArgInput(
|
||||
objcHeaderOutput: "Target-Swift.h",
|
||||
moduleName: "",
|
||||
moduleName: "Target",
|
||||
modulePathOutput: modulePathOutput.path,
|
||||
filemap: "",
|
||||
target: "",
|
||||
@@ -93,7 +94,8 @@ class SwiftcTests: FileXCTestCase {
|
||||
fileManager: FileManager.default,
|
||||
dependenciesWriterFactory: dependenciesWriterFactory,
|
||||
touchFactory: touchFactory,
|
||||
plugins: []
|
||||
plugins: [],
|
||||
allowedInputDeterminer: allowedInputDeterminer
|
||||
)
|
||||
|
||||
let compilation = try swiftc.mockCompilation()
|
||||
@@ -115,7 +117,8 @@ class SwiftcTests: FileXCTestCase {
|
||||
fileManager: FileManager.default,
|
||||
dependenciesWriterFactory: dependenciesWriterFactory,
|
||||
touchFactory: touchFactory,
|
||||
plugins: []
|
||||
plugins: [],
|
||||
allowedInputDeterminer: allowedInputDeterminer
|
||||
)
|
||||
|
||||
let compilation = try swiftc.mockCompilation()
|
||||
@@ -137,7 +140,8 @@ class SwiftcTests: FileXCTestCase {
|
||||
fileManager: FileManager.default,
|
||||
dependenciesWriterFactory: dependenciesWriterFactory,
|
||||
touchFactory: touchFactory,
|
||||
plugins: []
|
||||
plugins: [],
|
||||
allowedInputDeterminer: allowedInputDeterminer
|
||||
)
|
||||
|
||||
_ = try swiftc.mockCompilation()
|
||||
@@ -161,7 +165,8 @@ class SwiftcTests: FileXCTestCase {
|
||||
fileManager: FileManager.default,
|
||||
dependenciesWriterFactory: dependenciesWriterFactory,
|
||||
touchFactory: touchFactory,
|
||||
plugins: []
|
||||
plugins: [],
|
||||
allowedInputDeterminer: allowedInputDeterminer
|
||||
)
|
||||
|
||||
_ = try swiftc.mockCompilation()
|
||||
@@ -187,7 +192,8 @@ class SwiftcTests: FileXCTestCase {
|
||||
fileManager: FileManager.default,
|
||||
dependenciesWriterFactory: dependenciesWriterFactory,
|
||||
touchFactory: touchFactory,
|
||||
plugins: []
|
||||
plugins: [],
|
||||
allowedInputDeterminer: allowedInputDeterminer
|
||||
)
|
||||
|
||||
_ = try swiftc.mockCompilation()
|
||||
@@ -216,7 +222,8 @@ class SwiftcTests: FileXCTestCase {
|
||||
fileManager: FileManager.default,
|
||||
dependenciesWriterFactory: dependenciesWriterFactory,
|
||||
touchFactory: touchFactory,
|
||||
plugins: []
|
||||
plugins: [],
|
||||
allowedInputDeterminer: allowedInputDeterminer
|
||||
)
|
||||
|
||||
_ = try swiftc.mockCompilation()
|
||||
@@ -226,6 +233,30 @@ class SwiftcTests: FileXCTestCase {
|
||||
XCTAssertEqual(writerSpy.wroteSkipForSha, sampleRemoteCommit)
|
||||
}
|
||||
|
||||
func testAllowsNotAllowedInputFileThatAreAllowedByDeterminer() throws {
|
||||
inputFileListReader = ListReaderFake(files: [URL(fileURLWithPath: "specialFile.swift")])
|
||||
allowedInputDeterminer = FilenameBasedAllowedInputDeterminer(["specialFile.swift"])
|
||||
let swiftc = Swiftc(
|
||||
inputFileListReader: inputFileListReader,
|
||||
markerReader: markerReader,
|
||||
allowedFilesListScanner: allowedFilesListScanner,
|
||||
artifactOrganizer: artifactOrganizer,
|
||||
inputReader: swiftcInputReader,
|
||||
context: context,
|
||||
markerWriter: markerWriter,
|
||||
productsGenerator: productsGenerator,
|
||||
fileManager: FileManager.default,
|
||||
dependenciesWriterFactory: dependenciesWriterFactory,
|
||||
touchFactory: touchFactory,
|
||||
plugins: [],
|
||||
allowedInputDeterminer: allowedInputDeterminer
|
||||
)
|
||||
|
||||
let result = try swiftc.mockCompilation()
|
||||
|
||||
XCTAssertEqual(result, .success)
|
||||
}
|
||||
|
||||
func testRCTouchesOutputFile() throws {
|
||||
let compilationURL = URL(fileURLWithPath: "old.swift")
|
||||
inputFileListReader = ListReaderFake(files: [compilationURL])
|
||||
@@ -265,7 +296,8 @@ class SwiftcTests: FileXCTestCase {
|
||||
fileManager: FileManager.default,
|
||||
dependenciesWriterFactory: dependenciesWriterFactory,
|
||||
touchFactory: touchFactory,
|
||||
plugins: []
|
||||
plugins: [],
|
||||
allowedInputDeterminer: allowedInputDeterminer
|
||||
)
|
||||
|
||||
_ = try swiftc.mockCompilation()
|
||||
@@ -276,7 +308,7 @@ class SwiftcTests: FileXCTestCase {
|
||||
|
||||
func testCompilationUsesArchSpecificSwiftmoduleFiles() throws {
|
||||
let artifactRoot = URL(fileURLWithPath: "/cachedArtifact")
|
||||
let artifactObjCHeader = URL(fileURLWithPath: "/cachedArtifact/include/archTest/Target-Swift.h")
|
||||
let artifactObjCHeader = URL(fileURLWithPath: "/cachedArtifact/include/archTest/Target/Target-Swift.h")
|
||||
let artifactSwiftmodule = URL(fileURLWithPath: "/cachedArtifact/swiftmodule/archTest/Target.swiftmodule")
|
||||
let artifactSwiftdoc = URL(fileURLWithPath: "/cachedArtifact/swiftmodule/archTest/Target.swiftdoc")
|
||||
let artifactSwiftSourceInfo = URL(
|
||||
@@ -305,7 +337,8 @@ class SwiftcTests: FileXCTestCase {
|
||||
fileManager: FileManager.default,
|
||||
dependenciesWriterFactory: dependenciesWriterFactory,
|
||||
touchFactory: touchFactory,
|
||||
plugins: []
|
||||
plugins: [],
|
||||
allowedInputDeterminer: allowedInputDeterminer
|
||||
)
|
||||
|
||||
_ = try swiftc.mockCompilation()
|
||||
@@ -340,7 +373,8 @@ class SwiftcTests: FileXCTestCase {
|
||||
fileManager: FileManager.default,
|
||||
dependenciesWriterFactory: dependenciesWriterFactory,
|
||||
touchFactory: touchFactory,
|
||||
plugins: [plugin]
|
||||
plugins: [plugin],
|
||||
allowedInputDeterminer: allowedInputDeterminer
|
||||
)
|
||||
|
||||
_ = try swiftc.mockCompilation()
|
||||
@@ -382,7 +416,8 @@ class SwiftcTests: FileXCTestCase {
|
||||
fileManager: FileManager.default,
|
||||
dependenciesWriterFactory: FileDependenciesWriter.init,
|
||||
touchFactory: touchFactory,
|
||||
plugins: []
|
||||
plugins: [],
|
||||
allowedInputDeterminer: allowedInputDeterminer
|
||||
)
|
||||
|
||||
_ = try swiftc.mockCompilation()
|
||||
@@ -399,6 +434,57 @@ class SwiftcTests: FileXCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
func testGeneratesDFilesAndIndividualFilesWithAdditionallyAllowedFiles() throws {
|
||||
let outputFilesDir = workingDir.appendingPathComponent("outputFiles")
|
||||
try fileManager.spt_createEmptyDir(outputFilesDir)
|
||||
let moduleDFile = outputFilesDir.appendingPathComponent("master.d")
|
||||
let fileDFile = outputFilesDir.appendingPathComponent("magicalFile.d")
|
||||
let input = SwiftCompilationInfo(
|
||||
info: SwiftModuleCompilationInfo(
|
||||
dependencies: moduleDFile,
|
||||
swiftDependencies: outputFilesDir.appendingPathComponent("master.swiftdeps")
|
||||
),
|
||||
files: [
|
||||
SwiftFileCompilationInfo(
|
||||
file: "/magicalFile.swift",
|
||||
dependencies: fileDFile,
|
||||
object: outputFilesDir.appendingPathComponent("maficalFile.o"),
|
||||
swiftDependencies: nil
|
||||
),
|
||||
]
|
||||
)
|
||||
inputFileListReader = ListReaderFake(files: ["/magicalFile.swift"])
|
||||
swiftcInputReader = SwiftcInputReaderStub(info: input)
|
||||
markerReader = ListReaderFake(files: [])
|
||||
allowedInputDeterminer = FilenameBasedAllowedInputDeterminer(["magicalFile.swift"])
|
||||
let swiftc = Swiftc(
|
||||
inputFileListReader: inputFileListReader,
|
||||
markerReader: markerReader,
|
||||
allowedFilesListScanner: allowedFilesListScanner,
|
||||
artifactOrganizer: artifactOrganizer,
|
||||
inputReader: swiftcInputReader,
|
||||
context: context,
|
||||
markerWriter: markerWriter,
|
||||
productsGenerator: productsGenerator,
|
||||
fileManager: FileManager.default,
|
||||
dependenciesWriterFactory: FileDependenciesWriter.init,
|
||||
touchFactory: touchFactory,
|
||||
plugins: [],
|
||||
allowedInputDeterminer: allowedInputDeterminer
|
||||
)
|
||||
|
||||
_ = try swiftc.mockCompilation()
|
||||
|
||||
XCTAssertEqual(
|
||||
try FileDependenciesReader(moduleDFile, accessor: .default).readFilesAndDependencies(),
|
||||
["dependencies": ["/magicalFile.swift"]]
|
||||
)
|
||||
XCTAssertEqual(
|
||||
try FileDependenciesReader(fileDFile, accessor: .default).readFilesAndDependencies(),
|
||||
["dependencies": ["/magicalFile.swift"]]
|
||||
)
|
||||
}
|
||||
|
||||
func testSkipsGeneratingDFilesWhenNotProvidedInCompilationInfo() throws {
|
||||
let outputFilesDir = workingDir.appendingPathComponent("outputFiles")
|
||||
try fileManager.spt_createEmptyDir(outputFilesDir)
|
||||
@@ -429,7 +515,8 @@ class SwiftcTests: FileXCTestCase {
|
||||
fileManager: FileManager.default,
|
||||
dependenciesWriterFactory: FileDependenciesWriter.init,
|
||||
touchFactory: touchFactory,
|
||||
plugins: []
|
||||
plugins: [],
|
||||
allowedInputDeterminer: allowedInputDeterminer
|
||||
)
|
||||
|
||||
XCTAssertNoThrow(try swiftc.mockCompilation())
|
||||
@@ -465,7 +552,8 @@ class SwiftcTests: FileXCTestCase {
|
||||
fileManager: FileManager.default,
|
||||
dependenciesWriterFactory: FileDependenciesWriter.init,
|
||||
touchFactory: touchFactory,
|
||||
plugins: []
|
||||
plugins: [],
|
||||
allowedInputDeterminer: allowedInputDeterminer
|
||||
)
|
||||
|
||||
XCTAssertNoThrow(try swiftc.mockCompilation())
|
||||
|
||||
@@ -333,7 +333,7 @@ class TemplateBasedCCWrapperBuilderTests: FileXCTestCase {
|
||||
XCTAssertNotEqual(newFileOutputData, Data())
|
||||
}
|
||||
|
||||
func testPCHCompilationFallbacks() throws {
|
||||
func testPCHObjCCompilationFallbacks() throws {
|
||||
// Marker is empty to mimic the new file scenario
|
||||
let pchFile = directory.appendingPathComponent("input.pch")
|
||||
createValidPCHFile(pchFile)
|
||||
@@ -344,6 +344,17 @@ class TemplateBasedCCWrapperBuilderTests: FileXCTestCase {
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// 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.
|
||||
|
||||
@testable import XCRemoteCache
|
||||
|
||||
import XCTest
|
||||
|
||||
class AssetsFileDependenciesReaderTests: FileXCTestCase {
|
||||
private static let resourcesSubdirectory = "TestData/Dependencies/AssetsFileDependenciesReaderTests"
|
||||
private let dirAccessorFake = DirAccessorFake()
|
||||
|
||||
private func pathForTestData(name: String) throws -> URL {
|
||||
return try XCTUnwrap(Bundle.module.url(
|
||||
forResource: name,
|
||||
withExtension: "",
|
||||
subdirectory: AssetsFileDependenciesReaderTests.resourcesSubdirectory
|
||||
))
|
||||
}
|
||||
|
||||
func testParsingSampleFile() throws {
|
||||
let file = try pathForTestData(name: "assetcatalog_dependencies_sample")
|
||||
let fileData = try Data(contentsOf: file)
|
||||
let xcassetsPath: URL = "/StandaloneApp/Assets.xcassets"
|
||||
let contentsJson = xcassetsPath.appendingPathComponent("Contents.json")
|
||||
|
||||
try dirAccessorFake.write(toPath: file.path, contents: fileData)
|
||||
try dirAccessorFake.write(toPath: contentsJson.path, contents: Data())
|
||||
|
||||
let reader = AssetsFileDependenciesReader(file, dirAccessor: dirAccessorFake)
|
||||
|
||||
let dependencies = try reader.findDependencies()
|
||||
|
||||
XCTAssertEqual(dependencies, [contentsJson.path])
|
||||
}
|
||||
|
||||
func testThrowsWhenFileIsMissing() throws {
|
||||
let file: URL = "/nonExistingFile"
|
||||
|
||||
let reader = AssetsFileDependenciesReader(file, dirAccessor: dirAccessorFake)
|
||||
|
||||
XCTAssertThrowsError(try reader.findDependencies()) { error in
|
||||
guard case DependenciesReaderError.readingError = error else {
|
||||
XCTFail("Invalid error \(error)")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testReturnsEmptyArrayIsFileIsMalformed() throws {
|
||||
let file: URL = "/nonExistingFile"
|
||||
try dirAccessorFake.write(toPath: file.path, contents: Data())
|
||||
let reader = AssetsFileDependenciesReader(file, dirAccessor: dirAccessorFake)
|
||||
|
||||
let dependencies = try reader.findDependencies()
|
||||
|
||||
XCTAssertEqual(dependencies, [])
|
||||
}
|
||||
}
|
||||
@@ -20,8 +20,14 @@
|
||||
@testable import XCRemoteCache
|
||||
import XCTest
|
||||
|
||||
class PhaseCacheModeControllerTests: XCTestCase {
|
||||
class PhaseCacheModeControllerTests: FileXCTestCase {
|
||||
private var rootDir: URL!
|
||||
private let sampleURL = URL(fileURLWithPath: "")
|
||||
private var simpleController: PhaseCacheModeController!
|
||||
|
||||
override func setUp() async throws {
|
||||
rootDir = try prepareTempDir()
|
||||
}
|
||||
|
||||
func testDisablesForSpecifiedSha() {
|
||||
let dependenciesReader = DependenciesReaderFake(dependencies: ["skipForSha": ["dbd123"]])
|
||||
@@ -34,6 +40,7 @@ class PhaseCacheModeControllerTests: XCTestCase {
|
||||
dependenciesWriter: FileDependenciesWriter.init,
|
||||
dependenciesReader: { _, _ in dependenciesReader },
|
||||
markerWriter: FileMarkerWriter.init,
|
||||
llbuildLockFile: "/file",
|
||||
fileManager: FileManager.default
|
||||
)
|
||||
|
||||
@@ -51,6 +58,7 @@ class PhaseCacheModeControllerTests: XCTestCase {
|
||||
dependenciesWriter: FileDependenciesWriter.init,
|
||||
dependenciesReader: { _, _ in dependenciesReader },
|
||||
markerWriter: FileMarkerWriter.init,
|
||||
llbuildLockFile: "/tmp/lock",
|
||||
fileManager: FileManager.default
|
||||
)
|
||||
|
||||
@@ -68,6 +76,7 @@ class PhaseCacheModeControllerTests: XCTestCase {
|
||||
dependenciesWriter: FileDependenciesWriter.init,
|
||||
dependenciesReader: { _, _ in dependenciesReader },
|
||||
markerWriter: FileMarkerWriter.init,
|
||||
llbuildLockFile: "/tmp/lock",
|
||||
fileManager: FileManager.default
|
||||
)
|
||||
|
||||
@@ -85,6 +94,7 @@ class PhaseCacheModeControllerTests: XCTestCase {
|
||||
dependenciesWriter: { _, _ in dependenciesWriter },
|
||||
dependenciesReader: { _, _ in DependenciesReaderFake(dependencies: [:]) },
|
||||
markerWriter: { _, _ in MarkerWriterSpy() },
|
||||
llbuildLockFile: "/tmp/lock",
|
||||
fileManager: FileManager.default
|
||||
)
|
||||
|
||||
@@ -105,6 +115,7 @@ class PhaseCacheModeControllerTests: XCTestCase {
|
||||
dependenciesWriter: { _, _ in dependenciesWriter },
|
||||
dependenciesReader: { _, _ in DependenciesReaderFake(dependencies: [:]) },
|
||||
markerWriter: { _, _ in MarkerWriterSpy() },
|
||||
llbuildLockFile: "/tmp/lock",
|
||||
fileManager: FileManager.default
|
||||
)
|
||||
|
||||
@@ -125,6 +136,7 @@ class PhaseCacheModeControllerTests: XCTestCase {
|
||||
dependenciesWriter: { _, _ in DependenciesWriterSpy() },
|
||||
dependenciesReader: { _, _ in DependenciesReaderFake(dependencies: [:]) },
|
||||
markerWriter: { _, _ in markerWriter },
|
||||
llbuildLockFile: "/tmp/lock",
|
||||
fileManager: FileManager.default
|
||||
)
|
||||
|
||||
@@ -142,6 +154,7 @@ class PhaseCacheModeControllerTests: XCTestCase {
|
||||
dependenciesWriter: { _, _ in DependenciesWriterSpy() },
|
||||
dependenciesReader: { _, _ in DependenciesReaderFake(dependencies: [:]) },
|
||||
markerWriter: { _, _ in MarkerWriterSpy() },
|
||||
llbuildLockFile: "/tmp/lock",
|
||||
fileManager: FileManager.default
|
||||
)
|
||||
|
||||
@@ -163,6 +176,7 @@ class PhaseCacheModeControllerTests: XCTestCase {
|
||||
dependenciesWriter: { _, _ in dependenciesWriter },
|
||||
dependenciesReader: { _, _ in DependenciesReaderFake(dependencies: [:]) },
|
||||
markerWriter: { _, _ in markerWriterSpy },
|
||||
llbuildLockFile: "/tmp/lock",
|
||||
fileManager: FileManager.default
|
||||
)
|
||||
|
||||
@@ -174,4 +188,48 @@ class PhaseCacheModeControllerTests: XCTestCase {
|
||||
}
|
||||
XCTAssertEqual(Set(deps), expectedMarkerFiles)
|
||||
}
|
||||
|
||||
func testDeletesLockOnEnable() throws {
|
||||
let lockURL = rootDir.appendingPathComponent("1.lock")
|
||||
try fileManager.spt_createEmptyFile(lockURL)
|
||||
let dependenciesReader = DependenciesReaderFake(dependencies: [:])
|
||||
simpleController = PhaseCacheModeController(
|
||||
tempDir: sampleURL,
|
||||
mergeCommitFile: sampleURL,
|
||||
phaseDependencyPath: "",
|
||||
markerPath: "",
|
||||
forceCached: false,
|
||||
dependenciesWriter: { _, _ in DependenciesWriterSpy() },
|
||||
dependenciesReader: { _, _ in dependenciesReader },
|
||||
markerWriter: { _, _ in MarkerWriterSpy() },
|
||||
llbuildLockFile: lockURL,
|
||||
fileManager: FileManager.default
|
||||
)
|
||||
|
||||
try simpleController.enable(allowedInputFiles: [], dependencies: [])
|
||||
|
||||
XCTAssertFalse(fileManager.fileExists(atPath: lockURL.path))
|
||||
}
|
||||
|
||||
func testDeletesLockOnDisable() throws {
|
||||
let lockURL = rootDir.appendingPathComponent("1.lock")
|
||||
try fileManager.spt_createEmptyFile(lockURL)
|
||||
let dependenciesReader = DependenciesReaderFake(dependencies: [:])
|
||||
simpleController = PhaseCacheModeController(
|
||||
tempDir: sampleURL,
|
||||
mergeCommitFile: sampleURL,
|
||||
phaseDependencyPath: "",
|
||||
markerPath: "",
|
||||
forceCached: false,
|
||||
dependenciesWriter: { _, _ in DependenciesWriterSpy() },
|
||||
dependenciesReader: { _, _ in dependenciesReader },
|
||||
markerWriter: { _, _ in MarkerWriterSpy() },
|
||||
llbuildLockFile: lockURL,
|
||||
fileManager: FileManager.default
|
||||
)
|
||||
|
||||
try simpleController.disable()
|
||||
|
||||
XCTAssertFalse(fileManager.fileExists(atPath: lockURL.path))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// 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.
|
||||
|
||||
@testable import XCRemoteCache
|
||||
import XCTest
|
||||
|
||||
class StaticFileListReaderTests: XCTestCase {
|
||||
func testCanAlwaysRead() throws {
|
||||
let reader = StaticFileListReader(list: [])
|
||||
|
||||
XCTAssertTrue(reader.canRead())
|
||||
}
|
||||
|
||||
func testListsPassedUrls() throws {
|
||||
let url: URL = "/file"
|
||||
let reader = StaticFileListReader(list: [url])
|
||||
|
||||
XCTAssertEqual(try reader.listFilesURLs(), [url])
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import XCTest
|
||||
class TargetDependenciesReaderTests: XCTestCase {
|
||||
|
||||
private let workingURL: URL = "/test"
|
||||
private let assetsWorkingURL: URL = "/assetsTest"
|
||||
private var dirAccessor: DirAccessorFake!
|
||||
private var reader: TargetDependenciesReader!
|
||||
|
||||
@@ -35,9 +36,15 @@ class TargetDependenciesReaderTests: XCTestCase {
|
||||
let fakeDependency = url.deletingPathExtension().appendingPathExtension("swift")
|
||||
return DependenciesReaderFake(dependencies: ["": [fakeDependency.path]])
|
||||
}
|
||||
let assetsFakeDependencyReaderFactory: (URL) -> DependenciesReader = { url in
|
||||
let fakeDependency = url.deletingLastPathComponent().appendingPathComponent("Contents.json")
|
||||
return DependenciesReaderFake(dependencies: ["": [fakeDependency.path]])
|
||||
}
|
||||
reader = TargetDependenciesReader(
|
||||
workingURL,
|
||||
fileDependeciesReaderFactory: swiftFakeDependencyReaderFactory,
|
||||
compilationOutputDir: workingURL,
|
||||
assetsCatalogOutputDir: assetsWorkingURL,
|
||||
fileDependenciesReaderFactory: swiftFakeDependencyReaderFactory,
|
||||
assetsDependenciesReaderFactory: assetsFakeDependencyReaderFactory,
|
||||
dirScanner: dirAccessor
|
||||
)
|
||||
}
|
||||
@@ -70,4 +77,13 @@ class TargetDependenciesReaderTests: XCTestCase {
|
||||
|
||||
XCTAssertEqual(deps, ["/test/some-master.swift"])
|
||||
}
|
||||
|
||||
func testFindsAssetsCatalogDependencies() throws {
|
||||
let assetsContentFile: URL = "/assetsTest/assetcatalog_dependencies"
|
||||
try dirAccessor.write(toPath: assetsContentFile.path, contents: Data())
|
||||
|
||||
let deps = try reader.findDependencies()
|
||||
|
||||
XCTAssertEqual(deps, ["/assetsTest/Contents.json"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,27 @@ class CanonicalRequestTest: XCTestCase {
|
||||
}
|
||||
|
||||
func testCanonicalRequest() {
|
||||
request.url = URL(
|
||||
string: "https://region.amazonaws.com/bucket/with%20space?param=value&hej=hej&abd=cde&test=-_.~"
|
||||
)!
|
||||
let canonicalRequest = CanonicalRequest(
|
||||
request: request
|
||||
)
|
||||
XCTAssertEqual(
|
||||
canonicalRequest.value,
|
||||
"GET\n" +
|
||||
"/bucket/with%20space\n" +
|
||||
"abd=cde&hej=hej¶m=value&test=-_.~\n" +
|
||||
"a-header2key:A-Header2Value\n" +
|
||||
"b-header3key:B-Header3Value\n" +
|
||||
"c-header4key:C Header 4 Value\n" +
|
||||
"x-header1key:X-Header1Value\n\n" +
|
||||
"a-header2key;b-header3key;c-header4key;x-header1key\n" +
|
||||
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
)
|
||||
}
|
||||
|
||||
func testCanonicalRequestWithEmptySpaceInPath() {
|
||||
let canonicalRequest = CanonicalRequest(
|
||||
request: request
|
||||
)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// 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
|
||||
@testable import XCRemoteCache
|
||||
|
||||
/// A Processor fake that deletes the artifact
|
||||
class DestroyerArtifactProcessor: ArtifactProcessor {
|
||||
private let dirAccesor: DirAccessor
|
||||
|
||||
init(_ dirAccesor: DirAccessor) {
|
||||
self.dirAccesor = dirAccesor
|
||||
}
|
||||
func process(rawArtifact url: URL) throws {
|
||||
try dirAccesor.removeItem(atPath: url.path)
|
||||
}
|
||||
func process(localArtifact url: URL) throws {
|
||||
try dirAccesor.removeItem(atPath: url.path)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// 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
|
||||
@testable import XCRemoteCache
|
||||
|
||||
// FileAcccessor that fails if one wants to acquire a lock
|
||||
class DisallowedExclusiveFileAccessor: ExclusiveFileAccessor {
|
||||
func exclusiveAccess<T>(block: (FileHandle) throws -> (T)) throws -> T {
|
||||
throw "Invoked ProhibitedExclusiveFileAccessor"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// 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
|
||||
@testable import XCRemoteCache
|
||||
|
||||
// Thread-unsafe, in-memory lock
|
||||
class FakeExclusiveFileAccessor: ExclusiveFileAccessor {
|
||||
private(set) var isLocked = false
|
||||
private var pattern: [LockFileContent]
|
||||
|
||||
enum LockFileContent {
|
||||
case empty
|
||||
case nonEmptyForRead(URL)
|
||||
case nonEmptyForWrite(URL)
|
||||
|
||||
func fileHandle() throws -> FileHandle {
|
||||
switch self {
|
||||
case .empty: return FileHandle.nullDevice
|
||||
case .nonEmptyForRead(let url): return try FileHandle(forReadingFrom: url)
|
||||
case .nonEmptyForWrite(let url): return try FileHandle(forWritingTo: url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(pattern: [LockFileContent] = []) {
|
||||
// keep in the reversed order to always pop
|
||||
self.pattern = pattern.reversed()
|
||||
}
|
||||
|
||||
func exclusiveAccess<T>(block: (FileHandle) throws -> (T)) throws -> T {
|
||||
if isLocked {
|
||||
throw "FakeExclusiveFileAccessor lock is already locked"
|
||||
}
|
||||
defer {
|
||||
isLocked = false
|
||||
}
|
||||
isLocked = true
|
||||
let fileHandle = try (pattern.popLast() ?? .empty).fileHandle()
|
||||
return try block(fileHandle)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -34,7 +34,10 @@ class GitClientFake: GitClient {
|
||||
}
|
||||
|
||||
func getCommonPrimarySha() throws -> String {
|
||||
shaHistory[primaryBranchIndex].sha
|
||||
guard shaHistory.count > primaryBranchIndex else {
|
||||
throw GitClientError.noCommonShaWithPrimaryRepo(remoteName: "testing", error: "SampleError")
|
||||
}
|
||||
return shaHistory[primaryBranchIndex].sha
|
||||
}
|
||||
|
||||
func getShaDate(sha: String) throws -> Date {
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
// 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.
|
||||
|
||||
@testable import xclibtoolSupport
|
||||
import XCTest
|
||||
|
||||
class XCLibtoolHelperTests: XCTestCase {
|
||||
func testStaticFrameworkUniversalBinary() throws {
|
||||
let mode = try XCLibtoolHelper.buildMode(
|
||||
args: ["-o", "/universal/static", "/arch1/static", "/arch2/static"]
|
||||
)
|
||||
|
||||
XCTAssertEqual(mode, .createUniversalBinary(
|
||||
output: "/universal/static",
|
||||
inputs: ["/arch1/static", "/arch2/static"]
|
||||
))
|
||||
}
|
||||
|
||||
func testStaticLibraryUniversalBinary() throws {
|
||||
let mode = try XCLibtoolHelper.buildMode(
|
||||
args: ["-o", "/universal/static.a", "/arch1/static.a", "/arch2/static.a"]
|
||||
)
|
||||
|
||||
XCTAssertEqual(mode, .createUniversalBinary(
|
||||
output: "/universal/static.a",
|
||||
inputs: ["/arch1/static.a", "/arch2/static.a"]
|
||||
))
|
||||
}
|
||||
|
||||
func testUnknownExtensionInputThrowsUnsupportedMode() throws {
|
||||
XCTAssertThrowsError(
|
||||
try XCLibtoolHelper.buildMode(args: ["-o", "/universal/static.a", "/arch1/static.unknown"])) { error in
|
||||
switch error {
|
||||
case XCLibtoolHelperError.unsupportedMode: break
|
||||
default:
|
||||
XCTFail("Not expected error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testMissingOutputThrowsMissingOutput() throws {
|
||||
XCTAssertThrowsError(try XCLibtoolHelper.buildMode(args: ["/arch1/static"])) { error in
|
||||
switch error {
|
||||
case XCLibtoolHelperError.missingOutput: break
|
||||
default:
|
||||
XCTFail("Not expected error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testExplicitStaticFrameworkUniversalBinary() throws {
|
||||
let mode = try XCLibtoolHelper.buildMode(
|
||||
args: ["-static", "-o", "/universal/static", "/arch1/static", "/arch2/static"]
|
||||
)
|
||||
|
||||
XCTAssertEqual(mode, .createUniversalBinary(
|
||||
output: "/universal/static",
|
||||
inputs: ["/arch1/static", "/arch2/static"]
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ plugin 'cocoapods-xcremotecache'
|
||||
2. Configure XCRemoteCache at the top of your `Podfile` definition:
|
||||
```ruby
|
||||
xcremotecache({
|
||||
'cache_addresses' => ['http://localhost:8080/cache/pods'],
|
||||
'cache_addresses' => ['http://localhost:8080/cache/pods'],
|
||||
'primary_repo' => 'https://your.primary.repo.git',
|
||||
'mode' => 'consumer'
|
||||
})
|
||||
@@ -48,13 +48,14 @@ An object that is passed to the `xcremotecache` can contain all properties suppo
|
||||
| `exclude_build_configurations` | Comma-separated list of configurations that shouldn't use XCRemoteCache | `[]`| ⬜️ |
|
||||
| `final_target` | A target name that is build at the end of the build chain. Relevant only for a 'producer' mode to mark a given sha as ready to use from cache | `Debug` | ⬜️ |
|
||||
| `check_build_configuration` | A build configuration for which the remote cache availability is performed. Relevant only for a 'consumer' mode | `Debug` | ⬜️ |
|
||||
| `check_platform` | A platform for which the remote cache availability is performed. Relevant only for a 'consumer' mode | `iphonesimulator` | ⬜️
|
||||
| `check_platform` | A platform for which the remote cache availability is performed. Relevant only for a 'consumer' mode | `iphonesimulator` | ⬜️
|
||||
| `modify_lldb_init` | Controls if the pod integration should modify `~/.lldbinit` | `true` | ⬜️ |
|
||||
| `xccc_file` | The path where should be placed the `xccc` binary (in the pod installation phase) | `{podfile_dir}/.rc/xccc` | ⬜️ |
|
||||
| `remote_commit_file` | The path of the file with the remote commit sha (in the pod installation phase) | `{podfile_dir}/.rc/arc.rc`| ⬜️ |
|
||||
| `prettify_meta_files` | A Boolean value that opts-in pretty JSON formatting for meta files | `false` | ⬜️ |
|
||||
| `fake_src_root` | An arbitrary source location shared between producers and consumers. Should be unique for a project. | `/xxxxxxxxxx` | ⬜️ |
|
||||
| `disable_certificate_verification` | A Boolean value that opts-in SSL certificate validation is disabled | `false` | ⬜️ |
|
||||
| `exclude_sdks_configurations` | array of sdks to not integrate XCRemoteCache (e.g. "watchos*, watchsimulator*") (Experimental) | `[]`| ⬜️ |
|
||||
|
||||
## Uninstalling
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
# Licensed 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.
|
||||
@@ -29,42 +29,44 @@ module CocoapodsXCRemoteCacheModifier
|
||||
FAT_ARCHIVE_NAME_INFIX = 'arm64-x86_64'
|
||||
XCRC_COOCAPODS_ROOT_KEY = 'XCRC_COOCAPODS_ROOT'
|
||||
|
||||
# List of plugins' user properties that should not be copied to .rcinfo
|
||||
# List of plugins' user properties that should not be copied to .rcinfo
|
||||
CUSTOM_CONFIGURATION_KEYS = [
|
||||
'enabled',
|
||||
'enabled',
|
||||
'xcrc_location',
|
||||
'exclude_targets',
|
||||
'exclude_build_configurations',
|
||||
'final_target',
|
||||
'check_build_configuration',
|
||||
'check_platform',
|
||||
'check_build_configuration',
|
||||
'check_platform',
|
||||
'modify_lldb_init',
|
||||
'fake_src_root',
|
||||
'exclude_sdks_configurations'
|
||||
]
|
||||
|
||||
class XCRemoteCache
|
||||
@@configuration = nil
|
||||
|
||||
def self.configure(c)
|
||||
def self.configure(c)
|
||||
@@configuration = c
|
||||
end
|
||||
|
||||
def self.set_configuration_default_values
|
||||
default_values = {
|
||||
'mode' => 'consumer',
|
||||
'enabled' => true,
|
||||
'enabled' => true,
|
||||
'xcrc_location' => "XCRC",
|
||||
'exclude_build_configurations' => [],
|
||||
'check_build_configuration' => 'Debug',
|
||||
'check_platform' => 'iphonesimulator',
|
||||
'modify_lldb_init' => true,
|
||||
'check_platform' => 'iphonesimulator',
|
||||
'modify_lldb_init' => true,
|
||||
'xccc_file' => "#{BIN_DIR}/xccc",
|
||||
'remote_commit_file' => "#{BIN_DIR}/arc.rc",
|
||||
'exclude_targets' => [],
|
||||
'prettify_meta_files' => false,
|
||||
'fake_src_root' => "/#{'x' * 10 }",
|
||||
'disable_certificate_verification' => false,
|
||||
'custom_rewrite_envs' => []
|
||||
'custom_rewrite_envs' => [],
|
||||
'exclude_sdks_configurations' => []
|
||||
}
|
||||
@@configuration.merge! default_values.select { |k, v| !@@configuration.key?(k) }
|
||||
# Always include XCRC_COOCAPODS_ROOT_KEY in custom_rewrite_envs
|
||||
@@ -75,12 +77,12 @@ module CocoapodsXCRemoteCacheModifier
|
||||
|
||||
def self.validate_configuration()
|
||||
required_values = [
|
||||
'cache_addresses',
|
||||
'cache_addresses',
|
||||
'primary_repo',
|
||||
'check_build_configuration',
|
||||
'check_platform'
|
||||
]
|
||||
|
||||
|
||||
missing_configuration_values = required_values.select { |v| !@@configuration.key?(v) }
|
||||
unless missing_configuration_values.empty?
|
||||
throw "XCRemoteCache not fully configured. Make sure all required fields are provided. Missing fields are: #{missing_configuration_values.join(', ')}."
|
||||
@@ -105,13 +107,25 @@ module CocoapodsXCRemoteCacheModifier
|
||||
end
|
||||
|
||||
# @param target [Target] target to apply XCRemoteCache
|
||||
# @param repo_distance [Integer] distance from the git repo root to the target's $SRCROOT
|
||||
# @param repo_distance [Integer] distance from the git repo root to the target's $SRCROOT
|
||||
# @param xc_location [String] path to the dir with all XCRemoteCache binaries, relative to the repo root
|
||||
# @param xc_cc_path [String] path to the XCRemoteCache clang wrapper, relative to the repo root
|
||||
# @param mode [String] mode name ('consumer', 'producer', 'producer-fast' etc.)
|
||||
# @param exclude_build_configurations [String[]] list of targets that should have disabled remote cache
|
||||
# @param final_target [String] name of target that should trigger marking
|
||||
def self.enable_xcremotecache(target, repo_distance, xc_location, xc_cc_path, mode, exclude_build_configurations, final_target, fake_src_root)
|
||||
# @param exclude_sdks_configurations [String[]] list of sdks that should have disabled remote cache
|
||||
def self.enable_xcremotecache(
|
||||
target,
|
||||
repo_distance,
|
||||
xc_location,
|
||||
xc_cc_path,
|
||||
mode,
|
||||
exclude_build_configurations,
|
||||
final_target,
|
||||
fake_src_root,
|
||||
exclude_sdks_configurations,
|
||||
enable_swift_driver_integration
|
||||
)
|
||||
srcroot_relative_xc_location = parent_dir(xc_location, repo_distance)
|
||||
# location of the entrite CocoaPods project, relative to SRCROOT
|
||||
srcroot_relative_project_location = parent_dir('', repo_distance)
|
||||
@@ -120,22 +134,28 @@ module CocoapodsXCRemoteCacheModifier
|
||||
# apply only for relevant Configurations
|
||||
next if exclude_build_configurations.include?(config.name)
|
||||
if mode == 'consumer'
|
||||
config.build_settings['CC'] = ["$SRCROOT/#{parent_dir(xc_cc_path, repo_distance)}"]
|
||||
reset_build_setting(config.build_settings, 'CC', "$SRCROOT/#{parent_dir(xc_cc_path, repo_distance)}", exclude_sdks_configurations)
|
||||
elsif mode == 'producer' || mode == 'producer-fast'
|
||||
config.build_settings.delete('CC') if config.build_settings.key?('CC')
|
||||
end
|
||||
config.build_settings['SWIFT_EXEC'] = ["$SRCROOT/#{srcroot_relative_xc_location}/xcswiftc"]
|
||||
config.build_settings['LIBTOOL'] = ["$SRCROOT/#{srcroot_relative_xc_location}/xclibtool"]
|
||||
config.build_settings['LD'] = ["$SRCROOT/#{srcroot_relative_xc_location}/xcld"]
|
||||
config.build_settings['LDPLUSPLUS'] = ["$SRCROOT/#{srcroot_relative_xc_location}/xcldplusplus"]
|
||||
config.build_settings['SWIFT_USE_INTEGRATED_DRIVER'] = ['NO']
|
||||
swiftc_name = enable_swift_driver_integration ? 'swiftc' : 'xcswiftc'
|
||||
reset_build_setting(config.build_settings, 'SWIFT_EXEC', "$SRCROOT/#{srcroot_relative_xc_location}/#{swiftc_name}", exclude_sdks_configurations)
|
||||
reset_build_setting(config.build_settings, 'LIBTOOL', "$SRCROOT/#{srcroot_relative_xc_location}/xclibtool", exclude_sdks_configurations)
|
||||
# Setting LIBTOOL to '' breaks SwiftDriver intengration so resetting it to the original value 'libtool' for all excluded configurations
|
||||
add_build_setting_for_sdks(config.build_settings, 'LIBTOOL', 'libtool', exclude_sdks_configurations)
|
||||
reset_build_setting(config.build_settings, 'LD', "$SRCROOT/#{srcroot_relative_xc_location}/xcld", exclude_sdks_configurations)
|
||||
reset_build_setting(config.build_settings, 'LDPLUSPLUS', "$SRCROOT/#{srcroot_relative_xc_location}/xcldplusplus", exclude_sdks_configurations)
|
||||
reset_build_setting(config.build_settings, 'LIPO', "$SRCROOT/#{srcroot_relative_xc_location}/xclipo", exclude_sdks_configurations)
|
||||
reset_build_setting(config.build_settings, 'SWIFT_USE_INTEGRATED_DRIVER', 'NO', exclude_sdks_configurations) unless enable_swift_driver_integration
|
||||
|
||||
config.build_settings['XCREMOTE_CACHE_FAKE_SRCROOT'] = fake_src_root
|
||||
config.build_settings['XCRC_PLATFORM_PREFERRED_ARCH'] = ["$(LINK_FILE_LIST_$(CURRENT_VARIANT)_$(PLATFORM_PREFERRED_ARCH):dir:standardizepath:file:default=arm64)"]
|
||||
config.build_settings[XCRC_COOCAPODS_ROOT_KEY] = ["$SRCROOT/#{srcroot_relative_project_location}"]
|
||||
reset_build_setting(config.build_settings, 'XCREMOTE_CACHE_FAKE_SRCROOT', fake_src_root, exclude_sdks_configurations)
|
||||
reset_build_setting(config.build_settings, 'XCRC_PLATFORM_PREFERRED_ARCH', "$(LINK_FILE_LIST_$(CURRENT_VARIANT)_$(PLATFORM_PREFERRED_ARCH):dir:standardizepath:file:default=arm64)", exclude_sdks_configurations)
|
||||
reset_build_setting(config.build_settings, XCRC_COOCAPODS_ROOT_KEY, "$SRCROOT/#{srcroot_relative_project_location}", exclude_sdks_configurations)
|
||||
debug_prefix_map_replacement = '$(SRCROOT' + ':dir:standardizepath' * repo_distance + ')'
|
||||
add_cflags!(config.build_settings, '-fdebug-prefix-map', "#{debug_prefix_map_replacement}=$(XCREMOTE_CACHE_FAKE_SRCROOT)")
|
||||
add_swiftflags!(config.build_settings, '-debug-prefix-map', "#{debug_prefix_map_replacement}=$(XCREMOTE_CACHE_FAKE_SRCROOT)")
|
||||
add_cflags!(config.build_settings, '-fdebug-prefix-map', "#{debug_prefix_map_replacement}=$(XCREMOTE_CACHE_FAKE_SRCROOT)", exclude_sdks_configurations)
|
||||
add_swiftflags!(config.build_settings, '-debug-prefix-map', "#{debug_prefix_map_replacement}=$(XCREMOTE_CACHE_FAKE_SRCROOT)", exclude_sdks_configurations)
|
||||
delete_build_setting(config.build_settings, 'XCRC_DISABLED')
|
||||
add_build_setting_for_sdks(config.build_settings, 'XCRC_DISABLED', 'YES', exclude_sdks_configurations)
|
||||
end
|
||||
|
||||
# Prebuild
|
||||
@@ -156,7 +176,7 @@ module CocoapodsXCRemoteCacheModifier
|
||||
prebuild_script.dependency_file = "$(TARGET_TEMP_DIR)/prebuild.d"
|
||||
|
||||
# Move prebuild (last element) to the position before compile sources phase (to make it real 'prebuild')
|
||||
if !existing_prebuild_script
|
||||
if !existing_prebuild_script
|
||||
compile_phase_index = target.build_phases.index(target.source_build_phase)
|
||||
target.build_phases.insert(compile_phase_index, target.build_phases.delete(prebuild_script))
|
||||
end
|
||||
@@ -184,7 +204,7 @@ module CocoapodsXCRemoteCacheModifier
|
||||
]
|
||||
postbuild_script.dependency_file = "$(TARGET_TEMP_DIR)/postbuild.d"
|
||||
# Move postbuild (last element) to the position after compile sources phase (to make it real 'postbuild')
|
||||
if !existing_postbuild_script
|
||||
if !existing_postbuild_script
|
||||
compile_phase_index = target.build_phases.index(target.source_build_phase)
|
||||
target.build_phases.insert(compile_phase_index + 1, target.build_phases.delete(postbuild_script))
|
||||
end
|
||||
@@ -214,6 +234,7 @@ module CocoapodsXCRemoteCacheModifier
|
||||
config.build_settings.delete('CC') if config.build_settings.key?('CC')
|
||||
config.build_settings.delete('SWIFT_EXEC') if config.build_settings.key?('SWIFT_EXEC')
|
||||
config.build_settings.delete('LIBTOOL') if config.build_settings.key?('LIBTOOL')
|
||||
config.build_settings.delete('LIPO') if config.build_settings.key?('LIPO')
|
||||
config.build_settings.delete('LD') if config.build_settings.key?('LD')
|
||||
config.build_settings.delete('LDPLUSPLUS') if config.build_settings.key?('LDPLUSPLUS')
|
||||
config.build_settings.delete('SWIFT_USE_INTEGRATED_DRIVER') if config.build_settings.key?('SWIFT_USE_INTEGRATED_DRIVER')
|
||||
@@ -226,9 +247,9 @@ module CocoapodsXCRemoteCacheModifier
|
||||
end
|
||||
|
||||
# User project is not generated from scratch (contrary to `Pods`), delete all previous XCRemoteCache phases
|
||||
target.build_phases.delete_if {|phase|
|
||||
target.build_phases.delete_if {|phase|
|
||||
# Some phases (e.g. PBXSourcesBuildPhase) don't have strict name check respond_to?
|
||||
if phase.respond_to?(:name)
|
||||
if phase.respond_to?(:name)
|
||||
phase.name != nil && phase.name.start_with?("[XCRC]")
|
||||
end
|
||||
}
|
||||
@@ -240,9 +261,9 @@ module CocoapodsXCRemoteCacheModifier
|
||||
end
|
||||
|
||||
def self.download_xcrc_if_needed(local_location)
|
||||
required_binaries = ['xcld', 'xcldplusplus', 'xclibtool', 'xcpostbuild', 'xcprebuild', 'xcprepare', 'xcswiftc']
|
||||
required_binaries = ['xcld', 'xcldplusplus', 'xclibtool', 'xclipo', 'xcpostbuild', 'xcprebuild', 'xcprepare', 'xcswiftc']
|
||||
binaries_exist = required_binaries.reduce(true) do |exists, filename|
|
||||
file_path = File.join(local_location, filename)
|
||||
file_path = File.join(local_location, filename)
|
||||
exists = exists && File.exist?(file_path)
|
||||
end
|
||||
|
||||
@@ -256,13 +277,13 @@ module CocoapodsXCRemoteCacheModifier
|
||||
|
||||
if !system("unzip #{local_package_location} -d #{local_location}")
|
||||
throw "Unzipping XCRemoteCache failed"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.download_latest_xcrc_release(local_package_location)
|
||||
release_url = 'https://api.github.com/repos/spotify/XCRemoteCache/releases/latest'
|
||||
asset_url = nil
|
||||
|
||||
|
||||
URI.open(release_url) do |f|
|
||||
assets_array = JSON.parse(f.read)['assets']
|
||||
# Pick fat archive
|
||||
@@ -270,7 +291,7 @@ module CocoapodsXCRemoteCacheModifier
|
||||
asset_url = asset_array['url']
|
||||
end
|
||||
|
||||
if asset_url.nil?
|
||||
if asset_url.nil?
|
||||
throw "Downloading XCRemoteCache failed"
|
||||
end
|
||||
|
||||
@@ -281,9 +302,8 @@ module CocoapodsXCRemoteCacheModifier
|
||||
end
|
||||
end
|
||||
|
||||
def self.add_cflags!(options, key, value)
|
||||
return if options.fetch('OTHER_CFLAGS',[]).include?(value)
|
||||
options['OTHER_CFLAGS'] = remove_cflags!(options, key) << "#{key}=#{value}"
|
||||
def self.add_cflags!(options, key, value, exclude_sdks_configurations)
|
||||
reset_build_setting(options, 'OTHER_CFLAGS', remove_cflags!(options, key) << "#{key}=#{value}", exclude_sdks_configurations)
|
||||
end
|
||||
|
||||
def self.remove_cflags!(options, key)
|
||||
@@ -293,9 +313,8 @@ module CocoapodsXCRemoteCacheModifier
|
||||
options['OTHER_CFLAGS']
|
||||
end
|
||||
|
||||
def self.add_swiftflags!(options, key, value)
|
||||
return if options.fetch('OTHER_SWIFT_FLAGS','').include?(value)
|
||||
options['OTHER_SWIFT_FLAGS'] = remove_swiftflags!(options, key) + " #{key} #{value}"
|
||||
def self.add_swiftflags!(options, key, value, exclude_sdks_configurations)
|
||||
reset_build_setting(options, 'OTHER_SWIFT_FLAGS', remove_swiftflags!(options, key) + " #{key} #{value}", exclude_sdks_configurations)
|
||||
end
|
||||
|
||||
def self.remove_swiftflags!(options, key)
|
||||
@@ -303,6 +322,34 @@ module CocoapodsXCRemoteCacheModifier
|
||||
options['OTHER_SWIFT_FLAGS']
|
||||
end
|
||||
|
||||
def self.add_build_setting(build_settings, key, value, exclude_sdks_configurations)
|
||||
build_settings[key] = value
|
||||
for exclude_sdks_configuration in exclude_sdks_configurations
|
||||
build_settings["#{key}[sdk=#{exclude_sdks_configuration}]"] = [""]
|
||||
end
|
||||
end
|
||||
|
||||
# Deletes all previous build settings for a key, and sets a new value to all configurations
|
||||
# but the sdks in exclude_sdks_configurations
|
||||
def self.reset_build_setting(build_settings, key, value, exclude_sdks_configurations)
|
||||
delete_build_setting(build_settings, key)
|
||||
add_build_setting(build_settings, key, value, exclude_sdks_configurations)
|
||||
end
|
||||
|
||||
# Delete all build setting for a key, including settings like "[skd=*,arch=*]"
|
||||
def self.delete_build_setting(build_settings, key)
|
||||
for build_setting_key in build_settings.keys
|
||||
build_settings.delete(build_setting_key) if build_setting_key == key || build_setting_key.start_with?("#{key}[")
|
||||
end
|
||||
end
|
||||
|
||||
# Sets value for a key only for a subset of sdk configurations
|
||||
def self.add_build_setting_for_sdks(build_settings, key, value, sdk_configurations)
|
||||
for sdk_configuration in sdk_configurations
|
||||
build_settings["#{key}[sdk=#{sdk_configuration}]"] = value
|
||||
end
|
||||
end
|
||||
|
||||
# Uninstall the XCRemoteCache
|
||||
def self.disable_xcremotecache(user_project, pods_project = nil)
|
||||
user_project.targets.each do |target|
|
||||
@@ -336,7 +383,7 @@ module CocoapodsXCRemoteCacheModifier
|
||||
File.open(lldbinit_path) { |file|
|
||||
while(line = file.gets) != nil
|
||||
line = line.strip
|
||||
if line == LLDB_INIT_COMMENT
|
||||
if line == LLDB_INIT_COMMENT
|
||||
# skip current and next lines
|
||||
file.gets
|
||||
next
|
||||
@@ -351,7 +398,7 @@ module CocoapodsXCRemoteCacheModifier
|
||||
def self.add_lldbinit_rewrite(lines_content, user_proj_directory,fake_src_root)
|
||||
all_lines = lines_content.clone
|
||||
all_lines << LLDB_INIT_COMMENT
|
||||
all_lines << "settings set target.source-map #{fake_src_root} #{user_proj_directory}"
|
||||
all_lines << "settings set target.source-map #{fake_src_root} #{user_proj_directory}"
|
||||
all_lines << ""
|
||||
all_lines
|
||||
end
|
||||
@@ -362,6 +409,11 @@ module CocoapodsXCRemoteCacheModifier
|
||||
File.write(LLDB_INIT_PATH, lldbinit_lines.join("\n"), mode: "w")
|
||||
end
|
||||
|
||||
# Contrary to AbstractTarget.source_build_phase, it only finds a build phase, without creating one if it doesn't exist
|
||||
def self.find_source_build_phase(target)
|
||||
target.build_phases.find { |bp| bp.class == Xcodeproj::Project::Object::PBXSourcesBuildPhase }
|
||||
end
|
||||
|
||||
Pod::HooksManager.register('cocoapods-xcremotecache', :pre_install) do |installer_context|
|
||||
# The main responsibility of that hook is forcing Pods regeneration when XCRemoteCache is enabled for the first time
|
||||
# In the post_install hook, this plugin adds extra build settings and steps to all Pods targets, but only when XCRemoteCache
|
||||
@@ -413,8 +465,8 @@ module CocoapodsXCRemoteCacheModifier
|
||||
# Remote cache is still disabled - no need to force Pods projects/targets regeneration
|
||||
next
|
||||
end
|
||||
|
||||
# Force rebuilding all Pods project, because XCRC build steps and settings need to be added to Pods project/targets
|
||||
|
||||
# Force rebuilding all Pods project, because XCRC build steps and settings need to be added to Pods project/targets
|
||||
# It is relevant only when 'incremental_installation' is enabled, otherwise installed_cache_path does not exist on a disk
|
||||
installed_cache_path = installer_context.sandbox.project_installation_cache_path
|
||||
if !was_previously_enabled && File.exist?(installed_cache_path)
|
||||
@@ -431,7 +483,7 @@ module CocoapodsXCRemoteCacheModifier
|
||||
|
||||
user_project = installer_context.umbrella_targets[0].user_project
|
||||
|
||||
begin
|
||||
begin
|
||||
user_proj_directory = File.dirname(user_project.path)
|
||||
set_configuration_default_values
|
||||
|
||||
@@ -452,6 +504,8 @@ module CocoapodsXCRemoteCacheModifier
|
||||
check_build_configuration = @@configuration['check_build_configuration']
|
||||
check_platform = @@configuration['check_platform']
|
||||
fake_src_root = @@configuration['fake_src_root']
|
||||
exclude_sdks_configurations = @@configuration['exclude_sdks_configurations'] || []
|
||||
enable_swift_driver_integration = @@configuration['enable_swift_driver_integration'] || false
|
||||
|
||||
xccc_location_absolute = "#{user_proj_directory}/#{xccc_location}"
|
||||
xcrc_location_absolute = "#{user_proj_directory}/#{xcrc_location}"
|
||||
@@ -471,11 +525,13 @@ module CocoapodsXCRemoteCacheModifier
|
||||
# Attach XCRemoteCache to Pods targets
|
||||
# Enable only for native targets which can have compilation steps
|
||||
installer_context.pods_project.native_targets.each do |target|
|
||||
# Ensure the PBXSourcesBuildPhase exists as the flow would unnecessary create an empty source build phase otherwise
|
||||
next if find_source_build_phase(target).nil?
|
||||
next if target.source_build_phase.files_references.empty?
|
||||
next if target.name.start_with?("Pods-")
|
||||
next if target.name.end_with?("Tests")
|
||||
next if exclude_targets.include?(target.name)
|
||||
enable_xcremotecache(target, 1, xcrc_location, xccc_location, mode, exclude_build_configurations, final_target,fake_src_root)
|
||||
enable_xcremotecache(target, 1, xcrc_location, xccc_location, mode, exclude_build_configurations, final_target,fake_src_root, exclude_sdks_configurations, enable_swift_driver_integration)
|
||||
end
|
||||
|
||||
# Create .rcinfo into `Pods` directory as that .xcodeproj reads configuration from .xcodeproj location
|
||||
@@ -488,18 +544,18 @@ module CocoapodsXCRemoteCacheModifier
|
||||
next if target.source_build_phase.files_references.empty?
|
||||
next if target.name.end_with?("Tests")
|
||||
next if exclude_targets.include?(target.name)
|
||||
enable_xcremotecache(target, 1, xcrc_location, xccc_location, mode, exclude_build_configurations, final_target,fake_src_root)
|
||||
enable_xcremotecache(target, 1, xcrc_location, xccc_location, mode, exclude_build_configurations, final_target,fake_src_root, exclude_sdks_configurations, enable_swift_driver_integration)
|
||||
end
|
||||
generated_project.save()
|
||||
end
|
||||
|
||||
# Manual Pods/.rcinfo generation
|
||||
|
||||
|
||||
# all paths in .rcinfo are relative to the root so paths used in Pods.xcodeproj need to be aligned
|
||||
pods_path = Pathname.new(pods_proj_directory)
|
||||
root_path = Pathname.new(user_proj_directory)
|
||||
root_path_to_pods = root_path.relative_path_from(pods_path)
|
||||
|
||||
|
||||
pods_rcinfo = root_rcinfo.merge({
|
||||
'remote_commit_file' => "#{root_path_to_pods}/#{remote_commit_file}",
|
||||
'xccc_file' => "#{root_path_to_pods}/#{xccc_location}"
|
||||
@@ -511,7 +567,7 @@ module CocoapodsXCRemoteCacheModifier
|
||||
|
||||
# Enabled/disable XCRemoteCache for the main (user) project
|
||||
begin
|
||||
# TODO: Do not compile xcc again. `xcprepare` compiles it in pre-install anyway
|
||||
# TODO: Do not compile xcc again. `xcprepare` compiles it in pre-install anyway
|
||||
prepare_result = YAML.load`#{xcrc_location_absolute}/xcprepare --configuration #{check_build_configuration} --platform #{check_platform}`
|
||||
unless prepare_result['result'] || mode != 'consumer'
|
||||
# Uninstall the XCRemoteCache for the consumer mode
|
||||
@@ -529,7 +585,7 @@ module CocoapodsXCRemoteCacheModifier
|
||||
# Attach XCRC to the app targets
|
||||
user_project.targets.each do |target|
|
||||
next if exclude_targets.include?(target.name)
|
||||
enable_xcremotecache(target, 0, xcrc_location, xccc_location, mode, exclude_build_configurations, final_target,fake_src_root)
|
||||
enable_xcremotecache(target, 0, xcrc_location, xccc_location, mode, exclude_build_configurations, final_target,fake_src_root, exclude_sdks_configurations, enable_swift_driver_integration)
|
||||
end
|
||||
|
||||
# Set Target sourcemap
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
# Licensed 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.
|
||||
@@ -13,5 +13,5 @@
|
||||
# limitations under the License.
|
||||
|
||||
module CocoapodsXcremotecache
|
||||
VERSION = "0.0.14"
|
||||
VERSION = "0.0.18"
|
||||
end
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Architectural designs
|
||||
|
||||
1. [Swift Driver Integration](./SwiftDriverIntegration.md)
|
||||
@@ -0,0 +1,48 @@
|
||||
## Swift Driver Integration
|
||||
|
||||
### Pre Swift Driver Integration
|
||||
|
||||
Historically (prior to Xcode 14), Swift compilation step was invoked by Xcode as a single external process. Xcode was calling `swiftc` and passing all required parameters (like all input files, output destinations, header paths etc.), and reading its standard output to recognize the status/state of a compilation. Essentially, there were two build systems: "the big one" from Xcode and "small one" by Swift.
|
||||
|
||||
That design was easy to mock in the XCRemoteCache, where the `xcswiftc` wrapper was first inspecting if the cached artifact can be reused (e.g. no new input `.swift` files were added to the list of compilation files) and based on that either continuing with the local compilation (cache miss) or mocking the compilation and existing early (cache hit).
|
||||
|
||||
<p>
|
||||
<img src="./../img/pre-driver.png#gh-light-mode-only">
|
||||
<img src="./../img/pre-driver-dark.png#gh-dark-mode-only">
|
||||
</p>
|
||||
|
||||
### Swift Driver Integration Design
|
||||
|
||||
With the upgraded design (aka Swift Driver Integration), Xcode splits the work into `n` subprocesses (when `n` is ~CPU), each responsible to compile a subset of files/actions. To align with that, XCRemoteCache meeds to specify a single place to identify if the cached artifact is applicable. `swift-frontend` has been picked for that - process responsible for module emitting. By reviewing Xcode's behavior, it has been found that this process is scheduled very early in the workflow timeline (with some approximation, we could say it is scheduled as a first step) so it seems as best candidate for the "pre-work".
|
||||
|
||||
As the same executable `swift-frontend` is invoked multiple times for the same target (e.g. to emit module, multiple batches of compilation etc.), XCRemoteCaches uses a file lock-based synchronization. Each `xcswift-frontend` (the wrapper for `swift-frontend`) tries to acquire a unique lock file. The lock has a name `$LLBUILD_BUILD_ID.lock`, which is unique for each build, placed in the `Intermediate` directory. `xcswift-frontend` process reads its content to find if the "pre-work" from the emit-module has already been done - if not, it releases a lock a gives a way to other processes (presumably the "emit-module") to do the required work. As a lock file is unique per target and a build (it is actually unique per target compilation, placed in `TARGET_TEMP_DIR`), initially the file is empty.
|
||||
|
||||
Note the emit module step holds a shared lock for the time of the entire process lifetime, so only once the "pre-work" is finished, all other `xcswift-frontend` processes can continue their job (with either noop or fallbacking to the `swift-frontend` in case a cache miss). Non emit-module steps (compilation steps) acquire a lock only for a very short period - to read the content of that file, thus multiple batches of compilation can run in parallel.
|
||||
|
||||
<p>
|
||||
<img src="./../img/driver.png#gh-light-mode-only">
|
||||
<img src="./../img/driver-dark.png#gh-dark-mode-only">
|
||||
</p>
|
||||
|
||||
<img src="./../img/sample-driver-timeline.png" width="600px">
|
||||
|
||||
### Sample timelines
|
||||
|
||||
#### Emit Module acquires a lock first (common)
|
||||
|
||||
<p>
|
||||
<img src="./../img/driver-scenario1.png#gh-light-mode-only">
|
||||
<img src="./../img/driver-scenario1-dark.png#gh-dark-mode-only">
|
||||
</p>
|
||||
|
||||
#### A compilation step acquires a lock first (uncommon but possible)
|
||||
|
||||
<p>
|
||||
<img src="./../img/driver-scenario2.png#gh-light-mode-only">
|
||||
<img src="./../img/driver-scenario2-dark.png#gh-dark-mode-only">
|
||||
</p>
|
||||
|
||||
### Other considerations/open questions
|
||||
|
||||
* For mixed targets (ObjC&Swift), Xcode triggers `.m` compilation steps **after** the module emitting to ensure that the `-Swift.h` is available for clang compilation. That means, the synchronization algorithm will postpone any `clang` invocations until the Swift "pre-work" is done. Therefore, mixed targets should behave the same way as in the non Swift Driver Integration flow
|
||||
* For the WMO (Whole Module Optimization) mode, all compilation steps are combined into a single `swift-frontend` process. As the emit-module step is still invoked first, the WMO flow build can be considered as a special case of the algorithm described above (where there is only one compilation invocation). Therefore, the presented algorithm will work for the WMO mode out of the box.
|
||||
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 298 KiB |