Compare commits

...

95 Commits

Author SHA1 Message Date
Bartosz Polaczyk 75bac3293a Merge pull request #227 from CharlieSuP1/master
prebuild should run artifactsOrganizer.prepare(artifact:) method even  if artifacts exists locally
2024-07-09 19:43:02 -07:00
Bartosz Polaczyk 8c89e88716 Merge pull request #234 from BalestraPatrick/patch-1
Remove references to Spotify FOSS Slack
2024-07-09 19:42:11 -07:00
Bartosz Polaczyk 0f97aa120f Merge pull request #238 from Coder-Star/bug/action
fix: archive build, the ACTION value is install
2024-07-09 19:41:39 -07:00
CoderStar 9f5c455ea6 fix: archive build, the ACTION value is install 2024-07-09 14:53:53 +08:00
Bartosz Polaczyk 86e64d3eab Merge pull request #239 from polac24/bump-xcode-143
Bump CI to Xcode 14.3.1/macOS14
2024-07-03 07:19:45 -07:00
Bartosz Polaczyk 832e0ffeb0 Install nginx on CI 2024-07-02 23:23:16 -07:00
Bartosz Polaczyk 4db65a9bc5 Bump macOS to 14 2024-07-02 23:05:18 -07:00
Bartosz Polaczyk 34e8c0b911 Update release.yaml 2024-07-02 22:57:14 -07:00
Bartosz Polaczyk acd866c242 Update docs.yaml 2024-07-02 22:57:02 -07:00
Bartosz Polaczyk 2e6729ecaa Bump ci.yaml 2024-07-02 22:56:22 -07:00
Patrick Balestra a05fa9cab5 Remove references to Spotify FOSS Slack
The FOSS Slack is being shut down. We will continue use GitHub issues, discussions, etc. for receiving feedback and bug reports.
2024-01-10 13:28:41 +01:00
Bartosz Polaczyk 487a58aba0 Merge pull request #230 from grigorye/bug/shell-hangup-due-to-unread-pipe
Fixed task hang up in shellInternal due to unread error pipe.
2023-11-22 09:22:27 -08:00
Grigory Entin 62ace6a24f Fixed errorData generation.
Co-authored-by: Bartosz Polaczyk <polac24@gmail.com>
2023-11-21 09:54:22 +01:00
Grigory Entin 0290557197 Fixed task hang up in shellInternal due to unread error pipe. 2023-11-20 15:42:12 +01:00
supeng.charlie 68b1f76cd4 prebuild should run artifactsOrganizer.prepare(artifact:) method even if artifacts exists locally 2023-09-19 17:55:44 +08:00
Bartosz Polaczyk 64472d58bf Merge pull request #225 from polac24/fix-spaces-libtool
Support spaces in libtools paths
2023-08-17 08:54:47 +02:00
Bartosz Polaczyk 3d96dddc91 Fix linting 2023-08-16 14:23:13 +02:00
Bartosz Polaczyk deb6adcb70 Support spaces in libtools paths 2023-08-16 13:13:14 +02:00
Bartosz Polaczyk f8c854d007 Merge pull request #214 from polac24/up-to-date-meta
Ensure up-to-date meta json in the unzipped artifact
2023-08-03 15:48:53 +02:00
Bartosz Polaczyk 7fe04517e7 Merge pull request #222 from polac24/libtool-mode-fix
Improve XCLibtool's argument parsing
2023-08-03 12:51:29 +02:00
Bartosz Polaczyk 5029f9c73b Improve Libtool libraries argument parsing 2023-08-02 22:10:03 +02:00
Bartosz Polaczyk ed256234f1 Merge pull request #221 from grigorye/fix/xclibtool-static-broken-markers
Fixed xclibtool not accounting the artifacts retrieved from cache in certain scenarios.
2023-08-02 21:00:45 +02:00
Grigory Entin fd6e1da054 Fixed -static flag treated as an libtool input (library). 2023-08-02 04:26:23 +02:00
Grigory Entin 574129788c Fixed libtool executable accounted as its own argument. 2023-08-02 04:26:23 +02:00
Bartosz Polaczyk 6a1335ea97 Merge pull request #218 from polac24/support-
[Cocoapods] Ensure the source build phase exist before enabling xcrc
2023-08-01 10:50:27 +02:00
Bartosz Polaczyk be87c3779e Bump version 2023-07-29 23:11:58 -07:00
Bartosz Polaczyk 2c23632732 fix module 2023-07-29 22:55:23 -07:00
Bartosz Polaczyk b048393eb0 Define module of the source phase 2023-07-29 22:51:52 -07:00
Bartosz Polaczyk 8955cb2750 Bail out if source build phase phase doesnt exist 2023-07-29 22:46:54 -07:00
Bartosz Polaczyk afb1f9e531 Merge pull request #215 from polac24/xcode-15-lightweight-support
Add support for Xcode 15
2023-06-14 02:56:02 -04:00
Bartosz Polaczyk 53e7ddd34c Fix linters 2023-06-13 21:57:31 -07:00
Bartosz Polaczyk 344b1f13ca Cleanup 2023-06-13 21:25:34 -07:00
Bartosz Polaczyk bc13f58e73 Add unit tests 2023-06-13 21:08:56 -07:00
Bartosz Polaczyk 47ad8a890b Fix swiftc allowed paths 2023-06-12 23:33:09 -07:00
Bartosz Polaczyk 801920dc87 Fix reference path 2023-06-12 23:25:37 -07:00
Bartosz Polaczyk 37e144c36a Add unit tests 2023-06-12 22:58:05 -07:00
Bartosz Polaczyk f389887e37 Read also dependencies from assetcatalog_dependencies output 2023-06-12 22:22:59 -07:00
Bartosz Polaczyk 587840ddbc Fix linter 2023-06-12 19:44:08 -07:00
Bartosz Polaczyk 42a56b4f5b Add tests for ZipArtifactOrganizer 2023-06-12 19:23:36 -07:00
Bartosz Polaczyk 6a98cb4127 Add unit tests for updater 2023-06-12 18:59:35 -07:00
Bartosz Polaczyk d5e95962b5 Keep fresh meta in active artifact dir 2023-06-12 17:59:05 -07:00
Bartosz Polaczyk b74e002415 Merge pull request #212 from polac24/add-driver-tests
[SwiftDriverIntegraiton] Part IV: Add E2E tests for the swift driver integration #5
2023-06-07 10:07:04 -04:00
Bartosz Polaczyk 9a34a99f0b Merge pull request #210 from polac24/add-driver-sync
[SwiftDriver] Part III - Introduce synchronization between swift-frontend invocations
2023-06-06 02:23:21 -04:00
Bartosz Polaczyk f4eed9e8aa Apply review suggestions 2023-06-05 22:45:41 -07:00
Bartosz Polaczyk 811cc00f0c Add E2E tests for swift driver integration 2023-06-04 08:56:38 -07:00
Bartosz Polaczyk 56d9c208a1 Fix typos 2023-06-03 21:25:31 -07:00
Bartosz Polaczyk 8dffbd4162 Add missing unit tests 2023-06-03 17:50:50 -07:00
Bartosz Polaczyk 3853ce2bc2 Write to the shared frontend lock early 2023-06-03 09:32:53 -07:00
Bartosz Polaczyk 148c99d2f5 Add docs 2023-06-01 20:32:03 -07:00
Bartosz Polaczyk 65bf9156ec Merge pull request #209 from polac24/add-driver-parsing
Swift-driver integration, Part II: add Swift front-end parsing stage
2023-06-01 10:12:38 -04:00
Bartosz Polaczyk 22484b4daf Add syncing implementation 2023-05-31 19:55:30 -07:00
Bartosz Polaczyk 867bbb6265 Fix typos 2023-05-31 10:22:34 -04:00
Bartosz Polaczyk 532484c3ab Add comment 2023-05-30 21:38:22 -07:00
Bartosz Polaczyk 338cbd141a Fix linters 2023-05-30 21:08:05 -07:00
Bartosz Polaczyk f15dd8f98d Add Swift front-end mocking parsing stage 2023-05-30 20:53:48 -07:00
Bartosz Polaczyk ee31a3815f Merge pull request #208 from polac24/support-xcode14
Refactor existing implementation for swift-driver change
2023-05-30 09:58:36 -04:00
Bartosz Polaczyk 5749762b86 Fix compilation 2023-05-29 21:25:46 -07:00
Bartosz Polaczyk f8757b6ee7 Add unit tests 2023-05-29 21:01:53 -07:00
Bartosz Polaczyk c26aaf7d42 Refactor existing implementation for swift-driver change 2023-05-29 20:25:50 -07:00
Bartosz Polaczyk d837f6e14b Merge pull request #205 from polac24/bartosz/20230516-support-xcode143
Support Xcode14.3
2023-05-18 07:00:07 -07:00
Bartosz Polaczyk 5528d507b0 Support Xcode 14.3 2023-05-16 16:37:28 -07:00
Bartosz Polaczyk 352e72f44c Merge pull request #199 from polac24/add-graceful-missing-common-sha
Add support for graceful handling missing common sha
2023-04-20 10:04:08 -04:00
Bartosz Polaczyk 1c67b79a7a Apply suggestions from code review
Co-authored-by: Aleksander Grzyb <aleksander.grzyb@gmail.com>
2023-04-19 21:44:17 -04:00
Bartosz Polaczyk c7de203741 Fix linting 2023-04-18 19:51:10 -07:00
Bartosz Polaczyk b7e18916e6 Add support for graceful handling missing common sha 2023-04-18 19:41:53 -07:00
Bartosz Polaczyk dfb4039404 Merge pull request #198 from polac24/bartosz/20230413-override-exclusions
Set libtool Build Setting for excluded sdks
2023-04-14 20:21:37 -07:00
Bartosz Polaczyk b28613a2ef Fix formatting 2023-04-13 18:47:54 -07:00
Bartosz Polaczyk 1535b762bc Cleanup unnecessary unless 2023-04-13 18:33:10 -07:00
Bartosz Polaczyk e6816846c3 Add original LIBTOOL to excluded SDKs 2023-04-13 18:27:27 -07:00
Bartosz Polaczyk b89d98f411 Reset LIBTOOL to libtool for excluded sdks 2023-04-12 17:43:22 -07:00
Bartosz Polaczyk a0d3d1b0b9 Reset SWIFT_EXEC to swiftc for excluded sdks 2023-04-11 22:33:20 -07:00
Bartosz Polaczyk f432917505 Merge pull request #197 from polac24/fix-exclude-sdk
Reset cflags and swiftflags on each cocoapods integration
2023-04-11 18:49:32 -07:00
Bartosz Polaczyk 5d297a4fb2 Revert incorrect OTHER_SWIFT_FLAGS format 2023-04-10 22:04:45 -07:00
Bartosz Polaczyk b0d5f1660e Reset cflags and swiftflags on each cocoapods integration 2023-04-10 19:30:32 -07:00
Bartosz Polaczyk baea2de79a Merge pull request #196 from polac24/exclude-sdks
Add support for excluded sdks
2023-04-10 19:20:09 -07:00
Bartosz Polaczyk d2803f4ad5 Fix linter 2023-04-09 11:40:46 -07:00
Bartosz Polaczyk ab017367b2 Add disabled support in Marking 2023-04-09 11:35:50 -07:00
Bartosz Polaczyk 30cb648641 Add disabled support in XCPostbuild 2023-04-09 11:06:50 -07:00
Bartosz Polaczyk 3330ca45f8 Cleanup README.md 2023-04-09 09:33:42 -07:00
Bartosz Polaczyk d3193b15a8 Fix hook 2023-04-09 09:30:42 -07:00
Bartosz Polaczyk 7b14f2c9ff Revert e2e cleanups 2023-04-09 09:24:24 -07:00
Bartosz Polaczyk 4933b454a5 Reset build settings in cocoapods 2023-04-09 09:23:49 -07:00
Bartosz Polaczyk 79ffdce295 Fix standalone validation 2023-04-08 20:03:30 -07:00
Bartosz Polaczyk 376e6a17c5 Fix linting 2023-04-08 19:47:42 -07:00
Bartosz Polaczyk a4d1849821 Fix linting 2023-04-08 19:36:35 -07:00
Bartosz Polaczyk 325fb07080 Add explicit switch 2023-04-08 19:27:58 -07:00
Bartosz Polaczyk a2afe62751 Fix configs 2023-04-08 17:51:09 -07:00
Bartosz Polaczyk bbaa374e12 Fix configs jsons 2023-04-08 17:46:41 -07:00
Bartosz Polaczyk 39a259ff49 Fix hook 2023-04-08 17:39:12 -07:00
Bartosz Polaczyk a2b9cbf332 Add cocoapods E2E tests 2023-04-08 17:29:35 -07:00
Bartosz Polaczyk 5710594bc4 Bump version to 0.0.16 2023-04-08 04:37:35 -07:00
Bartosz Polaczyk 7d123792b8 Add cocoapods support 2023-04-08 04:37:14 -07:00
Bartosz Polaczyk 5856dbec77 Add skippedSDKs 2023-04-08 04:19:28 -07:00
Bartosz Polaczyk 600310f44b Merge pull request #194 from polac24/space-aws
Encode path in AWS V4 signing requests
2023-04-03 07:24:02 -07:00
Bartosz Polaczyk aae2b3289c Use addingPercentEncoding for path escaping 2023-03-31 18:33:39 -07:00
103 changed files with 3608 additions and 296 deletions
+4 -2
View File
@@ -13,14 +13,16 @@ jobs:
args: --strict
macOS:
runs-on: macos-12
runs-on: macos-14
env:
XCODE_VERSION: ${{ '14.2' }}
XCODE_VERSION: ${{ '14.3.1' }}
steps:
- name: Select Xcode
run: "sudo xcode-select -s /Applications/Xcode_$XCODE_VERSION.app"
- name: Checkout
uses: actions/checkout@v1
- name: Install nginx
run: brew install nginx
- name: Build and Run
run: rake build[release]
- name: Test
+2 -2
View File
@@ -7,9 +7,9 @@ on:
jobs:
docs:
runs-on: macos-12
runs-on: macos-14
env:
XCODE_VERSION: ${{ '14.2' }}
XCODE_VERSION: ${{ '14.3.1' }}
steps:
- uses: actions/checkout@v2
- name: Select Xcode
+2 -2
View File
@@ -6,9 +6,9 @@ on:
jobs:
macOS:
name: Add macOS binaries to release
runs-on: macos-12
runs-on: macos-14
env:
XCODE_VERSION: ${{ '14.2' }}
XCODE_VERSION: ${{ '14.3.1' }}
steps:
- name: Select Xcode
run: "sudo xcode-select -s /Applications/Xcode_$XCODE_VERSION.app"
+5
View File
@@ -32,6 +32,10 @@ let package = Package(
name: "xcswiftc",
dependencies: ["XCRemoteCache"]
),
.target(
name: "xcswift-frontend",
dependencies: ["XCRemoteCache"]
),
.target(
name: "xclibtoolSupport",
dependencies: ["XCRemoteCache"]
@@ -69,6 +73,7 @@ let package = Package(
dependencies: [
"xcprebuild",
"xcswiftc",
"xcswift-frontend",
"xclibtool",
"xcpostbuild",
"xcprepare",
+9 -4
View File
@@ -7,7 +7,6 @@ _XCRemoteCache is a remote cache tool for Xcode projects. It reuses target artif
[![Build Status](https://github.com/spotify/XCRemoteCache/workflows/CI/badge.svg)](https://github.com/spotify/XCRemoteCache/workflows/CI/badge.svg)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
[![Slack](https://slackin.spotify.com/badge.svg)](https://slackin.spotify.com)
[![Docs](https://github.com/spotify/XCRemoteCache/workflows/Docs/badge.svg)](https://spotify.github.io/XCRemoteCache/documentation/xcremotecache/)
- [How and Why?](#how-and-why)
@@ -45,6 +44,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 +152,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>
@@ -290,7 +291,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
@@ -357,6 +358,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
@@ -466,6 +469,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.
@@ -491,8 +498,6 @@ The zip package will be generated at `releases/XCRemoteCache.zip`.
Create a [new issue](https://github.com/spotify/XCRemoteCache/issues/new) with as many details as possible.
Reach us at the `#xcremotecache` channel in [Slack](https://slackin.spotify.com/).
## Contributing
We feel that a welcoming community is important and we ask that you follow Spotify's
+10 -3
View File
@@ -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', 'xclipo']
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)
}
}
@@ -25,6 +25,8 @@ public enum XCLibtoolMode: Equatable {
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 {
@@ -50,6 +52,8 @@ public class XCLibtool {
toolName: "Libtool",
fallbackCommand: "libtool"
)
case .version:
logic = FallbackXCLibtoolLogic(fallbackCommand: "libtool")
}
}
@@ -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 {
@@ -146,5 +151,11 @@ extension PostbuildContext {
// 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
}
@@ -87,6 +91,7 @@ class Prebuild {
switch artifactPreparationResult {
case .artifactExists(let artifactDir):
infoLog("Artifact exists locally at \(artifactDir)")
_ = try artifactsOrganizer.prepare(artifact: artifactDir)
try artifactsOrganizer.activate(extractedArtifact: artifactDir)
case .preparedForArtifact(let artifactPackage):
infoLog("Downloading artifact to \(artifactPackage)")
@@ -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(
@@ -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,24 +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
result["LIPO"] = wrappers.lipo.path
result["LDPLUSPLUS"] = wrappers.ldplusplus.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
@@ -63,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,15 +42,22 @@ 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"),
@@ -58,5 +65,6 @@ extension IntegrateContext {
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 {
@@ -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" +
+2 -2
View File
@@ -84,13 +84,13 @@ private func shellInternal(_ cmd: String, args: [String] = [], stdout: PipeLike?
task.currentDirectoryPath = dir
}
task.launch()
let errorData = (stderr != nil) ? nil : errorHandle.fileHandleForReading.readDataToEndOfFile()
task.waitUntilExit()
if task.terminationStatus != 0 {
if stderr != nil {
guard let errorData = errorData else {
// Error stream was captured so cannot inspect its content
throw ShellError.statusError("Failed command", task.terminationStatus)
}
let errorData = errorHandle.fileHandleForReading.readDataToEndOfFile()
let errorString = String(data: errorData, encoding: .utf8)?.trim() ?? "No error returned from the process."
throw ShellError.statusError(
"status \(task.terminationStatus): \(errorString)", task.terminationStatus
@@ -37,7 +37,7 @@ class ActionSpecificCacheHitLogger: CacheHitLogger {
case .index:
hitCounter = .indexingTargetHitCount
missCounter = .indexingTargetMissCount
case .build:
case .build, .archive:
hitCounter = .targetCacheHit
missCounter = .targetCacheMiss
case .unknown:
@@ -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"
}
}
@@ -25,6 +25,8 @@ enum BuildActionType: String, Codable {
case build
/// An extra build, exclusive for indexing (Introduced in Xcode 13)
case index = "indexbuild"
/// Archive build
case archive = "install"
/// Unknown type, probably incompatible Xcode version used
case unknown
}
+1 -1
View File
@@ -35,7 +35,7 @@ public class XCLibtoolMain {
let args = ProcessInfo().arguments
do {
let mode = try XCLibtoolHelper.buildMode(args: args)
let mode = try XCLibtoolHelper.buildMode(args: Array(args.dropFirst()))
try XCLibtool(mode).run()
} catch {
exit(1, "Failed with: \(error). Args: \(args)")
@@ -32,9 +32,12 @@ public class XCLibtoolHelper {
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
@@ -44,7 +47,9 @@ public class XCLibtoolHelper {
case "-dependency_info":
dependencyInfo = args[i + 1]
i += 1
case let input where ["", "a"].contains(URL(string: args[i])?.pathExtension):
case let input where input.starts(with: "/") &&
["", "a"].contains(URL(fileURLWithPath: input).pathExtension):
// Assume always absolute paths to the library
// Support for static frameworks (no extension) and static libraries (.a)
inputLibraries.append(input)
default:
@@ -52,6 +57,9 @@ public class XCLibtoolHelper {
}
i += 1
}
if asksForVersion {
return .version
}
guard let outputInput = output else {
throw XCLibtoolHelperError.missingOutput
}
+6
View File
@@ -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)
}
}
+22
View File
@@ -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()
+31 -24
View File
@@ -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 {
@@ -159,4 +160,44 @@ class PostbuildContextTests: FileXCTestCase {
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")
}
}
@@ -45,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)
@@ -55,7 +61,13 @@ 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)
@@ -65,10 +77,110 @@ class XcodeProjBuildSettingsIntegrateAppenderTests: XCTestCase {
func testConsumerSettingLdPlusPlus() 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 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())
@@ -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&param=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 {
@@ -23,23 +23,23 @@ import XCTest
class XCLibtoolHelperTests: XCTestCase {
func testStaticFrameworkUniversalBinary() throws {
let mode = try XCLibtoolHelper.buildMode(
args: ["-o", "/universal/static", "/arch1/static", "arch2/static"]
args: ["-o", "/universal/static", "/arch1/static", "/arch2/static"]
)
XCTAssertEqual(mode, .createUniversalBinary(
output: "/universal/static",
inputs: ["/arch1/static", "arch2/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"]
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"]
inputs: ["/arch1/static.a", "/arch2/static.a"]
))
}
@@ -63,4 +63,26 @@ class XCLibtoolHelperTests: XCTestCase {
}
}
}
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"]
))
}
func testRecognizesPathsWithSpaces() throws {
let mode = try XCLibtoolHelper.buildMode(
args: ["-static", "-o", "/universal/static", "/arch/with space/static"]
)
XCTAssertEqual(mode, .createUniversalBinary(
output: "/universal/static",
inputs: ["/arch/with space/static"]
))
}
}
+3 -2
View File
@@ -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
@@ -40,6 +40,7 @@ module CocoapodsXCRemoteCacheModifier
'check_platform',
'modify_lldb_init',
'fake_src_root',
'exclude_sdks_configurations'
]
class XCRemoteCache
@@ -64,7 +65,8 @@ module CocoapodsXCRemoteCacheModifier
'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
@@ -111,7 +113,19 @@ module CocoapodsXCRemoteCacheModifier
# @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,23 +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['LIPO'] = ["$SRCROOT/#{srcroot_relative_xc_location}/xclipo"]
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
@@ -283,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)
@@ -295,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)
@@ -305,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|
@@ -364,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
@@ -454,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}"
@@ -473,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
@@ -490,7 +544,7 @@ 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
@@ -531,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
@@ -13,5 +13,5 @@
# limitations under the License.
module CocoapodsXcremotecache
VERSION = "0.0.15"
VERSION = "0.0.18"
end
+3
View File
@@ -0,0 +1,3 @@
# Architectural designs
1. [Swift Driver Integration](./SwiftDriverIntegration.md)
+48
View File
@@ -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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

@@ -376,7 +376,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "ditto \"${SCRIPT_INPUT_FILE_0}\" \"${SCRIPT_OUTPUT_FILE_0}\"\n[ -f \"${SCRIPT_INPUT_FILE_1}\" ] && ditto \"${SCRIPT_INPUT_FILE_1}\" \"${SCRIPT_OUTPUT_FILE_1}\" || rm \"${SCRIPT_OUTPUT_FILE_1}\"\n\n";
shellScript = "ditto \"${SCRIPT_INPUT_FILE_0}\" \"${SCRIPT_OUTPUT_FILE_0}\"\n[ -f \"${SCRIPT_INPUT_FILE_1}\" ] && ditto \"${SCRIPT_INPUT_FILE_1}\" \"${SCRIPT_OUTPUT_FILE_1}\" || rm -f \"${SCRIPT_OUTPUT_FILE_1}\"\n\n";
};
/* End PBXShellScriptBuildPhase section */
+9
View File
@@ -0,0 +1,9 @@
### How to add a CocoaPods scenario
In order to run different test suites in cocoapods integration, you can create multiple `.Podfile` files as a "starting point" of the E2E test.
#### Files organization
1. File `{SUITE_NAME}.Podfile` contains a base Podfile that should be excercised
1. [Optional] File `{SUITE_NAME}.Podfile.config` contains a hash of extra xcremote-cache-plugin parameters that should be added to the `xcremotecache()` configuration in a podfile
1. [Optional] File `{SUITE_NAME}.Podfile.expectations` overrides a set of default validation steps that should be checked after consumer's build. Supported steps are: `hits`, `misses` or `hit_rate` (e.g. `100` for 100$). By default, 100% `hit_rate` and 0 `misses` are used - if you want to skip a validation step for a given suite, you can set it to `null` in a dictionary
+8
View File
@@ -0,0 +1,8 @@
plugin 'cocoapods-xcremotecache'
target 'XCRemoteCacheSample' do
use_frameworks!
pod 'Firebase/Analytics'
pod 'ReactiveSwift'
end
@@ -0,0 +1,3 @@
{
"exclude_sdks_configurations": ["iphoneos*", "iphonesimulator*"]
}
@@ -0,0 +1,5 @@
{
"hits": 0,
"misses": 0,
"hit_rate": null
}

Some files were not shown because too many files have changed in this diff Show More