Compare commits

..

29 Commits

Author SHA1 Message Date
Bartosz Polaczyk de066f2b1c Merge pull request #175 from polac24/polac24-private-swiftinterface
Add SPI files: private.swiftinterface and abi.json
2022-11-18 15:47:03 +01:00
Bartosz Polaczyk 73d7a13246 Fix linter issue 2022-11-14 20:27:45 -08:00
Bartosz Polaczyk 75fdd27a5f Add unit tests for SPI changes in Xcode14 2022-11-14 20:00:18 -08:00
Bartosz Polaczyk 2fa1f4e927 Update SwiftmoduleFileExtension.swift 2022-11-14 06:34:45 +01:00
Bartosz Polaczyk 0a64893489 Bundle optional private.swiftinterface 2022-11-14 06:28:10 +01:00
Vadim Smal 56850cf2b0 Merge pull request #167 from CognitiveDisson/catalog-info
Add spotify OSS maintainer metadata
2022-09-01 15:56:55 +01:00
Vadim Smal cef92d2f0b Add spotify OSS maintainer metadata 2022-09-01 12:23:49 +01:00
Bartosz Polaczyk 816a9c07c3 Merge pull request #149 from polac24/20220606-publish-swifth-md5
Support exposing enums from ObjC via Bridging headers
2022-08-25 09:53:50 +02:00
Vadim Smal 1e741bc859 Merge pull request #159 from polac24/update-docs-readme
Update Docs link in Readme
2022-08-24 11:13:35 +01:00
Vadim Smal 398b9b11e4 Merge pull request #161 from CognitiveDisson/features/add-retry-logic-for-download
Add retry logic for download
2022-08-24 09:26:18 +01:00
Vadim Smal cb76934ca2 Add retry logic for download 2022-08-11 18:06:20 +01:00
Bartosz Polaczyk aa92805e14 Update Docs link in Readme 2022-08-02 20:22:15 +02:00
Vadim Smal d6355074b2 Merge pull request #158 from CognitiveDisson/maxConcurrentRequests
Add upload_batch_size
2022-08-02 13:59:00 +01:00
Vadim Smal 070e671ddb Merge pull request #156 from polac24/20220711-fix-incremental
[CocoaPodsPlugin] Regenerate cached projects when XCRC is finally on
2022-07-31 21:25:52 +01:00
Vadim Smal 4efdbabf3e Check retryDelay and uploadBatchSize reading from rcinfo 2022-07-20 12:54:51 +01:00
Vadim Smal f33819da60 Rename max_concurrent_requests to upload_batch_size 2022-07-19 17:58:47 +01:00
Vadim Smal f16e6b06f3 Add maxConcurrentRequests 2022-07-19 10:22:09 +01:00
Bartosz Polaczyk 4262620c57 Update Sources/XCRemoteCache/Artifacts/ArtifactProcessor.swift
Co-authored-by: PatrikBillgren <PatrikBillgren@users.noreply.github.com>
2022-07-15 16:03:54 +02:00
Bartosz Polaczyk 36fc5ae1e4 Bump plugin version 2022-07-11 22:49:13 +02:00
Bartosz Polaczyk 2d7c881b3b Do not skip install cache invalidation in consumer 2022-07-11 22:48:45 +02:00
Bartosz Polaczyk aed4a124cd [CocoaPodsPlugin] Regenerate cached projects when XCRC is finally enabled 2022-07-11 21:10:57 +02:00
Bartosz Polaczyk 2a5c6edfce More cleanup 2022-06-20 21:28:17 +02:00
Bartosz Polaczyk 7a1703e70f Pre-review cleanup 2022-06-20 21:24:47 +02:00
Bartosz Polaczyk 8222478817 Delete previous -Swift.h.md5 2022-06-07 08:53:53 -04:00
Bartosz Polaczyk 7a1b5267bf Merge remote-tracking branch 'upstream/master' into 20220606-publish-swifth-md5 2022-06-07 08:52:44 -04:00
Bartosz Polaczyk 2bd50e1c19 Delete old override for .h 2022-06-07 08:49:17 -04:00
Bartosz Polaczyk 6d0fd51c8e Add docs 2022-06-06 18:15:58 -04:00
Bartosz Polaczyk 65a16d0964 Use -Swift.h.md5 placeholder if available 2022-06-06 18:02:47 -04:00
Bartosz Polaczyk a152d9b159 Add support for generating md5 for -Swift.h 2022-06-06 18:01:27 -04:00
52 changed files with 1061 additions and 71 deletions
+37 -1
View File
@@ -8,7 +8,7 @@ _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://spotify.github.io/XCRemoteCache/documentation/xcremotecache/)](https://github.com/spotify/XCRemoteCache/workflows/Docs/badge.svg)
[![Docs](https://github.com/spotify/XCRemoteCache/workflows/Docs/badge.svg)](https://spotify.github.io/XCRemoteCache/documentation/xcremotecache/)
- [How and Why?](#how-and-why)
* [Accurate target input files](#accurate-target-input-files)
@@ -269,6 +269,41 @@ That command creates an empty file on a remote server which informs that for giv
_Note that for the `producer` mode, the prebuild build phase and `xccc`, `xcld`, `xcldplusplus`, `xclibtool` wrappers become no-op, so it is recommended to not add them for the `producer` mode._
##### 7. Generalize `-Swift.h` (Optional only if using static library with a bridging header with public `NS_ENUM` exposed from ObjC)
If a static library target contains a mixed target with a bridging header exposing an enum from ObjC in a public Swift API, your custom script that moves `*-Swift.h` to the shared location, it should also move `*-Swift.h.md5` next to it.
Example:
##### Existing script (Before):
```shell
ditto "${SCRIPT_INPUT_FILE_0}" "${SCRIPT_OUTPUT_FILE_0}"
```
where
* `SCRIPT_INPUT_FILE_0="$(DERIVED_SOURCES_DIR)/$(SWIFT_OBJC_INTERFACE_HEADER_NAME)"`
* `SCRIPT_OUTPUT_FILE_0="$(BUILT_PRODUCTS_DIR)/include/$(PRODUCT_MODULE_NAME)/$(SWIFT_OBJC_INTERFACE_HEADER_NAME)"`
##### Correct script (After):
```shell
ditto "${SCRIPT_INPUT_FILE_0}" "${SCRIPT_OUTPUT_FILE_0}"
[ -f "${SCRIPT_INPUT_FILE_1}" ] && ditto "${SCRIPT_INPUT_FILE_1}" "${SCRIPT_OUTPUT_FILE_1}" || rm "${SCRIPT_OUTPUT_FILE_1}"
```
where
* `SCRIPT_INPUT_FILE_0="$(DERIVED_SOURCES_DIR)/$(SWIFT_OBJC_INTERFACE_HEADER_NAME)"`
* `SCRIPT_INPUT_FILE_1="$(DERIVED_SOURCES_DIR)/$(SWIFT_OBJC_INTERFACE_HEADER_NAME).md5"`
* `SCRIPT_OUTPUT_FILE_0="$(BUILT_PRODUCTS_DIR)/include/$(PRODUCT_MODULE_NAME)/$(SWIFT_OBJC_INTERFACE_HEADER_NAME)"`
* `SCRIPT_OUTPUT_FILE_1="$(BUILT_PRODUCTS_DIR)/include/$(PRODUCT_MODULE_NAME)/$(SWIFT_OBJC_INTERFACE_HEADER_NAME).md5"`
Note: This step is not required if at least one of these is true:
* you build a framework (not a static library)
* you don't expose `NS_ENUM` type from ObjC to Swift via a bridging header
## A full list of configuration parameters:
| Property | Description | Default | Required |
@@ -299,6 +334,7 @@ _Note that for the `producer` mode, the prebuild build phase and `xccc`, `xcld`,
| `download_retries` | Number of retries for download requests | `0` | ⬜️ |
| `upload_retries` | Number of retries for upload requests | `3` | ⬜️ |
| `retry_delay` | Delay between retries in seconds | `10` | ⬜️ |
| `upload_batch_size` | Maximum number of simultaneous requests. 0 means no limits | `0` | ⬜️ |
| `request_custom_headers` | Dictionary of extra HTTP headers for all remote server requests | `[]` | ⬜️ |
| `thin_target_mock_filename` | Filename (without an extension) of the compilation input file that is used as a fake compilation for the forced-cached target (aka thin target) | `standin` | ⬜️ |
| `focused_targets` | A list of all targets that are not thinned. If empty, all targets are meant to be non-thin | `[]` | ⬜️ |
+16
View File
@@ -72,6 +72,22 @@ task :build, [:configuration, :arch, :sdks, :is_archive] do |task, args|
end
end
desc 'Build release artifacts'
task :prepare_release do
system("rm -rf releases && rm -rf tmp")
Rake::Task['build'].invoke("release", "x86_64-apple-macosx")
system("mkdir -p tmp && unzip releases/XCRemoteCache.zip -d tmp/xcremotecache-x86_64")
system("rm -rf releases")
Rake::Task['build'].invoke("release", "arm64-apple-macosx")
system("rake 'build[release, arm64-apple-macosx]'")
system("mkdir -p tmp && unzip releases/XCRemoteCache.zip -d tmp/xcremotecache-arm64")
system("rm -rf releases")
system("mkdir -p releases && zip -jr releases/XCRemoteCache-macOS-x86_64.zip LICENSE README.md tmp/xcremotecache-x86_64")
system("zip -jr releases/XCRemoteCache-macOS-arm64.zip LICENSE README.md tmp/xcremotecache-arm64")
system("mkdir -p tmp/xcremotecache && ls tmp/xcremotecache-x86_64 | xargs -I {} lipo -create -output tmp/xcremotecache/{} tmp/xcremotecache-x86_64/{} tmp/xcremotecache-arm64/{}")
system("zip -jr releases/XCRemoteCache-macOS-arm64-x86_64.zip LICENSE README.md tmp/xcremotecache")
end
desc 'run tests with SPM'
task :test do
# Running tests
@@ -42,6 +42,7 @@ class BuildArtifactCreator: ArtifactSwiftProductsBuilderImpl, ArtifactCreator {
private let modulesFolderPath: String
private let dSYMPath: URL
private let metaWriter: MetaWriter
private let artifactProcessor: ArtifactProcessor
private let fileManager: FileManager
init(
@@ -52,6 +53,7 @@ class BuildArtifactCreator: ArtifactSwiftProductsBuilderImpl, ArtifactCreator {
modulesFolderPath: String,
dSYMPath: URL,
metaWriter: MetaWriter,
artifactProcessor: ArtifactProcessor,
fileManager: FileManager
) {
self.buildDir = buildDir
@@ -62,6 +64,7 @@ class BuildArtifactCreator: ArtifactSwiftProductsBuilderImpl, ArtifactCreator {
self.fileManager = fileManager
self.dSYMPath = dSYMPath
self.metaWriter = metaWriter
self.artifactProcessor = artifactProcessor
super.init(workingDir: tempDir, moduleName: moduleName, fileManager: fileManager)
}
@@ -87,6 +90,7 @@ class BuildArtifactCreator: ArtifactSwiftProductsBuilderImpl, ArtifactCreator {
/// - Parameter tempDir: Temp location to organize file hierarchy in the artifact
/// - returns: URLs to include into the artifact package
fileprivate func prepareSwiftArtifacts(tempDir: URL) throws -> [URL] {
try artifactProcessor.process(localArtifact: tempDir)
var artifacts: [URL] = []
// Add optional directory with generated ObjC headers
@@ -31,7 +31,7 @@ enum ArtifactOrganizerLocationPreparationResult: Equatable {
case preparedForArtifact(artifact: URL)
}
/// Prepares .zip artifact for the local operations
/// Prepares existing .zip artifact for the local operations
protocol ArtifactOrganizer {
/// Prepares the location for the artifact unzipping
/// - Parameter fileKey: artifact fileKey that corresponds to the zip filename on the remote cache server
@@ -48,10 +48,13 @@ protocol ArtifactOrganizer {
class ZipArtifactOrganizer: ArtifactOrganizer {
private let cacheDir: URL
// all processors that should "prepare" the unzipped raw artifact
private let artifactProcessors: [ArtifactProcessor]
private let fileManager: FileManager
init(targetTempDir: URL, fileManager: FileManager) {
init(targetTempDir: URL, artifactProcessors: [ArtifactProcessor], fileManager: FileManager) {
cacheDir = targetTempDir.appendingPathComponent("xccache")
self.artifactProcessors = artifactProcessors
self.fileManager = fileManager
}
@@ -93,6 +96,10 @@ class ZipArtifactOrganizer: ArtifactOrganizer {
// when the command was interrupted (internal crash or `kill -9` signal)
let tempDestination = destinationURL.appendingPathExtension("tmp")
try Zip.unzipFile(artifact, destination: tempDestination, overwrite: true, password: nil)
try artifactProcessors.forEach { processor in
try processor.process(rawArtifact: tempDestination)
}
try fileManager.moveItem(at: tempDestination, to: destinationURL)
return destinationURL
}
@@ -0,0 +1,80 @@
// Copyright (c) 2022 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
/// Performs a pre/postprocessing on an artifact package
/// Could be a place for file reorganization (to support legacy package formats) and/or
/// remapp absolute paths in some package files
protocol ArtifactProcessor {
/// Processes a raw artifact in a directory. Raw artifact is a format of an artifact
/// that is stored in a remote cache server (generic)
/// - Parameter rawArtifact: directory that contains raw artifact content
func process(rawArtifact: URL) throws
/// Processes a local artifact in a directory
/// - Parameter localArtifact: directory that contains local (machine-specific) artifact content
func process(localArtifact: URL) throws
}
/// Processes downloaded artifact by replacing generic paths in generated ObjC headers placed in ./include
class UnzippedArtifactProcessor: ArtifactProcessor {
/// All directories in an artifact that should be processed by path remapping
private static let remappingDirs = ["include"]
private let fileRemapper: FileDependenciesRemapper
private let dirScanner: DirScanner
init(fileRemapper: FileDependenciesRemapper, dirScanner: DirScanner) {
self.fileRemapper = fileRemapper
self.dirScanner = dirScanner
}
private func findProcessingEligableFiles(path: String) throws -> [URL] {
let remappingURL = URL(fileURLWithPath: path)
let allFiles = try dirScanner.recursiveItems(at: remappingURL)
return allFiles.filter({ !$0.isHidden })
}
/// Replaces all generic paths in a raw artifact's `include` dir with
/// absolute paths, specific for a given machine and configuration
/// - Parameter rawArtifact: raw artifact location
func process(rawArtifact url: URL) throws {
for remappingDir in Self.remappingDirs {
let remappingPath = url.appendingPathComponent(remappingDir).path
let allFiles = try findProcessingEligableFiles(path: remappingPath)
try allFiles.forEach(fileRemapper.remap(fromGeneric:))
}
}
func process(localArtifact url: URL) throws {
for remappingDir in Self.remappingDirs {
let remappingPath = url.appendingPathComponent(remappingDir).path
let allFiles = try findProcessingEligableFiles(path: remappingPath)
try allFiles.forEach(fileRemapper.remap(fromLocal:))
}
}
}
fileprivate extension URL {
// Recognize hidden files starting with a dot
var isHidden: Bool {
lastPathComponent.hasPrefix(".")
}
}
@@ -88,7 +88,7 @@ class ArtifactSwiftProductsBuilderImpl: ArtifactSwiftProductsBuilder {
throw ArtifactSwiftProductsBuilderError.populatingNonExistingObjCHeader
}
try fileManager.createDirectory(at: moduleObjCURL, withIntermediateDirectories: true, attributes: nil)
try fileManager.spt_forceLinkItem(at: headerURL, to: headerArtifactURL)
try fileManager.spt_forceCopyItem(at: headerURL, to: headerArtifactURL)
}
func includeModuleDefinitionsToTheArtifact(arch: String, moduleURL: URL) throws {
@@ -0,0 +1,81 @@
// Copyright (c) 2022 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
enum FileDependenciesRemapperError: Error {
/// Thrown when the file to remap is invalid (e.g. doesn't exist or has unexpected format)
case invalidRemappingFile(URL)
}
/// Replaces paths in a file content between generic (placeholder-based)
/// and local formats
protocol FileDependenciesRemapper {
/// Replaces all generic paths (with placeholders) to a local, machine
/// specific absolute paths
/// - Parameter url: location of a file that should be remapped in-place
func remap(fromGeneric url: URL) throws
/// Replaces all local, machine specific absolute paths to
/// generic ones
/// - Parameter url: location of a file that should be remapped in-place
func remap(fromLocal url: URL) throws
}
/// Remaps absolute paths in a text files stored on a disk
/// Note: That class can be used only for text-based files, not binaries
class TextFileDependenciesRemapper: FileDependenciesRemapper {
private static let linesSeparator = "\n"
private let remapper: DependenciesRemapper
private let fileAccessor: FileAccessor
init(remapper: DependenciesRemapper, fileAccessor: FileAccessor) {
self.remapper = remapper
self.fileAccessor = fileAccessor
}
private func readFileLines(_ url: URL) throws -> [String] {
guard let content = try fileAccessor.contents(atPath: url.path) else {
// the file is empty
return []
}
guard let contentString = String(data: content, encoding: .utf8) else {
throw FileDependenciesRemapperError.invalidRemappingFile(url)
}
return contentString.components(separatedBy: .newlines)
}
private func storeFileLines(lines: [String], url: URL) throws {
let contentString = lines.joined(separator: "\n")
let contentData = contentString.data(using: String.Encoding.utf8)
try fileAccessor.write(toPath: url.path, contents: contentData)
}
func remap(fromGeneric url: URL) throws {
let contentLines = try readFileLines(url)
let remappedContent = try remapper.replace(genericPaths: contentLines)
try storeFileLines(lines: remappedContent, url: url)
}
func remap(fromLocal url: URL) throws {
let contentLines = try readFileLines(url)
let remappedContent = try remapper.replace(localPaths: contentLines)
try storeFileLines(lines: remappedContent, url: url)
}
}
@@ -31,6 +31,8 @@ enum SwiftmoduleFileExtension: String {
case swiftdoc
case swiftsourceinfo
case swiftinterface
case privateSwiftinterface = "private.swiftinterface"
case abiJson = "abi.json"
}
extension SwiftmoduleFileExtension {
@@ -40,5 +42,7 @@ extension SwiftmoduleFileExtension {
.swiftdoc: .required,
.swiftsourceinfo: .optional,
.swiftinterface: .optional,
.privateSwiftinterface: .optional,
.abiJson: .optional,
]
}
@@ -27,13 +27,19 @@ protocol ThinningConsumerArtifactsOrganizerFactory {
}
class ThinningConsumerZipArtifactsOrganizerFactory: ThinningConsumerArtifactsOrganizerFactory {
private let processors: [ArtifactProcessor]
private let fileManager: FileManager
init(fileManager: FileManager) {
init(processors: [ArtifactProcessor], fileManager: FileManager) {
self.processors = processors
self.fileManager = fileManager
}
func build(targetTempDir: URL) -> ArtifactOrganizer {
ZipArtifactOrganizer(targetTempDir: targetTempDir, fileManager: fileManager)
ZipArtifactOrganizer(
targetTempDir: targetTempDir,
artifactProcessors: processors,
fileManager: fileManager
)
}
}
@@ -58,7 +58,7 @@ class ThinningDiskSwiftcProductsGenerator: SwiftcProductsGenerator {
func generateFrom(
artifactSwiftModuleFiles sourceAtifactSwiftModuleFiles: [SwiftmoduleFileExtension: URL],
artifactSwiftModuleObjCFile: URL
) throws -> URL {
) throws -> SwiftcProductsGeneratorOutput {
// Move cached -Swift.h file to the expected location
try diskCopier.copy(file: artifactSwiftModuleObjCFile, destination: objcHeaderOutput)
for (ext, url) in sourceAtifactSwiftModuleFiles {
@@ -79,6 +79,9 @@ class ThinningDiskSwiftcProductsGenerator: SwiftcProductsGenerator {
}
// Build parent dir of the .swiftmodule file that contains a module
return modulePathOutput.deletingLastPathComponent()
return SwiftcProductsGeneratorOutput(
swiftmoduleDir: modulePathOutput.deletingLastPathComponent(),
objcHeaderFile: objcHeaderOutput
)
}
}
@@ -73,11 +73,12 @@ class UnzippedArtifactSwiftProductsOrganizer: SwiftProductsOrganizer {
.appendingPathComponent(moduleName)
.appendingPathComponent("\(moduleName)-Swift.h")
let generatedModuleDir = try productsGenerator.generateFrom(
let generatedModule = try productsGenerator.generateFrom(
artifactSwiftModuleFiles: artifactSwiftmoduleFiles,
artifactSwiftModuleObjCFile: artifactSwiftModuleObjCFile
)
try fingerprintSyncer.decorate(sourceDir: generatedModuleDir, fingerprint: fingerprint)
try fingerprintSyncer.decorate(sourceDir: generatedModule.swiftmoduleDir, fingerprint: fingerprint)
try fingerprintSyncer.decorate(file: generatedModule.objcHeaderFile, fingerprint: fingerprint)
}
}
@@ -238,13 +238,32 @@ class Postbuild {
let moduleSwiftProductURL = context.productsDir
.appendingPathComponent(context.modulesFolderPath)
.appendingPathComponent("\(modulename).swiftmodule")
let objcHeaderSwiftProductURL = context.derivedSourcesDir
.appendingPathComponent("\(modulename)-Swift.h")
// This header is obly valid if building a frameworks
let objcHeaderSwiftPublicPathURL = context.publicHeadersFolderPath?
.appendingPathComponent("\(modulename)-Swift.h")
if let fingerprint = contextSpecificFingerprint {
try fingerprintSyncer.decorate(
sourceDir: moduleSwiftProductURL,
fingerprint: fingerprint
)
try fingerprintSyncer.decorate(
file: objcHeaderSwiftProductURL,
fingerprint: fingerprint
)
if let objcPublic = objcHeaderSwiftPublicPathURL {
try fingerprintSyncer.decorate(
file: objcPublic,
fingerprint: fingerprint
)
}
} else {
try fingerprintSyncer.delete(sourceDir: moduleSwiftProductURL)
try fingerprintSyncer.delete(sourceDir: objcHeaderSwiftProductURL)
if let objcPublic = objcHeaderSwiftPublicPathURL {
try fingerprintSyncer.delete(file: objcPublic)
}
}
}
}
@@ -74,7 +74,7 @@ public struct PostbuildContext {
let builtProductsDir: URL
/// Location to the product bundle. Can be nil for libraries
let bundleDir: URL?
let derivedSourcesDir: URL
var derivedSourcesDir: URL
/// List of all targets to downloaded from the thinning aggregation target
var thinnedTargets: [String]
/// Action type: build, indexbuild etc
@@ -85,6 +85,8 @@ public struct PostbuildContext {
let overlayHeadersPath: URL
/// Regexes of files that should not be included in the dependency list
let irrelevantDependenciesPaths: [String]
/// Location of public headers. Not always available (e.g. static libraries)
var publicHeadersFolderPath: URL?
}
extension PostbuildContext {
@@ -138,5 +140,11 @@ extension PostbuildContext {
/// Note: The file has yaml extension, even it is in the json format
overlayHeadersPath = targetTempDir.appendingPathComponent("all-product-headers.yaml")
irrelevantDependenciesPaths = config.irrelevantDependenciesPaths
let publicHeadersPath: String = try env.readEnv(key: "PUBLIC_HEADERS_FOLDER_PATH")
if publicHeadersPath != "/usr/local/include" {
// '/usr/local/include' is a value of PUBLIC_HEADERS_FOLDER_PATH when no public headers are automatically
// generated and it is up to a project configuration to place it in a common location (e.g. static library)
publicHeadersFolderPath = builtProductsDir.appendingPathComponent(publicHeadersPath)
}
}
}
@@ -87,8 +87,14 @@ public class XCPostbuild {
fingerprintFilesGenerator,
algorithm: MD5Algorithm()
)
let organizer = ZipArtifactOrganizer(targetTempDir: context.targetTempDir, fileManager: fileManager)
let organizer = ZipArtifactOrganizer(
targetTempDir: context.targetTempDir,
// In postbuild we don't preprocess artifacts (no need to replace path placeholders)
artifactProcessors: [],
fileManager: fileManager
)
let metaWriter = JsonMetaWriter(fileWriter: fileManager, pretty: config.prettifyMetaFiles)
let fileRemapper = TextFileDependenciesRemapper(remapper: envsRemapper, fileAccessor: fileManager)
let artifactCreator = BuildArtifactCreator(
buildDir: context.productsDir,
tempDir: context.targetTempDir,
@@ -97,6 +103,7 @@ public class XCPostbuild {
modulesFolderPath: context.modulesFolderPath,
dSYMPath: context.dSYMPath,
metaWriter: metaWriter,
artifactProcessor: UnzippedArtifactProcessor(fileRemapper: fileRemapper, dirScanner: fileManager),
fileManager: fileManager
)
let dirAccessor = DirAccessorComposer(
@@ -131,6 +138,7 @@ public class XCPostbuild {
mode: context.mode,
downloadStreamURL: context.recommendedCacheAddress,
upstreamStreamURL: context.cacheAddresses,
uploadBatchSize: config.uploadBatchSize,
networkClient: networkClient,
urlBuilderFactory: {
try URLBuilderImpl(
@@ -201,7 +209,10 @@ public class XCPostbuild {
if context.moduleName == config.thinningTargetModuleName {
switch context.mode {
case .consumer:
// no need to process artifacts in postbuild. Prebuild has already
// run a processor on a downloaded artifact
let artifactOrganizerFactory = ThinningConsumerZipArtifactsOrganizerFactory(
processors: [],
fileManager: fileManager
)
let swiftProductsLocationProvider =
@@ -152,13 +152,25 @@ public class XCPrebuild {
filesFingerprintGenerator,
algorithm: MD5Algorithm()
)
let organizer = ZipArtifactOrganizer(targetTempDir: context.targetTempDir, fileManager: fileManager)
let fileRemapper = TextFileDependenciesRemapper(
remapper: envsRemapper,
fileAccessor: fileManager
)
let artifactProcessor = UnzippedArtifactProcessor(fileRemapper: fileRemapper, dirScanner: fileManager)
let organizer = ZipArtifactOrganizer(
targetTempDir: context.targetTempDir,
artifactProcessors: [artifactProcessor],
fileManager: fileManager
)
let metaReader = JsonMetaReader(fileAccessor: fileManager)
var consumerPlugins: [ArtifactConsumerPrebuildPlugin] = []
if config.thinningEnabled {
if context.moduleName == config.thinningTargetModuleName, let thinnedTarget = context.thinnedTargets {
let organizerFactory = ThinningConsumerZipArtifactsOrganizerFactory(fileManager: .default)
let organizerFactory = ThinningConsumerZipArtifactsOrganizerFactory(
processors: [artifactProcessor],
fileManager: fileManager
)
let aggregationPlugin = ThinningConsumerPrebuildPlugin(
targetName: context.targetName,
tempDir: context.targetTempDir,
@@ -77,6 +77,7 @@ public class XCPrepareMark {
mode: .producer,
downloadStreamURL: context.recommendedCacheAddress,
upstreamStreamURL: context.cacheAddresses,
uploadBatchSize: config.uploadBatchSize,
networkClient: networkClient
) { [configuration, platform] cacheAddress in
// Prepare URLs don't include target name or envFingperint, which are valid only for a target level
@@ -78,7 +78,12 @@ public class XCCreateBinary {
}
let markerURL = tempDir.appendingPathComponent(config.modeMarkerPath)
do {
let organizer = ZipArtifactOrganizer(targetTempDir: tempDir, fileManager: fileManager)
let organizer = ZipArtifactOrganizer(
targetTempDir: tempDir,
// Creation binary doesn't call artifact preprocessing
artifactProcessors: [],
fileManager: fileManager
)
let dependenciesWriter = FileDatWriter(dependencyInfo, fileManager: fileManager)
let markerReader = FileMarkerReader(markerURL, fileManager: fileManager)
guard fileManager.fileExists(atPath: markerURL.path) else {
@@ -55,7 +55,7 @@ class MirroredLinkingSwiftcProductsGenerator: SwiftcProductsGenerator {
func generateFrom(
artifactSwiftModuleFiles: [SwiftmoduleFileExtension: URL],
artifactSwiftModuleObjCFile: URL
) throws -> URL {
) throws -> SwiftcProductsGeneratorOutput {
/// Predict moduleName from the `*.swiftmodule` artifact
let foundSwiftmoduleFile = artifactSwiftModuleFiles[.swiftmodule]
guard let mainSwiftmoduleFile = foundSwiftmoduleFile else {
@@ -26,14 +26,19 @@ enum DiskSwiftcProductsGeneratorError: Error {
case unknownSwiftmoduleFile
}
struct SwiftcProductsGeneratorOutput {
let swiftmoduleDir: URL
let objcHeaderFile: URL
}
/// Generates swiftc product to the expected location
protocol SwiftcProductsGenerator {
/// Generates products from given files
/// - Returns: location dir where .swiftmodule files have been placed
/// - Returns: location dir where .swiftmodule and ObjC header files have been placed
func generateFrom(
artifactSwiftModuleFiles: [SwiftmoduleFileExtension: URL],
artifactSwiftModuleObjCFile: URL
) throws -> URL
) throws -> SwiftcProductsGeneratorOutput
}
/// Generator that produces all products in the locations where Xcode expects it, using provided disk copier
@@ -64,7 +69,7 @@ class DiskSwiftcProductsGenerator: SwiftcProductsGenerator {
func generateFrom(
artifactSwiftModuleFiles sourceAtifactSwiftModuleFiles: [SwiftmoduleFileExtension: URL],
artifactSwiftModuleObjCFile: URL
) throws -> URL {
) throws -> SwiftcProductsGeneratorOutput {
// Move cached -Swift.h file to the expected location
try diskCopier.copy(file: artifactSwiftModuleObjCFile, destination: objcHeaderOutput)
for (ext, url) in sourceAtifactSwiftModuleFiles {
@@ -85,6 +90,9 @@ class DiskSwiftcProductsGenerator: SwiftcProductsGenerator {
}
// Build parent dir of the .swiftmodule file that contains a module
return modulePathOutput.deletingLastPathComponent()
return SwiftcProductsGeneratorOutput(
swiftmoduleDir: modulePathOutput.deletingLastPathComponent(),
objcHeaderFile: objcHeaderOutput
)
}
}
@@ -83,7 +83,12 @@ public class XCSwiftc {
let inputReader = SwiftcFilemapInputEditor(context.filemap, fileManager: fileManager)
let fileListEditor = FileListEditor(context.fileList, fileManager: fileManager)
let artifactOrganizer = ZipArtifactOrganizer(targetTempDir: context.tempDir, fileManager: fileManager)
let artifactOrganizer = ZipArtifactOrganizer(
targetTempDir: context.tempDir,
// xcswiftc doesn't call artifact preprocessing
artifactProcessors: [],
fileManager: fileManager
)
// TODO: check for allowedFile comparing a list of all inputfiles, not dependencies from a marker
let makerReferencedFilesListScanner = FileListScannerImpl(markerReader, caseSensitive: false)
let allowedFilesListScanner = ExceptionsFilteredFileListScanner(
@@ -85,6 +85,8 @@ public struct XCRemoteCacheConfig: Encodable {
var uploadRetries: Int = 3
/// Delay between retries in seconds
var retryDelay: Double = 10.0
/// Maximum number of simultaneous requests. 0 means no limits
var uploadBatchSize: Int = 0
/// Extra headers appended to all remote HTTP(S) requests
var requestCustomHeaders: [String: String] = [:]
/// Filename (without an extension) of the compilation input file that is used
@@ -109,7 +111,8 @@ public struct XCRemoteCacheConfig: Encodable {
var turnOffRemoteCacheOnFirstTimeout: Bool = false
/// List of all extensions that should carry over source fingerprints. Extensions of all product files that
/// contain non-deterministic content (absolute paths, timestamp, etc) should be included
var productFilesExtensionsWithContentOverride = ["swiftmodule"]
/// .h files may contain absolute paths if NS_ENUM is used in a public API from Swift code
var productFilesExtensionsWithContentOverride = ["swiftmodule", "h"]
/// If true, plugins for thinning support should be enabled
var thinningEnabled: Bool = false
/// Module name of a target that works as a helper for thinned targets
@@ -178,6 +181,7 @@ extension XCRemoteCacheConfig {
merge.downloadRetries = scheme.downloadRetries ?? downloadRetries
merge.uploadRetries = scheme.uploadRetries ?? uploadRetries
merge.retryDelay = scheme.retryDelay ?? retryDelay
merge.uploadBatchSize = scheme.uploadBatchSize ?? uploadBatchSize
merge.requestCustomHeaders = scheme.requestCustomHeaders ?? requestCustomHeaders
merge.thinTargetMockFilename = scheme.thinTargetMockFilename ?? thinTargetMockFilename
merge.focusedTargets = scheme.focusedTargets ?? focusedTargets
@@ -247,6 +251,7 @@ struct ConfigFileScheme: Decodable {
let downloadRetries: Int?
let uploadRetries: Int?
let retryDelay: Double?
let uploadBatchSize: Int?
let requestCustomHeaders: [String: String]?
let thinTargetMockFilename: String?
let focusedTargets: [String]?
@@ -296,6 +301,7 @@ struct ConfigFileScheme: Decodable {
case downloadRetries = "download_retries"
case uploadRetries = "upload_retries"
case retryDelay = "retry_delay"
case uploadBatchSize = "upload_batch_size"
case requestCustomHeaders = "request_custom_headers"
case thinTargetMockFilename = "thin_target_mock_filename"
case focusedTargets = "focused_targets"
@@ -30,6 +30,10 @@ protocol FingerprintSyncer {
func decorate(sourceDir: URL, fingerprint: String) throws
/// Deletes fingerprint overrides in the dir (if already created)
func delete(sourceDir: URL) throws
/// Sets a fingerprint override for a singe file placed
func decorate(file: URL, fingerprint: String) throws
/// Deletes fingerprint override for a file (if already created)
func delete(file: URL) throws
}
class FileFingerprintSyncer: FingerprintSyncer {
@@ -78,4 +82,25 @@ class FileFingerprintSyncer: FingerprintSyncer {
try dirAccessor.removeItem(atPath: file.path)
}
}
func decorate(file: URL, fingerprint: String) throws {
guard let fingerprintData = fingerprint.data(using: .utf8) else {
throw FingerprintSyncerError.invalidFingerprint
}
let fingerprintFile = file.appendingPathExtension(fingerprintExtension)
try dirAccessor.write(toPath: fingerprintFile.path, contents: fingerprintData)
}
func delete(file: URL) throws {
guard case .file = try dirAccessor.itemType(atPath: file.path) else {
// no file to decorate (no module was generated)
return
}
let overrideURL = file.appendingPathExtension(fingerprintExtension)
guard case .file = try dirAccessor.itemType(atPath: overrideURL.path) else {
// no override
return
}
try dirAccessor.removeItem(atPath: overrideURL.path)
}
}
@@ -33,8 +33,13 @@ protocol DirScanner {
/// Returns all items in a directory (shallow search)
/// - Parameter at: url of an existing directory to search
/// - Throws: an error if dir doesn't exist or I/O error
/// - Throws: an error if a dir doesn't exist or on I/O error
func items(at dir: URL) throws -> [URL]
/// Returns all items in a directory (recursive search)
/// - Parameter at: url of an existing directory to search
/// - Throws: an error if a dir doesn't exist or on I/O error
func recursiveItems(at dir: URL) throws -> [URL]
}
typealias DirAccessor = FileAccessor & DirScanner
@@ -54,4 +59,18 @@ extension FileManager: DirScanner {
let resolvedDir = dir.resolvingSymlinksInPath()
return try contentsOfDirectory(at: resolvedDir, includingPropertiesForKeys: nil, options: [])
}
func recursiveItems(at dir: URL) throws -> [URL] {
// Iterating DFS
var queue: [URL] = [dir]
var results: [URL] = []
while let item = queue.popLast() {
if try itemType(atPath: item.path) == .dir {
try queue.append(contentsOf: items(at: item))
} else {
results.append(item)
}
}
return results
}
}
@@ -52,4 +52,8 @@ class DirAccessorComposer: DirAccessor {
func items(at dir: URL) throws -> [URL] {
try dirScanner.items(at: dir)
}
func recursiveItems(at dir: URL) throws -> [URL] {
try dirScanner.recursiveItems(at: dir)
}
}
@@ -77,7 +77,12 @@ class NetworkClientImpl: NetworkClient {
func download(_ url: URL, to location: URL, completion: @escaping (Result<Void, NetworkClientError>) -> Void) {
var request = URLRequest(url: url)
setupAuthenticationSignatureIfPresent(&request)
makeDownloadRequest(request, output: location, completion: completion)
makeDownloadRequest(
request,
output: location,
retries: maxRetries,
completion: completion
)
}
func upload(_ file: URL, as url: URL, completion: @escaping (Result<Void, NetworkClientError>) -> Void) {
@@ -125,7 +130,7 @@ class NetworkClientImpl: NetworkClient {
dataTask.resume()
}
private func makeDownloadRequest(_ request: URLRequest, output: URL, completion: @escaping (Result<Void, NetworkClientError>) -> Void) {
private func makeDownloadRequest(_ request: URLRequest, output: URL, retries: Int, completion: @escaping (Result<Void, NetworkClientError>) -> Void) {
guard fileManager.fileExists(atPath: output.path) == false else {
infoLog("Download file found in the destination, skipping download.")
completion(.success(()))
@@ -135,6 +140,17 @@ class NetworkClientImpl: NetworkClient {
let dataTask = session.downloadTask(with: request) { [fileManager] fileURL, _, error in
guard let fileURL = fileURL else {
let networkError = error.map(NetworkClientError.build) ?? .inconsistentSession
if retries > 0 {
infoLog("Download request failed with \(networkError). Left retries: \(retries).")
self.retryDownload(
request,
output: output,
retries: retries,
completion: completion,
after: self.retryDelay
)
return
}
errorLog("Download request failed: \(networkError)")
completion(.failure(networkError))
return
@@ -196,7 +212,24 @@ class NetworkClientImpl: NetworkClient {
private func retryUpload(_ request: URLRequest, input: URL, retries: Int, completion: @escaping (Result<Void, NetworkClientError>) -> Void, after: TimeInterval) {
DispatchQueue.global().asyncAfter(deadline: .now() + after) { [weak self] in
guard let self = self else { return }
self.makeUploadRequest(request, input: input, retries: retries - 1, completion: completion)
self.makeUploadRequest(
request,
input: input,
retries: retries - 1,
completion: completion
)
}
}
private func retryDownload(_ request: URLRequest, output: URL, retries: Int, completion: @escaping (Result<Void, NetworkClientError>) -> Void, after: TimeInterval) {
DispatchQueue.global().asyncAfter(deadline: .now() + after) { [weak self] in
guard let self = self else { return }
self.makeDownloadRequest(
request,
output: output,
retries: retries - 1,
completion: completion
)
}
}
}
@@ -27,11 +27,20 @@ class RemoteNetworkClientAbstractFactory {
private let upstreamStreamURL: [URL]
private let networkClient: NetworkClient
private let urlBuilderFactory: (URL) throws -> URLBuilder
private let uploadBatchSize: Int
init(mode: Mode, downloadStreamURL: URL, upstreamStreamURL: [URL], networkClient: NetworkClient, urlBuilderFactory: @escaping (URL) throws -> URLBuilder) {
init(
mode: Mode,
downloadStreamURL: URL,
upstreamStreamURL: [URL],
uploadBatchSize: Int,
networkClient: NetworkClient,
urlBuilderFactory: @escaping (URL) throws -> URLBuilder
) {
self.mode = mode
self.downloadStreamURL = downloadStreamURL
self.upstreamStreamURL = upstreamStreamURL
self.uploadBatchSize = uploadBatchSize
self.networkClient = networkClient
self.urlBuilderFactory = urlBuilderFactory
}
@@ -49,7 +58,8 @@ class RemoteNetworkClientAbstractFactory {
return ReplicatedRemotesNetworkClient(
networkClient,
download: downloadURLBuilder,
uploads: upstreamBuilders
uploads: upstreamBuilders,
uploadBatchSize: uploadBatchSize
)
case .consumer:
return RemoteNetworkClientImpl(networkClient, downloadURLBuilder)
@@ -23,10 +23,12 @@ import Foundation
class ReplicatedRemotesNetworkClient: RemoteNetworkClientImpl {
private let networkClient: NetworkClient
private let uploadURLBuilders: [URLBuilder]
private let uploadBatchSize: Int
init(_ networkClient: NetworkClient, download: URLBuilder, uploads uploadURLBuilders: [URLBuilder]) {
init(_ networkClient: NetworkClient, download: URLBuilder, uploads uploadURLBuilders: [URLBuilder], uploadBatchSize: Int) {
self.networkClient = networkClient
self.uploadURLBuilders = uploadURLBuilders
self.uploadBatchSize = uploadBatchSize
super.init(networkClient, download)
}
@@ -39,6 +41,9 @@ class ReplicatedRemotesNetworkClient: RemoteNetworkClientImpl {
let group = DispatchGroup()
var results: [Result<Void, NetworkClientError>] = Array(repeating: .failure(.noResponse), count: urls.count)
urls.enumerated().forEach { index, url in
if uploadBatchSize > 0 && index > 0 && index % uploadBatchSize == 0 {
group.wait()
}
group.enter()
networkClient.upload(file, as: url) { receivedResult in
results[index] = receivedResult
@@ -58,6 +63,9 @@ class ReplicatedRemotesNetworkClient: RemoteNetworkClientImpl {
let group = DispatchGroup()
var results: [Result<Void, NetworkClientError>] = Array(repeating: .failure(.noResponse), count: urls.count)
urls.enumerated().forEach { index, url in
if uploadBatchSize > 0 && index > 0 && index % uploadBatchSize == 0 {
group.wait()
}
group.enter()
networkClient.create(url) { receivedResult in
results[index] = receivedResult
@@ -28,6 +28,8 @@ class ArtifactSwiftProductsBuilderImplTests: FileXCTestCase {
private var swiftmoduleDocFile: URL!
private var swiftmoduleSourceInfoFile: URL!
private var swiftmoduleInterfaceFile: URL!
private var privateSwiftmoduleInterfaceFile: URL!
private var abiJsonFile: URL!
private var workingDir: URL!
private var builder: ArtifactSwiftProductsBuilderImpl!
@@ -39,6 +41,9 @@ class ArtifactSwiftProductsBuilderImplTests: FileXCTestCase {
swiftmoduleDocFile = moduleDir.appendingPathComponent("MyModule.swiftdoc")
swiftmoduleSourceInfoFile = moduleDir.appendingPathComponent("MyModule.swiftsourceinfo")
swiftmoduleInterfaceFile = moduleDir.appendingPathComponent("MyModule.swiftinterface")
privateSwiftmoduleInterfaceFile = moduleDir.appendingPathComponent("MyModule.private.swiftinterface")
abiJsonFile = moduleDir.appendingPathComponent("MyModule.abi.json")
workingDir = rootDir.appendingPathComponent("working")
builder = ArtifactSwiftProductsBuilderImpl(
workingDir: workingDir,
@@ -98,6 +103,8 @@ class ArtifactSwiftProductsBuilderImplTests: FileXCTestCase {
try fileManager.spt_createEmptyFile(swiftmoduleDocFile)
try fileManager.spt_createEmptyFile(swiftmoduleSourceInfoFile)
try fileManager.spt_createEmptyFile(swiftmoduleInterfaceFile)
try fileManager.spt_createEmptyFile(privateSwiftmoduleInterfaceFile)
try fileManager.spt_createEmptyFile(abiJsonFile)
let builderSwiftmoduleDir =
builder
.buildingArtifactSwiftModulesLocation()
@@ -110,6 +117,10 @@ class ArtifactSwiftProductsBuilderImplTests: FileXCTestCase {
builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftsourceinfo")
let expectedBuildedSwiftInterfaceFile =
builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftinterface")
let expectedPrivateSwiftmoduleInterfaceFile =
builderSwiftmoduleDir.appendingPathComponent("MyModule.private.swiftinterface")
let expectedAbiJsonFile =
builderSwiftmoduleDir.appendingPathComponent("MyModule.abi.json")
try builder.includeModuleDefinitionsToTheArtifact(arch: "arm64", moduleURL: swiftmoduleFile)
@@ -117,6 +128,8 @@ class ArtifactSwiftProductsBuilderImplTests: FileXCTestCase {
XCTAssertTrue(fileManager.fileExists(atPath: expectedBuildedSwiftmoduledocFile.path))
XCTAssertTrue(fileManager.fileExists(atPath: expectedBuildedSwiftSourceInfoFile.path))
XCTAssertTrue(fileManager.fileExists(atPath: expectedBuildedSwiftInterfaceFile.path))
XCTAssertTrue(fileManager.fileExists(atPath: expectedPrivateSwiftmoduleInterfaceFile.path))
XCTAssertTrue(fileManager.fileExists(atPath: expectedAbiJsonFile.path))
}
func testFailsIncludingWhenMissingRequiredSwiftmoduleFiles() throws {
@@ -32,6 +32,8 @@ class BuildArtifactCreatorTests: FileXCTestCase {
private var swiftdocURL: URL!
private var swiftSourceInfoURL: URL!
private var swiftInterfaceURL: URL!
private var privateSwiftInterfaceURL: URL!
private var abiJsonURL: URL!
private var executablePath: String!
private var executableURL: URL!
private var creator: BuildArtifactCreator!
@@ -53,11 +55,16 @@ class BuildArtifactCreatorTests: FileXCTestCase {
.appendingPathComponent("Target.swiftsourceinfo")
swiftInterfaceURL = workDirectory.appendingPathComponent("Objects-normal")
.appendingPathComponent("Target.swiftinterface")
privateSwiftInterfaceURL = workDirectory.appendingPathComponent("Objects-normal")
.appendingPathComponent("Target.private.swiftinterface")
abiJsonURL = workDirectory.appendingPathComponent("Objects-normal")
.appendingPathComponent("Target.abi.json")
executablePath = "libTarget.a"
executableURL = buildDir.appendingPathComponent(executablePath)
dSYM = executableURL.deletingPathExtension().appendingPathExtension(".dSYM")
try fileManager.spt_createEmptyFile(executableURL)
try fileManager.spt_createEmptyFile(headerURL)
let artifactProcessor = NoopArtifactProcessor()
creator = BuildArtifactCreator(
buildDir: buildDir,
@@ -67,6 +74,7 @@ class BuildArtifactCreatorTests: FileXCTestCase {
modulesFolderPath: "",
dSYMPath: dSYM,
metaWriter: JsonMetaWriter(fileWriter: fileManager, pretty: false),
artifactProcessor: artifactProcessor,
fileManager: fileManager
)
}
@@ -122,6 +130,8 @@ class BuildArtifactCreatorTests: FileXCTestCase {
try fileManager.spt_createEmptyFile(swiftdocURL)
try fileManager.spt_createEmptyFile(swiftSourceInfoURL)
try fileManager.spt_createEmptyFile(swiftInterfaceURL)
try fileManager.spt_createEmptyFile(privateSwiftInterfaceURL)
try fileManager.spt_createEmptyFile(abiJsonURL)
try creator.includeModuleDefinitionsToTheArtifact(arch: "arch", moduleURL: swiftmoduleURL)
let artifact = try creator.createArtifact(artifactKey: "key", meta: sampleMeta)
@@ -136,6 +146,8 @@ class BuildArtifactCreatorTests: FileXCTestCase {
unzippedURL.appendingPathComponent("swiftmodule/arch/Target.swiftdoc"),
unzippedURL.appendingPathComponent("swiftmodule/arch/Target.swiftsourceinfo"),
unzippedURL.appendingPathComponent("swiftmodule/arch/Target.swiftinterface"),
unzippedURL.appendingPathComponent("swiftmodule/arch/Target.private.swiftinterface"),
unzippedURL.appendingPathComponent("swiftmodule/arch/Target.abi.json"),
])
}
@@ -0,0 +1,101 @@
// Copyright (c) 2022 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
@testable import XCRemoteCache
import XCTest
class TextFileDependenciesRemapperTests: FileXCTestCase {
let stringsRemapper = StringDependenciesRemapper(
mappings: [
.init(generic: "$(SRCROOT)", local: "/example"),
])
let fileAccessor = FileAccessorFake(mode: .strict)
var remapper: TextFileDependenciesRemapper!
override func setUp() {
super.setUp()
remapper = TextFileDependenciesRemapper(
remapper: stringsRemapper,
fileAccessor: fileAccessor
)
}
func testRemapsGenericPlaceholders() throws {
try fileAccessor.write(toPath: "/file", contents: "Some $(SRCROOT).")
try remapper.remap(fromGeneric: "/file")
try XCTAssertEqual(fileAccessor.contents(atPath: "/file"), "Some /example.")
}
func testRemapsLocalPathsToPlaceholders() throws {
try fileAccessor.write(toPath: "/file", contents: "Some /example.")
try remapper.remap(fromLocal: "/file")
try XCTAssertEqual(fileAccessor.contents(atPath: "/file"), "Some $(SRCROOT).")
}
func testPersistsEmptyLines() throws {
let multilineData = """
Line1
Line 2
""".data(using: .utf8)
try fileAccessor.write(toPath: "/file", contents: multilineData)
try remapper.remap(fromGeneric: "/file")
try XCTAssertEqual(fileAccessor.contents(atPath: "/file"), multilineData)
}
func testPersistsEmptyLineAtTheEnd() throws {
// swiftlint:disable trailing_whitespace
let multilineData = """
Line1
Line 2
""".data(using: .utf8)
// swiftlint:enable trailing_whitespace
try fileAccessor.write(toPath: "/file", contents: multilineData)
try remapper.remap(fromGeneric: "/file")
try XCTAssertEqual(fileAccessor.contents(atPath: "/file"), multilineData)
}
func testReplacesMultipletimesInLine() throws {
try fileAccessor.write(toPath: "/file", contents: "$(SRCROOT) and $(SRCROOT)")
try remapper.remap(fromGeneric: "/file")
try XCTAssertEqual(fileAccessor.contents(atPath: "/file"), "/example and /example")
}
func testReplacesInMultipleLine() throws {
try fileAccessor.write(toPath: "/file", contents: "$(SRCROOT)\n$(SRCROOT)")
try remapper.remap(fromGeneric: "/file")
try XCTAssertEqual(fileAccessor.contents(atPath: "/file"), "/example\n/example")
}
}
@@ -0,0 +1,71 @@
// Copyright (c) 2022 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
@testable import XCRemoteCache
import XCTest
class UnzippedArtifactProcessorTests: FileXCTestCase {
private let fileAccessor = FileAccessorFake(mode: .strict)
private let remapper = StringDependenciesRemapper(mappings: [.init(generic: "$(SRCROOT)", local: "/local")])
private var fileRemapper: FileDependenciesRemapper!
private var processor: UnzippedArtifactProcessor!
override func setUp() {
super.setUp()
fileRemapper = TextFileDependenciesRemapper(remapper: remapper, fileAccessor: fileAccessor)
processor = UnzippedArtifactProcessor(
fileRemapper: fileRemapper,
dirScanner: fileAccessor
)
}
func testProcessingRawArtifactReplacesPlaceholders() throws {
try fileAccessor.write(toPath: "/artifact/include/file", contents: "Some $(SRCROOT)")
try processor.process(rawArtifact: "/artifact")
XCTAssertEqual(try fileAccessor.contents(atPath: "/artifact/include/file"), "Some /local")
}
func testProcessingRawArtifactReplacesInNestedInclude() throws {
try fileAccessor.write(toPath: "/artifact/include/nested/file", contents: "Some $(SRCROOT)")
try processor.process(rawArtifact: "/artifact")
XCTAssertEqual(try fileAccessor.contents(atPath: "/artifact/include/nested/file"), "Some /local")
}
func testProcessingRawArtifactDoesntReplacesInNonIncludeDir() throws {
try fileAccessor.write(toPath: "/artifact/some/file", contents: "Some $(SRCROOT)")
try processor.process(rawArtifact: "/artifact")
XCTAssertEqual(try fileAccessor.contents(atPath: "/artifact/some/file"), "Some $(SRCROOT)")
}
func testDoesntProcessEmptyFiles() throws {
try fileAccessor.write(toPath: "/artifact/include/.hidden", contents: "Some $(SRCROOT)")
try processor.process(rawArtifact: "/artifact")
XCTAssertEqual(try fileAccessor.contents(atPath: "/artifact/include/.hidden"), "Some $(SRCROOT)")
}
}
@@ -54,7 +54,11 @@ class ZipArtifactOrganizerTests: XCTestCase {
func testPreparePlacesArtifactInTheActiveLocation() throws {
let zipURL = try prepareZipFile(content: "Magic", fileName: "content.txt")
let organizer = ZipArtifactOrganizer(targetTempDir: workingDirectory, fileManager: fileManager)
let organizer = ZipArtifactOrganizer(
targetTempDir: workingDirectory,
artifactProcessors: [],
fileManager: fileManager
)
let preparedArtifact = try organizer.prepare(artifact: zipURL)
@@ -64,7 +68,11 @@ class ZipArtifactOrganizerTests: XCTestCase {
func testPreparingExistingArtifact() throws {
let zipURL = try prepareZipFile(content: "Magic", fileName: "content.txt")
let organizer = ZipArtifactOrganizer(targetTempDir: workingDirectory, fileManager: fileManager)
let organizer = ZipArtifactOrganizer(
targetTempDir: workingDirectory,
artifactProcessors: [],
fileManager: fileManager
)
_ = try organizer.prepare(artifact: zipURL)
let preparedArtifact = try organizer.prepare(artifact: zipURL)
@@ -75,7 +83,11 @@ class ZipArtifactOrganizerTests: XCTestCase {
func testPreparePlacesArtifactInTheFileKeyRelatedLocation() throws {
let zipURL = try prepareZipFile(content: "Magic", fileName: "content.txt", zipFileName: "abb32_fileKey")
let organizer = ZipArtifactOrganizer(targetTempDir: workingDirectory, fileManager: fileManager)
let organizer = ZipArtifactOrganizer(
targetTempDir: workingDirectory,
artifactProcessors: [],
fileManager: fileManager
)
let expectedArtifactLocation = workingDirectory.appendingPathComponent("abb32_fileKey")
let preparedArtifact = try organizer.prepare(artifact: zipURL)
@@ -89,7 +101,11 @@ class ZipArtifactOrganizerTests: XCTestCase {
let artifactLocation = workingDirectory.appendingPathComponent("xccache")
.appendingPathComponent(sampleFileKey, isDirectory: true)
try fileManager.createDirectory(at: artifactLocation, withIntermediateDirectories: true, attributes: nil)
let organizer = ZipArtifactOrganizer(targetTempDir: workingDirectory, fileManager: fileManager)
let organizer = ZipArtifactOrganizer(
targetTempDir: workingDirectory,
artifactProcessors: [],
fileManager: fileManager
)
let result = try organizer.prepareArtifactLocationFor(fileKey: sampleFileKey)
if case .artifactExists(artifactDir: let u) = result {
@@ -105,7 +121,11 @@ class ZipArtifactOrganizerTests: XCTestCase {
.appendingPathComponent("xccache")
.appendingPathComponent(sampleFileKey)
.appendingPathExtension("zip")
let organizer = ZipArtifactOrganizer(targetTempDir: workingDirectory, fileManager: fileManager)
let organizer = ZipArtifactOrganizer(
targetTempDir: workingDirectory,
artifactProcessors: [],
fileManager: fileManager
)
let result = try organizer.prepareArtifactLocationFor(fileKey: sampleFileKey)
@@ -126,7 +146,11 @@ class ZipArtifactOrganizerTests: XCTestCase {
try fileManager.createDirectory(at: activeArtifact, withIntermediateDirectories: true, attributes: nil)
try fileManager.spt_forceSymbolicLink(at: activeLink, withDestinationURL: activeArtifact)
let organizer = ZipArtifactOrganizer(targetTempDir: workingDirectory, fileManager: fileManager)
let organizer = ZipArtifactOrganizer(
targetTempDir: workingDirectory,
artifactProcessors: [],
fileManager: fileManager
)
let fileKey = try organizer.getActiveArtifactFilekey()
@@ -67,7 +67,8 @@ class ThinningDiskSwiftcProductsGeneratorTests: FileXCTestCase {
artifactSwiftModuleObjCFile: headerFile
)
XCTAssertEqual(generatedModulePath, destinationSwiftModuleDir)
XCTAssertEqual(generatedModulePath.swiftmoduleDir, destinationSwiftModuleDir)
XCTAssertEqual(generatedModulePath.objcHeaderFile, objCHeader)
XCTAssertEqual(fileManager.contents(atPath: expectedSwiftSourceInfoFile.path), "sourceInfo".data(using: .utf8))
XCTAssertEqual(fileManager.contents(atPath: objCHeader.path), "header".data(using: .utf8))
}
@@ -30,6 +30,7 @@ class UnzippedArtifactSwiftProductsOrganizerTests: XCTestCase {
override func setUpWithError() throws {
try super.setUpWithError()
let destination = SwiftcProductsGeneratorOutput(swiftmoduleDir: destination, objcHeaderFile: "")
generator = SwiftcProductsGeneratorSpy(generatedDestination: destination)
dirAccessor = DirAccessorFake()
syncer = FileFingerprintSyncer(
@@ -44,6 +44,7 @@ class PostbuildContextTests: FileXCTestCase {
"BUILT_PRODUCTS_DIR": "BUILT_PRODUCTS_DIR",
"DERIVED_SOURCES_DIR": "DERIVED_SOURCES_DIR",
"CURRENT_VARIANT": "normal",
"PUBLIC_HEADERS_FOLDER_PATH": "/usr/local/include",
]
override func setUpWithError() throws {
@@ -130,4 +131,23 @@ class PostbuildContextTests: FileXCTestCase {
XCTAssertEqual(context.compilationTempDir, "/OBJECT_FILE_DIR_custom/x86_64")
}
func testGenericPublicHeaderDestinationIsSkipped() throws {
var envs = Self.SampleEnvs
envs["PUBLIC_HEADERS_FOLDER_PATH"] = "/usr/local/include"
let context = try PostbuildContext(config, env: envs)
XCTAssertNil(context.publicHeadersFolderPath)
}
func testPublicHeaderFolderIsRelativeToProductsDir() throws {
var envs = Self.SampleEnvs
envs["BUILT_PRODUCTS_DIR"] = "/MyBuiltProductsDir"
envs["PUBLIC_HEADERS_FOLDER_PATH"] = "MyModule.grameworks/Headers"
let context = try PostbuildContext(config, env: envs)
XCTAssertEqual(context.publicHeadersFolderPath, "/MyBuiltProductsDir/MyModule.grameworks/Headers")
}
}
@@ -55,7 +55,8 @@ class PostbuildTests: FileXCTestCase {
action: .build,
modeMarkerPath: "",
overlayHeadersPath: "",
irrelevantDependenciesPaths: []
irrelevantDependenciesPaths: [],
publicHeadersFolderPath: nil
)
private var network = RemoteNetworkClientImpl(
NetworkClientFake(fileManager: .default),
@@ -643,4 +644,79 @@ class PostbuildTests: FileXCTestCase {
XCTAssertEqual(downloadedMeta, expectedMeta)
}
func testDecoratesDerivedSwiftHeaderWithEmptyModulesFolderPath() throws {
let dir = try prepareTempDir()
let derivedSourcesDir = dir
.appendingPathComponent("DerivedSources")
let swiftSwiftHeader = derivedSourcesDir
.appendingPathComponent("MyModule-Swift.h")
let swiftSwiftHeaderOverride = swiftSwiftHeader
.appendingPathExtension("md5")
try fileManager.spt_createEmptyDir(derivedSourcesDir)
try fileManager.spt_createEmptyFile(swiftSwiftHeader)
postbuildContext.moduleName = "MyModule"
postbuildContext.derivedSourcesDir = derivedSourcesDir
let postbuild = Postbuild(
context: postbuildContext,
networkClient: network,
remapper: remapper,
fingerprintAccumulator: fingerprintGenerator,
artifactsOrganizer: organizer,
artifactCreator: artifactCreator,
fingerprintSyncer: syncer,
dependenciesReader: dependenciesReader,
dependencyProcessor: processor,
fingerprintOverrideManager: overrideManager,
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [],
consumerPlugins: []
)
try postbuild.performBuildCompletion()
XCTAssertTrue(fileManager.fileExists(atPath: swiftSwiftHeaderOverride.path))
}
func testDecoratesPublicSwiftHeaderWithEmptyModulesFolderPath() throws {
let dir = try prepareTempDir()
let productsDir = dir
.appendingPathComponent("MyModule.framework")
.appendingPathComponent("Headers")
let swiftSwiftHeader = productsDir
.appendingPathComponent("MyModule-Swift.h")
let swiftSwiftHeaderOverride = swiftSwiftHeader
.appendingPathExtension("md5")
try fileManager.spt_createEmptyDir(productsDir)
try fileManager.spt_createEmptyFile(swiftSwiftHeader)
postbuildContext.moduleName = "MyModule"
postbuildContext.publicHeadersFolderPath = productsDir
let postbuild = Postbuild(
context: postbuildContext,
networkClient: network,
remapper: remapper,
fingerprintAccumulator: fingerprintGenerator,
artifactsOrganizer: organizer,
artifactCreator: artifactCreator,
fingerprintSyncer: syncer,
dependenciesReader: dependenciesReader,
dependencyProcessor: processor,
fingerprintOverrideManager: overrideManager,
dSYMOrganizer: DSYMOrganizerFake(dSYMFile: nil),
modeController: modeController,
metaReader: metaReader,
metaWriter: metaWriter,
creatorPlugins: [],
consumerPlugins: []
)
try postbuild.performBuildCompletion()
XCTAssertTrue(fileManager.fileExists(atPath: swiftSwiftHeaderOverride.path))
}
}
@@ -70,7 +70,9 @@ class SwiftcTests: FileXCTestCase {
)
context = try SwiftcContext(config: config, input: input)
markerWriter = MarkerWriterSpy()
productsGenerator = SwiftcProductsGeneratorSpy()
productsGenerator = SwiftcProductsGeneratorSpy(
generatedDestination: SwiftcProductsGeneratorOutput(swiftmoduleDir: "", objcHeaderFile: "")
)
let dependenciesWriterSpy = DependenciesWriterSpy()
self.dependenciesWriterSpy = dependenciesWriterSpy
dependenciesWriterFactory = { [dependenciesWriterSpy] _, _ in dependenciesWriterSpy }
@@ -283,6 +285,12 @@ class SwiftcTests: FileXCTestCase {
let artifactSwiftInterfaceInfo = URL(
fileURLWithPath: "/cachedArtifact/swiftmodule/archTest/Target.swiftinterface"
)
let artifactPrivateSwiftInterfaceInfo = URL(
fileURLWithPath: "/cachedArtifact/swiftmodule/archTest/Target.private.swiftinterface"
)
let artifactAbiJsonInfo = URL(
fileURLWithPath: "/cachedArtifact/swiftmodule/archTest/Target.abi.json"
)
artifactOrganizer = ArtifactOrganizerFake(artifactRoot: artifactRoot)
let swiftc = Swiftc(
@@ -303,17 +311,14 @@ class SwiftcTests: FileXCTestCase {
_ = try swiftc.mockCompilation()
let swiftModuleFiles = try productsGenerator.generated.first.unwrap()
let swiftModuleURL = swiftModuleFiles.0[.swiftmodule]
let swiftDocURL = swiftModuleFiles.0[.swiftdoc]
let swiftSourceInfoURL = swiftModuleFiles.0[.swiftsourceinfo]
let swiftInterfaceURL = swiftModuleFiles.0[.swiftinterface]
let swiftHeaderURL = swiftModuleFiles.1
XCTAssertEqual(swiftModuleURL, artifactSwiftmodule)
XCTAssertEqual(swiftDocURL, artifactSwiftdoc)
XCTAssertEqual(swiftSourceInfoURL, artifactSwiftSourceInfo)
XCTAssertEqual(swiftHeaderURL, artifactObjCHeader)
XCTAssertEqual(swiftInterfaceURL, artifactSwiftInterfaceInfo)
XCTAssertEqual(swiftModuleFiles.0[.swiftmodule], artifactSwiftmodule)
XCTAssertEqual(swiftModuleFiles.0[.swiftdoc], artifactSwiftdoc)
XCTAssertEqual(swiftModuleFiles.0[.swiftsourceinfo], artifactSwiftSourceInfo)
XCTAssertEqual(swiftModuleFiles.0[.swiftinterface], artifactSwiftInterfaceInfo)
XCTAssertEqual(swiftModuleFiles.0[.privateSwiftinterface], artifactPrivateSwiftInterfaceInfo)
XCTAssertEqual(swiftModuleFiles.0[.abiJson], artifactAbiJsonInfo)
XCTAssertEqual(swiftModuleFiles.1, artifactObjCHeader)
}
@@ -32,11 +32,18 @@ class XCRemoteCacheConfigReaderTests: XCTestCase {
}
func testReadsFromExtraConfig() throws {
try fileReader.write(toPath: "/.rcinfo", contents: "cache_addresses: [test]")
let contents = [
"cache_addresses: [test]",
"retry_delay: 30",
"upload_batch_size: 5",
].joined(separator: "\n").data(using: .utf8)
try fileReader.write(toPath: "/.rcinfo", contents: contents)
let config = try reader.readConfiguration()
XCTAssertEqual(config.cacheAddresses, ["test"])
XCTAssertEqual(config.retryDelay, 30)
XCTAssertEqual(config.uploadBatchSize, 5)
}
func testOverridesExtraConfigFromExtraFile() throws {
@@ -74,4 +74,51 @@ class FileFingerprintSyncerTests: FileXCTestCase {
XCTAssertTrue(fileManager.fileExists(atPath: nonOverrideFile.path))
}
func testDecoratesFile() throws {
let header = swiftmoduleDir.appendingPathComponent("Module-Swift.h")
let headerOverride = swiftmoduleDir.appendingPathComponent("Module-Swift.h.md5")
try fileManager.spt_createEmptyFile(header)
try syncer.decorate(file: header, fingerprint: "1")
XCTAssertEqual(try String(contentsOf: headerOverride), "1")
}
func testFileDecorateOverridesPreviousOverlay() throws {
let header = swiftmoduleDir.appendingPathComponent("Module-Swift.h")
let headerOverride = swiftmoduleDir.appendingPathComponent("Module-Swift.h.md5")
try fileManager.spt_createEmptyFile(header)
try "1".write(to: headerOverride, atomically: true, encoding: .utf8)
try syncer.decorate(file: header, fingerprint: "2")
XCTAssertEqual(try String(contentsOf: headerOverride), "2")
}
func testDeletesFileOverride() throws {
let header = swiftmoduleDir.appendingPathComponent("Module-Swift.h")
let headerOverride = swiftmoduleDir.appendingPathComponent("Module-Swift.h.md5")
try fileManager.spt_createEmptyFile(header)
try fileManager.spt_createEmptyFile(headerOverride)
try syncer.delete(file: header)
XCTAssertFalse(fileManager.fileExists(atPath: headerOverride.path))
}
func testDeletesDoesntDeleteWhenFileIsMissing() throws {
let nonExistingFile = swiftmoduleDir.appendingPathComponent("Module-Swift.h")
XCTAssertNoThrow(try syncer.delete(file: nonExistingFile))
}
func testDeletesDoesntDeleteWhenOverrideIsMissing() throws {
let header = swiftmoduleDir.appendingPathComponent("Module-Swift.h")
try fileManager.spt_createEmptyFile(header)
XCTAssertNoThrow(try syncer.delete(file: header))
}
}
@@ -71,4 +71,17 @@ class FileManagerDirScannerTests: FileXCTestCase {
try XCTAssertThrowsError(dirScanner.items(at: dir))
}
func testFindsAllFilesRecursively() throws {
let dir = workingDirectory!.appendingPathComponent("dir")
let nestedDir = dir.appendingPathComponent("nested")
let nestedFile = nestedDir.appendingPathComponent("file")
try fileManager.spt_createEmptyFile(nestedFile)
let allFiles = try dirScanner.recursiveItems(at: dir)
// returned items may contain symbolic links in a path
let allFilesResolve = allFiles.map { $0.resolvingSymlinksInPath() }
XCTAssertEqual(allFilesResolve, [nestedFile])
}
}
@@ -167,7 +167,7 @@ class NetworkClientImplTests: XCTestCase {
func testUploadSuccessDoesntRetry() throws {
client = NetworkClientImpl(
session: session,
retries: 0,
retries: 2,
retryDelay: 0,
fileManager: fileManager,
awsV4Signature: nil
@@ -178,6 +178,38 @@ class NetworkClientImplTests: XCTestCase {
XCTAssertEqual(requests.map(\.url), [url], "Expected 1 request - original only")
}
func testDownloadFilureWith400Retries() throws {
client = NetworkClientImpl(
session: session,
retries: 2,
retryDelay: 0,
fileManager: fileManager,
awsV4Signature: nil
)
responses[url] = nil
_ = try waitForResponse({ client.download(url, to: fileURL, completion: $0) }, timeout: 0.5)
XCTAssertEqual(
requests.map(\.url),
Array(repeating: url, count: 3),
"Expected 3 requests (original + 2 retries)"
)
}
func testDownloadSuccessDoesntRetry() throws {
client = NetworkClientImpl(
session: session,
retries: 2,
retryDelay: 0,
fileManager: fileManager,
awsV4Signature: nil
)
responses[url] = .success(successResponse, Data())
_ = try waitForResponse { client.download(url, to: fileURL, completion: $0) }
XCTAssertEqual(requests.map(\.url), [url], "Expected 1 request - original only")
}
func testFileExits400CompletesWithFalse() throws {
responses[url] = .success(failureResponse, Data())
let response = try waitForResponse { client.fileExists(url, completion: $0) }
@@ -39,7 +39,12 @@ class ReplicatedRemotesNetworkClientTests: XCTestCase {
uploadURLs = try [URL(string: "http://upload1.com").unwrap(), URL(string: "http://upload2.com").unwrap()]
download = URLBuilderFake(downloadURL)
uploads = uploadURLs.map(URLBuilderFake.init)
client = ReplicatedRemotesNetworkClient(networkClient, download: download, uploads: uploads)
client = ReplicatedRemotesNetworkClient(
networkClient,
download: download,
uploads: uploads,
uploadBatchSize: 1
)
}
private func prepareLocalEmptyFile() throws -> URL {
@@ -62,6 +67,30 @@ class ReplicatedRemotesNetworkClientTests: XCTestCase {
XCTAssertTrue(try networkClient.fileExistsSynchronously(expectedArtifact2))
}
func testUploadsWithLimit() throws {
var expectedArtifacts = [URL]()
var uploadURLs = [URL]()
for index in 0...99 {
let expectedArtifact = try URL(string: "http://upload\(index).com/file/id1").unwrap()
expectedArtifacts.append(expectedArtifact)
let uploadURL = try URL(string: "http://upload\(index).com").unwrap()
uploadURLs.append(uploadURL)
}
uploads = uploadURLs.map(URLBuilderFake.init)
client = ReplicatedRemotesNetworkClient(
networkClient,
download: download,
uploads: uploads,
uploadBatchSize: 10
)
try client.uploadSynchronously(localSampleFile, as: .artifact(id: "id1"))
for expectedArtifact in expectedArtifacts {
XCTAssertTrue(try networkClient.fileExistsSynchronously(expectedArtifact))
}
}
func testCreatesInAllStreams() throws {
let expectedMeta1 = try URL(string: "http://upload1.com/meta/commit_id").unwrap()
let expectedMeta2 = try URL(string: "http://upload2.com/meta/commit_id").unwrap()
@@ -71,4 +100,5 @@ class ReplicatedRemotesNetworkClientTests: XCTestCase {
XCTAssertTrue(try networkClient.fileExistsSynchronously(expectedMeta1))
XCTAssertTrue(try networkClient.fileExistsSynchronously(expectedMeta2))
}
}
@@ -47,6 +47,16 @@ class DirAccessorFake: DirAccessor {
}
}
func recursiveItems(at dir: URL) throws -> [URL] {
memory.compactMap { url, _ in
// comparing paths to ignore dir or url's "isDir" property
if url.deletingLastPathComponent().path.starts(with: dir.path) {
return url
}
return nil
}
}
func contents(atPath path: String) throws -> Data? {
memory[URL(fileURLWithPath: path)]
}
@@ -60,3 +60,28 @@ class FileAccessorFake: FileAccessor {
return storage[path]?.mdate
}
}
extension FileAccessorFake: DirScanner {
func itemType(atPath path: String) throws -> ItemType {
if storage[path] != nil {
return .file
}
if try !recursiveItems(at: URL(fileURLWithPath: path)).isEmpty {
return .dir
}
return .nonExisting
}
func items(at dir: URL) throws -> [URL] {
storage.keys.map(URL.init(fileURLWithPath:)).filter {
$0.deletingLastPathComponent() == dir
}
}
func recursiveItems(at dir: URL) throws -> [URL] {
let paths = storage.keys.filter {
$0.hasPrefix(dir.path)
}
return paths.map(URL.init(fileURLWithPath:))
}
}
@@ -0,0 +1,28 @@
// 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
/// No-operation processor
class NoopArtifactProcessor: ArtifactProcessor {
func process(rawArtifact url: URL) throws {}
func process(localArtifact url: URL) throws {}
}
@@ -39,7 +39,7 @@ class SwiftcProductsGeneratorFake: SwiftcProductsGenerator {
func generateFrom(
artifactSwiftModuleFiles: [SwiftmoduleFileExtension: URL],
artifactSwiftModuleObjCFile: URL
) throws -> URL {
) throws -> SwiftcProductsGeneratorOutput {
let swiftmoduleDestBasename = swiftmoduleDest.deletingPathExtension()
for (ext, url) in artifactSwiftModuleFiles {
try dirAccessor.write(
@@ -51,6 +51,9 @@ class SwiftcProductsGeneratorFake: SwiftcProductsGenerator {
toPath: swiftmoduleObjCFile.path,
contents: dirAccessor.contents(atPath: artifactSwiftModuleObjCFile.path)
)
return swiftmoduleDest.deletingLastPathComponent()
return SwiftcProductsGeneratorOutput(
swiftmoduleDir: swiftmoduleDest.deletingLastPathComponent(),
objcHeaderFile: swiftmoduleObjCFile
)
}
}
@@ -22,16 +22,16 @@ import Foundation
class SwiftcProductsGeneratorSpy: SwiftcProductsGenerator {
private(set) var generated: [([SwiftmoduleFileExtension: URL], URL)] = []
private let generationDestination: URL
private let generationDestination: SwiftcProductsGeneratorOutput
init(generatedDestination: URL = "") {
init(generatedDestination: SwiftcProductsGeneratorOutput) {
generationDestination = generatedDestination
}
func generateFrom(
artifactSwiftModuleFiles: [SwiftmoduleFileExtension: URL],
artifactSwiftModuleObjCFile: URL
) throws -> URL {
) throws -> SwiftcProductsGeneratorOutput {
generated.append((
artifactSwiftModuleFiles,
artifactSwiftModuleObjCFile
+7
View File
@@ -0,0 +1,7 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: XCRemoteCache
spec:
type: library
owner: foundation
@@ -362,12 +362,73 @@ module CocoapodsXCRemoteCacheModifier
File.write(LLDB_INIT_PATH, lldbinit_lines.join("\n"), mode: "w")
end
Pod::HooksManager.register('cocoapods-xcremotecache', :post_install) do |installer_context|
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
# is enabled and all artifacts are available (i.e. xcprepare returns 0).
# If Pods projects/targets are cached from previous `pod install` action that didn't enable XCRemoteCache (e.g. artifacts
# are not available in the remote cache), these projects/targets should be invalidated to include XCRemoteCache-related
# build steps and build settings.
if @@configuration.nil?
Pod::UI.puts "[XCRC] Warning! XCRemoteCache not configured. Call xcremotecache({...}) in Podfile to enable XCRemoteCache"
next
end
begin
# `user_pod_directory`` and `user_proj_directory` in the 'postinstall' should be equal
user_pod_directory = File.dirname(installer_context.podfile.defined_in_file)
set_configuration_default_values
unless @@configuration['enabled']
# No need to check if enabling remote cache for the first time
next
end
validate_configuration()
mode = @@configuration['mode']
remote_commit_file = @@configuration['remote_commit_file']
xcrc_location = @@configuration['xcrc_location']
check_build_configuration = @@configuration['check_build_configuration']
check_platform = @@configuration['check_platform']
xcrc_location_absolute = "#{user_pod_directory}/#{xcrc_location}"
remote_commit_file_absolute = "#{user_pod_directory}/#{remote_commit_file}"
# Download XCRC
download_xcrc_if_needed(xcrc_location_absolute)
# Save .rcinfo
root_rcinfo = generate_rcinfo()
save_rcinfo(root_rcinfo, user_pod_directory)
# Create directory for xccc & arc.rc location
Dir.mkdir(BIN_DIR) unless File.exist?(BIN_DIR)
# Remove previous xccc & arc.rc
was_previously_enabled = File.exist?(remote_commit_file_absolute)
File.delete(remote_commit_file_absolute) if File.exist?(remote_commit_file_absolute)
prepare_result = YAML.load`#{xcrc_location_absolute}/xcprepare --configuration #{check_build_configuration} --platform #{check_platform}`
if !prepare_result['result'] && mode == 'consumer'
# Remote cache is still disabled - no need to force Pods projects/targets regeneration
next
end
# Force rebuilding all Pods project, because XCRC build steps and settings need to be added to Pods project/targets
# It is relevant only when 'incremental_installation' is enabled, otherwise installed_cache_path does not exist on a disk
installed_cache_path = installer_context.sandbox.project_installation_cache_path
if !was_previously_enabled && File.exist?(installed_cache_path)
Pod::UI.puts "[XCRC] Forces Pods project regenerations because XCRC is enabled for the first time."
File.delete(installed_cache_path)
end
end
end
Pod::HooksManager.register('cocoapods-xcremotecache', :post_install) do |installer_context|
if @@configuration.nil?
next
end
user_project = installer_context.umbrella_targets[0].user_project
begin
@@ -394,20 +455,12 @@ module CocoapodsXCRemoteCacheModifier
xccc_location_absolute = "#{user_proj_directory}/#{xccc_location}"
xcrc_location_absolute = "#{user_proj_directory}/#{xcrc_location}"
remote_commit_file_absolute = "#{user_proj_directory}/#{remote_commit_file}"
# Download XCRC
download_xcrc_if_needed(xcrc_location_absolute)
# Save .rcinfo
root_rcinfo = generate_rcinfo()
save_rcinfo(root_rcinfo, user_proj_directory)
# Create directory for xccc & arc.rc location
Dir.mkdir(BIN_DIR) unless File.exist?(BIN_DIR)
# Remove previous xccc & arc.rc
File.delete(remote_commit_file_absolute) if File.exist?(remote_commit_file_absolute)
# Remove previous xccc
File.delete(xccc_location_absolute) if File.exist?(xccc_location_absolute)
# Prepare XCRC
@@ -458,6 +511,7 @@ module CocoapodsXCRemoteCacheModifier
# Enabled/disable XCRemoteCache for the main (user) project
begin
# TODO: Do not compile xcc again. `xcprepare` compiles it in pre-install anyway
prepare_result = YAML.load`#{xcrc_location_absolute}/xcprepare --configuration #{check_build_configuration} --platform #{check_platform}`
unless prepare_result['result'] || mode != 'consumer'
# Uninstall the XCRemoteCache for the consumer mode
@@ -13,5 +13,5 @@
# limitations under the License.
module CocoapodsXcremotecache
VERSION = "0.0.13"
VERSION = "0.0.14"
end
@@ -240,7 +240,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "ditto \"${SCRIPT_INPUT_FILE_0}\" \"${SCRIPT_OUTPUT_FILE_0}\"\nditto \"${SCRIPT_INPUT_FILE_1}\" \"${SCRIPT_OUTPUT_FILE_1}\" || true\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 \"${SCRIPT_OUTPUT_FILE_1}\"\n\n";
};
/* End PBXShellScriptBuildPhase section */
+1 -3
View File
@@ -26,8 +26,7 @@ namespace :e2e do
Stats = Struct.new(:hits, :misses, :hit_rate)
# run E2E tests
# TODO: add :run_standalone when support for bridging headers support is ready
task :run => [:run_cocoapods]
task :run => [:run_cocoapods, :run_standalone]
# run E2E tests for CocoaPods-powered projects
task :run_cocoapods do
@@ -56,7 +55,6 @@ namespace :e2e do
Dir.chdir(E2E_STANDALONE_SAMPLE_DIR) do
clean_git
# Run integrate the project
p "#{XCRC_BINARIES}/xcprepare integrate --input StandaloneApp.xcodeproj --mode producer --final-producer-target StandaloneApp"
system("pwd")
system("#{XCRC_BINARIES}/xcprepare integrate --input StandaloneApp.xcodeproj --mode producer --final-producer-target StandaloneApp")
# Build the project to fill in the cache