Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e86cac3ec | |||
| 22faa5dbdb | |||
| 522900748d | |||
| 4998cc4f87 | |||
| 9221f9d2b5 | |||
| 1127257ad5 | |||
| 599e5fe561 | |||
| 478649a1d7 | |||
| 1c5aa569dd | |||
| c6b31d3086 | |||
| f0a4d361b1 | |||
| 0778639ae2 | |||
| c44b793a19 | |||
| 2b383f046e | |||
| cf0b27d03c | |||
| b2d47760cf | |||
| 714c9ef35b | |||
| b872a8d7f9 | |||
| 56b6722dbe | |||
| 92875bcdee | |||
| 46a99bbe32 | |||
| 408698f1e8 | |||
| e54ce770e7 | |||
| 49be11184e | |||
| d8850b555a | |||
| 93a60b2b40 | |||
| cde81d852f | |||
| 97328144fd | |||
| 3f8333c07b | |||
| 2160645f2c | |||
| d9c2213f50 | |||
| 327d282e23 | |||
| 06781763aa | |||
| 60c7a586c7 | |||
| 2ac3da9035 | |||
| 4dbc5c9b19 | |||
| 6d10ac8c7d | |||
| 8b8c2d627c | |||
| b1026b16da | |||
| e1cc629c55 | |||
| f75c77efa2 | |||
| 3f8ec5b453 | |||
| 94b57475e8 | |||
| a30d56ac16 | |||
| 81077edfa4 | |||
| da3a5d59fa | |||
| 94490532f7 | |||
| 326cac7668 | |||
| 44a09befa0 | |||
| cdddc5bf19 | |||
| 93b8dcd0c3 | |||
| da0d1c20d2 | |||
| 8155e042b5 | |||
| f3832c31bd | |||
| 6a7e6d1135 | |||
| da59c2a211 | |||
| eaba7f3c67 | |||
| bab3326175 | |||
| dc0a82058b | |||
| c5c2732cc9 | |||
| 46debcfa8a | |||
| 3f6d3af5a5 | |||
| c42cb7ac55 | |||
| c65eccc5ee |
@@ -23,3 +23,5 @@ jobs:
|
||||
run: rake build[release]
|
||||
- name: Test
|
||||
run: rake test
|
||||
- name: E2ETests
|
||||
run: rake e2e_only
|
||||
|
||||
@@ -44,3 +44,29 @@ jobs:
|
||||
file_glob: true
|
||||
tag: ${{ github.ref }}
|
||||
overwrite: true
|
||||
cocoapods:
|
||||
name: Publish CocoaPods plugin
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
defaults:
|
||||
run:
|
||||
working-directory: cocoapods-plugin
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.0'
|
||||
- run: bundle install
|
||||
- name: Publish to RubyGems
|
||||
run: |
|
||||
mkdir -p $HOME/.gem
|
||||
touch $HOME/.gem/credentials
|
||||
chmod 0600 $HOME/.gem/credentials
|
||||
printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
|
||||
gem build *.gemspec
|
||||
CURRENT_VERSION=$(gem list cocoapods-xcremotecache --remote -q | sed 's/[^0-9\.]//g')
|
||||
[ -f cocoapods-xcremotecache-$CURRENT_VERSION.gem ] && echo "Version $CURRENT_VERSION already exists" || gem push *.gem
|
||||
env:
|
||||
GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}"
|
||||
|
||||
+5
-1
@@ -1,9 +1,13 @@
|
||||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
*.xcodeproj/
|
||||
*.xcworkspace/
|
||||
DerivedData
|
||||
/.swiftpm/
|
||||
releases
|
||||
tmp/
|
||||
.idea/
|
||||
xcuserdata
|
||||
*.gem
|
||||
Pods/
|
||||
|
||||
+3
-2
@@ -1,4 +1,4 @@
|
||||
// swift-tools-version:5.1
|
||||
// swift-tools-version:5.3
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package
|
||||
|
||||
import PackageDescription
|
||||
@@ -56,7 +56,8 @@ let package = Package(
|
||||
),
|
||||
.testTarget(
|
||||
name: "XCRemoteCacheTests",
|
||||
dependencies: ["XCRemoteCache"]
|
||||
dependencies: ["XCRemoteCache"],
|
||||
resources: [.copy("TestData")]
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -45,6 +45,7 @@ _XCRemoteCache is a remote cache tool for Xcode projects. It reuses target artif
|
||||
- [FAQ](#faq)
|
||||
- [Development](#development)
|
||||
- [Release](#release)
|
||||
* [Releasing CocoaPods plugin](#releasing-cocoapods-plugin)
|
||||
* [Building release package](#building-release-package)
|
||||
- [Contributing](#contributing)
|
||||
- [Code of conduct](#code-of-conduct)
|
||||
@@ -310,7 +311,7 @@ _Note that for the `producer` mode, the prebuild build phase and `xccc`, `xcld`,
|
||||
| `aws_region` | Region for AWS V4 Signature Authorization. E.g. `eu`. | `""` | ⬜️ |
|
||||
| `aws_service` | Service for AWS V4 Signature Authorization. E.g. `storage`. | `""` | ⬜️ |
|
||||
| `out_of_band_mappings` | A dictionary of files path remapping that should be applied to make it absolute path agnostic on a list of dependencies. Useful if a project refers files out of repo root, either compilation files or precompiled dependencies. Keys represent generic replacement and values are substrings that should be replaced. Example: for mapping `["COOL_LIBRARY": "/CoolLibrary"]` `/CoolLibrary/main.swift`will be represented as `$(COOL_LIBRARY)/main.swift`). Warning: remapping order is not-deterministic so avoid remappings with multiple matchings. | `[:]` | ⬜️ |
|
||||
|
||||
| `disable_certificate_verification` | A Boolean value that opts-in SSL certificate validation is disabled | `false` | ⬜️ |
|
||||
|
||||
## Backend cache server
|
||||
|
||||
@@ -421,6 +422,12 @@ Follow the [Development](docs/Development.md) guide. It has all the information
|
||||
To release a version, in [Releases](https://github.com/spotify/XCRemoteCache/releases) draft a new release with `v0.3.0{-rc0}` tag format.
|
||||
Packages with binaries will be automatically uploaded to the GitHub [Releases](https://github.com/spotify/XCRemoteCache/releases) page.
|
||||
|
||||
### Releasing CocoaPods plugin
|
||||
|
||||
Bump a gem version defined in [gem_version.rb](cocoapods-plugin/lib/cocoapods-xcremotecache/gem_version.rb) and create a new release described above.
|
||||
|
||||
A plugin is automatically uploaded to [RubyGems](https://rubygems.org/gems/cocoapods-xcremotecache) if a given version doesn't exist yet.
|
||||
|
||||
### Building release package
|
||||
|
||||
To build a release zip package for a single platform (e.g. `x86_64-apple-macosx`, `arm64-apple-macosx`), call:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# encoding: utf-8
|
||||
require_relative 'tasks/e2e'
|
||||
|
||||
################################
|
||||
# Rake configuration
|
||||
@@ -77,6 +78,12 @@ task :test do
|
||||
spm_test()
|
||||
end
|
||||
|
||||
desc 'build and run E2E tests'
|
||||
task :e2e => [:build, :e2e_only]
|
||||
|
||||
desc 'run E2E tests without building the XCRemoteCache binary'
|
||||
task :e2e_only => ['e2e:run']
|
||||
|
||||
################################
|
||||
# Helper functions
|
||||
################################
|
||||
|
||||
@@ -51,8 +51,6 @@ protocol ArtifactSwiftProductsBuilder {
|
||||
/// # {workingDir}/xccache/produced/include/#{moduleName} (if `moduleName` is defined)
|
||||
class ArtifactSwiftProductsBuilderImpl: ArtifactSwiftProductsBuilder {
|
||||
|
||||
/// List of all required swiftmodule related extensions that should be copied to the artifact
|
||||
private static let swiftmoduleExtensionsToInclude = ["swiftmodule", "swiftdoc", "swiftsourceinfo"]
|
||||
private let workingDir: URL
|
||||
private let moduleName: String?
|
||||
private let fileManager: FileManager
|
||||
|
||||
@@ -30,6 +30,7 @@ enum SwiftmoduleFileExtension: String {
|
||||
case swiftmodule
|
||||
case swiftdoc
|
||||
case swiftsourceinfo
|
||||
case swiftinterface
|
||||
}
|
||||
|
||||
extension SwiftmoduleFileExtension {
|
||||
@@ -38,5 +39,6 @@ extension SwiftmoduleFileExtension {
|
||||
.swiftmodule: .required,
|
||||
.swiftdoc: .required,
|
||||
.swiftsourceinfo: .optional,
|
||||
.swiftinterface: .optional,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -79,6 +79,8 @@ public struct PostbuildContext {
|
||||
/// Action type: build, indexbuild etc.
|
||||
var action: BuildActionType
|
||||
let modeMarkerPath: String
|
||||
/// location of the json file that define virtual files system overlay (mappings of the virtual location file -> local file path)
|
||||
let overlayHeadersPath: URL
|
||||
}
|
||||
|
||||
extension PostbuildContext {
|
||||
@@ -127,5 +129,7 @@ extension PostbuildContext {
|
||||
thinnedTargets = thinFocusedTargetsString.split(separator: ",").map(String.init)
|
||||
action = (try? BuildActionType(rawValue: env.readEnv(key: "ACTION"))) ?? .unknown
|
||||
modeMarkerPath = config.modeMarkerPath
|
||||
/// Note: The file has yaml extension, even it is in the json format
|
||||
overlayHeadersPath = targetTempDir.appendingPathComponent("all-product-headers.yaml")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ public class XCPostbuild {
|
||||
// Initialize dependencies
|
||||
let primaryGitBranch = GitBranch(repoLocation: config.primaryRepo, branch: config.primaryBranch)
|
||||
let gitClient = GitClientImpl(repoRoot: config.repoRoot, primary: primaryGitBranch, shell: shellGetStdout)
|
||||
let pathRemapper = try StringDependenciesRemapperFactory().build(
|
||||
let envsRemapper = try StringDependenciesRemapperFactory().build(
|
||||
orderKeys: DependenciesMapping.rewrittenEnvs,
|
||||
envs: env,
|
||||
customMappings: config.outOfBandMappings
|
||||
@@ -145,6 +145,18 @@ public class XCPostbuild {
|
||||
fileDependeciesReaderFactory: fileReaderFactory,
|
||||
dirScanner: fileManager
|
||||
)
|
||||
// As the PostbuildContext assumes file location and filename (`all-product-headers.yaml`)
|
||||
// do not fail in case of a missing headers overlay file. In the future, all overlay files could be
|
||||
// captured from the swiftc invocation similarly is stored in the `history.compile` for the consumer mode.
|
||||
let overlayReader = JsonOverlayReader(
|
||||
context.overlayHeadersPath,
|
||||
mode: .bestEffort,
|
||||
fileReader: fileManager
|
||||
)
|
||||
let overlayRemapper = try OverlayDependenciesRemapper(
|
||||
overlayReader: overlayReader
|
||||
)
|
||||
let pathRemapper = DependenciesRemapperComposite([overlayRemapper, envsRemapper])
|
||||
let dependencyProcessor = DependencyProcessorImpl(
|
||||
xcode: context.xcodeDir,
|
||||
product: context.productsDir,
|
||||
|
||||
@@ -43,6 +43,8 @@ public struct PrebuildContext {
|
||||
let targetName: String
|
||||
/// List of all targets to downloaded from the thinning aggregation target
|
||||
var thinnedTargets: [String]?
|
||||
/// location of the json file that define virtual files system overlay (mappings of the virtual location file -> local file path)
|
||||
let overlayHeadersPath: URL
|
||||
}
|
||||
|
||||
extension PrebuildContext {
|
||||
@@ -64,5 +66,7 @@ extension PrebuildContext {
|
||||
self.targetName = targetName
|
||||
let thinFocusedTargetsString: String? = env.readEnv(key: "SPT_XCREMOTE_CACHE_THINNED_TARGETS")
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,11 +115,22 @@ public class XCPrebuild {
|
||||
)
|
||||
let client: NetworkClient = config.disableHttpCache ? networkClient : cacheNetworkClient
|
||||
let remoteNetworkClient = RemoteNetworkClientImpl(client, urlBuilder)
|
||||
let pathRemapper = try StringDependenciesRemapperFactory().build(
|
||||
let envsRemapper = try StringDependenciesRemapperFactory().build(
|
||||
orderKeys: DependenciesMapping.rewrittenEnvs,
|
||||
envs: env,
|
||||
customMappings: config.outOfBandMappings
|
||||
)
|
||||
// As PrebuildContext assumes file location and its filename (`all-product-headers.yaml`)
|
||||
// do not fail in case of a missing headers overlay file.
|
||||
let overlayReader = JsonOverlayReader(
|
||||
context.overlayHeadersPath,
|
||||
mode: .bestEffort,
|
||||
fileReader: fileManager
|
||||
)
|
||||
let overlayRemapper = try OverlayDependenciesRemapper(
|
||||
overlayReader: overlayReader
|
||||
)
|
||||
let pathRemapper = DependenciesRemapperComposite([overlayRemapper, envsRemapper])
|
||||
let filesFingerprintGenerator = FingerprintAccumulatorImpl(
|
||||
algorithm: MD5Algorithm(),
|
||||
fileManager: fileManager
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
// under the License.
|
||||
|
||||
import Foundation
|
||||
import Yams
|
||||
|
||||
/// Print current configuration to the console
|
||||
public class XCConfig {
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
// under the License.
|
||||
|
||||
import Foundation
|
||||
import Yams
|
||||
|
||||
/// Switch between Online/Offline modes
|
||||
public enum XCPrepareMode {
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
// under the License.
|
||||
|
||||
import Foundation
|
||||
import Yams
|
||||
|
||||
/// Manages XCRemoteCache statistics: rests, print to the standard output etc
|
||||
public class XCStats {
|
||||
|
||||
@@ -129,6 +129,8 @@ public struct XCRemoteCacheConfig: Encodable {
|
||||
/// `/CoolLibrary/main.swift`will be represented as `$(COOL_LIBRARY)/main.swift`).
|
||||
/// Warning: remapping order is not-deterministic so avoid remappings with multiple matchings.
|
||||
var outOfBandMappings: [String: String] = [:]
|
||||
/// If true, SSL certificate validation is disabled
|
||||
var disableCertificateVerification: Bool = false
|
||||
}
|
||||
|
||||
extension XCRemoteCacheConfig {
|
||||
@@ -180,6 +182,7 @@ extension XCRemoteCacheConfig {
|
||||
merge.AWSRegion = scheme.AWSRegion ?? AWSRegion
|
||||
merge.AWSService = scheme.AWSService ?? AWSService
|
||||
merge.outOfBandMappings = scheme.outOfBandMappings ?? outOfBandMappings
|
||||
merge.disableCertificateVerification = scheme.disableCertificateVerification ?? disableCertificateVerification
|
||||
return merge
|
||||
}
|
||||
|
||||
@@ -240,6 +243,7 @@ struct ConfigFileScheme: Decodable {
|
||||
let AWSRegion: String?
|
||||
let AWSService: String?
|
||||
let outOfBandMappings: [String: String]?
|
||||
let disableCertificateVerification: Bool?
|
||||
|
||||
// Yams library doesn't support encoding strategy, see https://github.com/jpsim/Yams/issues/84
|
||||
enum CodingKeys: String, CodingKey {
|
||||
@@ -283,6 +287,7 @@ struct ConfigFileScheme: Decodable {
|
||||
case AWSRegion = "aws_region"
|
||||
case AWSService = "aws_service"
|
||||
case outOfBandMappings = "out_of_band_mappings"
|
||||
case disableCertificateVerification = "disable_certificate_verification"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,11 +58,11 @@ class DependencyProcessorImpl: DependencyProcessor {
|
||||
private let bundlePath: String?
|
||||
|
||||
init(xcode: URL, product: URL, source: URL, intermediate: URL, bundle: URL?) {
|
||||
xcodePath = xcode.path
|
||||
productPath = product.path
|
||||
sourcePath = source.path
|
||||
intermediatePath = intermediate.path
|
||||
bundlePath = bundle?.path
|
||||
xcodePath = xcode.path.dirPath()
|
||||
productPath = product.path.dirPath()
|
||||
sourcePath = source.path.dirPath()
|
||||
intermediatePath = intermediate.path.dirPath()
|
||||
bundlePath = bundle?.path.dirPath()
|
||||
}
|
||||
|
||||
func process(_ files: [URL]) -> [Dependency] {
|
||||
@@ -72,7 +72,7 @@ class DependencyProcessorImpl: DependencyProcessor {
|
||||
|
||||
private func classify(_ files: [URL]) -> [Dependency] {
|
||||
return files.map { file -> Dependency in
|
||||
let filePath = file.path
|
||||
let filePath = file.resolvingSymlinksInPath().path
|
||||
if filePath.hasPrefix(xcodePath) {
|
||||
return Dependency(url: file, type: .xcode)
|
||||
} else if filePath.hasPrefix(intermediatePath) {
|
||||
@@ -111,3 +111,9 @@ class DependencyProcessorImpl: DependencyProcessor {
|
||||
return !irrelevantDependenciesType.contains(dependency.type)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension String {
|
||||
func dirPath() -> String {
|
||||
hasSuffix("/") ? self : appending("/")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2021 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import Foundation
|
||||
|
||||
/// File paths remapper according the virtual file system mappings
|
||||
/// Warning: this class is not thread safe
|
||||
class OverlayDependenciesRemapper: DependenciesRemapper {
|
||||
private var mappings: [OverlayMapping]
|
||||
|
||||
init(overlayReader: OverlayReader) throws {
|
||||
mappings = try overlayReader.provideMappings()
|
||||
}
|
||||
|
||||
private func mapPath(
|
||||
_ path: String,
|
||||
source: KeyPath<OverlayMapping,URL>,
|
||||
destination: KeyPath<OverlayMapping,URL>
|
||||
) -> String {
|
||||
guard let mapping = mappings.first(where: { $0[keyPath: source].path == path }) else {
|
||||
// TODO: support partial mappings, where a directory path can be replaced with some other directory
|
||||
// no direct mapping found
|
||||
return path
|
||||
}
|
||||
return mapping[keyPath: destination].path
|
||||
}
|
||||
|
||||
func replace(genericPaths: [String]) -> [String] {
|
||||
Set(genericPaths.map {
|
||||
mapPath($0, source: \.virtual, destination: \.local)
|
||||
}).sorted()
|
||||
}
|
||||
|
||||
func replace(localPaths: [String]) -> [String] {
|
||||
Set(localPaths.map {
|
||||
mapPath($0, source: \.local, destination: \.virtual)
|
||||
}).sorted()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
// Copyright (c) 2021 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Maps overlay's virtual URL with an actual (local) location
|
||||
struct OverlayMapping: Hashable {
|
||||
let virtual: URL
|
||||
let local: URL
|
||||
}
|
||||
|
||||
enum JsonOverlayReaderError: Error {
|
||||
/// The source file is missing
|
||||
case missingSourceFile(URL)
|
||||
/// The file exists but its content is invalid
|
||||
case invalidSourceContent(URL)
|
||||
/// the overlay format is not supported - either contains a nested directory or a single file
|
||||
case unsupportedFormat
|
||||
}
|
||||
/// Provides virtual file system overlay mappings
|
||||
protocol OverlayReader {
|
||||
func provideMappings() throws -> [OverlayMapping]
|
||||
}
|
||||
|
||||
class JsonOverlayReader: OverlayReader {
|
||||
|
||||
enum Mode {
|
||||
/// Interrupts the operation if the representation file is missing
|
||||
case strict
|
||||
/// Assume empty overlay mapping if the file doesn't exist
|
||||
case bestEffort
|
||||
}
|
||||
|
||||
private struct Overlay: Decodable {
|
||||
enum OverlayType: String, Decodable {
|
||||
case file
|
||||
case directory
|
||||
}
|
||||
|
||||
struct Content: Decodable {
|
||||
let externalContents: String
|
||||
let name: String
|
||||
let type: OverlayType
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case externalContents = "external-contents"
|
||||
case name
|
||||
case type
|
||||
}
|
||||
}
|
||||
|
||||
struct RootContent: Decodable {
|
||||
let contents: [Content]
|
||||
let name: String
|
||||
let type: OverlayType
|
||||
}
|
||||
let roots: [RootContent]
|
||||
}
|
||||
|
||||
private lazy var jsonDecoder = JSONDecoder()
|
||||
private let json: URL
|
||||
private let mode: Mode
|
||||
private let fileReader: FileReader
|
||||
|
||||
|
||||
init(_ json: URL, mode: Mode, fileReader: FileReader) {
|
||||
self.json = json
|
||||
self.mode = mode
|
||||
self.fileReader = fileReader
|
||||
}
|
||||
|
||||
func provideMappings() throws -> [OverlayMapping] {
|
||||
guard let jsonContent = try fileReader.contents(atPath: json.path) else {
|
||||
switch mode {
|
||||
case .strict:
|
||||
throw JsonOverlayReaderError.missingSourceFile(json)
|
||||
case .bestEffort:
|
||||
printWarning("overlay mapping file \(json) doesn't exist. Skipping overlay for the best-effort mode.")
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
let overlay: Overlay = try jsonDecoder.decode(Overlay.self, from: jsonContent)
|
||||
let mappings: [OverlayMapping] = try overlay.roots.reduce([]) { prev, root in
|
||||
switch root.type {
|
||||
case .directory:
|
||||
//iterate all contents
|
||||
let dir = URL(fileURLWithPath: root.name)
|
||||
let mappings: [OverlayMapping] = try root.contents.map { content in
|
||||
switch content.type {
|
||||
case .file:
|
||||
let virtual = dir.appendingPathComponent(content.name)
|
||||
let local = URL(fileURLWithPath: content.externalContents)
|
||||
return .init(virtual: virtual, local: local)
|
||||
case .directory:
|
||||
throw JsonOverlayReaderError.unsupportedFormat
|
||||
}
|
||||
|
||||
}
|
||||
return prev + mappings
|
||||
case .file:
|
||||
throw JsonOverlayReaderError.unsupportedFormat
|
||||
}
|
||||
}
|
||||
|
||||
return mappings
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2021 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import Foundation
|
||||
|
||||
final class IgnoringCertificatesTrustManager: NSObject, URLSessionDelegate {
|
||||
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
||||
guard let serverTrust = challenge.protectionSpace.serverTrust else {
|
||||
completionHandler(.performDefaultHandling, nil)
|
||||
return
|
||||
}
|
||||
|
||||
let urlCredential = URLCredential(trust: serverTrust)
|
||||
completionHandler(.useCredential, urlCredential)
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,15 @@ class DefaultURLSessionFactory: URLSessionFactory {
|
||||
let configuration = URLSessionConfiguration.default
|
||||
configuration.httpAdditionalHeaders = config.requestCustomHeaders
|
||||
configuration.timeoutIntervalForRequest = config.timeoutResponseDataChunksInterval
|
||||
return URLSession(configuration: configuration)
|
||||
switch config.disableCertificateVerification {
|
||||
case true:
|
||||
return URLSession(
|
||||
configuration: configuration,
|
||||
delegate: IgnoringCertificatesTrustManager(),
|
||||
delegateQueue: nil
|
||||
)
|
||||
case false:
|
||||
return URLSession(configuration: configuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ class ArtifactSwiftProductsBuilderImplTests: FileXCTestCase {
|
||||
private var swiftmoduleFile: URL!
|
||||
private var swiftmoduleDocFile: URL!
|
||||
private var swiftmoduleSourceInfoFile: URL!
|
||||
private var swiftmoduleInterfaceFile: URL!
|
||||
private var workingDir: URL!
|
||||
private var builder: ArtifactSwiftProductsBuilderImpl!
|
||||
|
||||
@@ -37,6 +38,7 @@ class ArtifactSwiftProductsBuilderImplTests: FileXCTestCase {
|
||||
swiftmoduleFile = moduleDir.appendingPathComponent("MyModule.swiftmodule")
|
||||
swiftmoduleDocFile = moduleDir.appendingPathComponent("MyModule.swiftdoc")
|
||||
swiftmoduleSourceInfoFile = moduleDir.appendingPathComponent("MyModule.swiftsourceinfo")
|
||||
swiftmoduleInterfaceFile = moduleDir.appendingPathComponent("MyModule.swiftinterface")
|
||||
workingDir = rootDir.appendingPathComponent("working")
|
||||
builder = ArtifactSwiftProductsBuilderImpl(
|
||||
workingDir: workingDir,
|
||||
@@ -69,7 +71,7 @@ class ArtifactSwiftProductsBuilderImplTests: FileXCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
func testIncludesAllSwiftmoduleFiles() throws {
|
||||
func testIncludesAllBasicSwiftmoduleFiles() throws {
|
||||
try fileManager.spt_createEmptyFile(swiftmoduleFile)
|
||||
try fileManager.spt_createEmptyFile(swiftmoduleDocFile)
|
||||
try fileManager.spt_createEmptyFile(swiftmoduleSourceInfoFile)
|
||||
@@ -91,6 +93,32 @@ class ArtifactSwiftProductsBuilderImplTests: FileXCTestCase {
|
||||
XCTAssertTrue(fileManager.fileExists(atPath: expectedBuildedSwiftSourceInfoFile.path))
|
||||
}
|
||||
|
||||
func testIncludesAllEvolutionEnabledSwiftmoduleFiles() throws {
|
||||
try fileManager.spt_createEmptyFile(swiftmoduleFile)
|
||||
try fileManager.spt_createEmptyFile(swiftmoduleDocFile)
|
||||
try fileManager.spt_createEmptyFile(swiftmoduleSourceInfoFile)
|
||||
try fileManager.spt_createEmptyFile(swiftmoduleInterfaceFile)
|
||||
let builderSwiftmoduleDir =
|
||||
builder
|
||||
.buildingArtifactSwiftModulesLocation()
|
||||
.appendingPathComponent("arm64")
|
||||
let expectedBuildedSwiftmoduleFile =
|
||||
builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftmodule")
|
||||
let expectedBuildedSwiftmoduledocFile =
|
||||
builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftdoc")
|
||||
let expectedBuildedSwiftSourceInfoFile =
|
||||
builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftsourceinfo")
|
||||
let expectedBuildedSwiftInterfaceFile =
|
||||
builderSwiftmoduleDir.appendingPathComponent("MyModule.swiftinterface")
|
||||
|
||||
try builder.includeModuleDefinitionsToTheArtifact(arch: "arm64", moduleURL: swiftmoduleFile)
|
||||
|
||||
XCTAssertTrue(fileManager.fileExists(atPath: expectedBuildedSwiftmoduleFile.path))
|
||||
XCTAssertTrue(fileManager.fileExists(atPath: expectedBuildedSwiftmoduledocFile.path))
|
||||
XCTAssertTrue(fileManager.fileExists(atPath: expectedBuildedSwiftSourceInfoFile.path))
|
||||
XCTAssertTrue(fileManager.fileExists(atPath: expectedBuildedSwiftInterfaceFile.path))
|
||||
}
|
||||
|
||||
func testFailsIncludingWhenMissingRequiredSwiftmoduleFiles() throws {
|
||||
XCTAssertThrowsError(
|
||||
try builder.includeModuleDefinitionsToTheArtifact(
|
||||
|
||||
@@ -31,6 +31,7 @@ class BuildArtifactCreatorTests: FileXCTestCase {
|
||||
private var swiftmoduleURL: URL!
|
||||
private var swiftdocURL: URL!
|
||||
private var swiftSourceInfoURL: URL!
|
||||
private var swiftInterfaceURL: URL!
|
||||
private var executablePath: String!
|
||||
private var executableURL: URL!
|
||||
private var creator: BuildArtifactCreator!
|
||||
@@ -50,6 +51,8 @@ class BuildArtifactCreatorTests: FileXCTestCase {
|
||||
.appendingPathComponent("Target.swiftdoc")
|
||||
swiftSourceInfoURL = workDirectory.appendingPathComponent("Objects-normal")
|
||||
.appendingPathComponent("Target.swiftsourceinfo")
|
||||
swiftInterfaceURL = workDirectory.appendingPathComponent("Objects-normal")
|
||||
.appendingPathComponent("Target.swiftinterface")
|
||||
executablePath = "libTarget.a"
|
||||
executableURL = buildDir.appendingPathComponent(executablePath)
|
||||
dSYM = executableURL.deletingPathExtension().appendingPathExtension(".dSYM")
|
||||
@@ -114,6 +117,28 @@ class BuildArtifactCreatorTests: FileXCTestCase {
|
||||
])
|
||||
}
|
||||
|
||||
func testPackagesEvolutionEnabledSwiftmoduleFiles() throws {
|
||||
try fileManager.spt_createEmptyFile(swiftmoduleURL)
|
||||
try fileManager.spt_createEmptyFile(swiftdocURL)
|
||||
try fileManager.spt_createEmptyFile(swiftSourceInfoURL)
|
||||
try fileManager.spt_createEmptyFile(swiftInterfaceURL)
|
||||
|
||||
try creator.includeModuleDefinitionsToTheArtifact(arch: "arch", moduleURL: swiftmoduleURL)
|
||||
let artifact = try creator.createArtifact(artifactKey: "key", meta: sampleMeta)
|
||||
|
||||
let unzippedURL = workDirectory.appendingPathComponent(UUID().uuidString)
|
||||
try Zip.unzipFile(artifact.package, destination: unzippedURL, overwrite: true, password: nil, progress: nil)
|
||||
let allFiles = try fileManager.spt_allFilesRecusively(unzippedURL)
|
||||
XCTAssertEqual(Set(allFiles), [
|
||||
unzippedURL.appendingPathComponent("libTarget.a"),
|
||||
unzippedURL.appendingPathComponent("fileKey.json"),
|
||||
unzippedURL.appendingPathComponent("swiftmodule/arch/Target.swiftmodule"),
|
||||
unzippedURL.appendingPathComponent("swiftmodule/arch/Target.swiftdoc"),
|
||||
unzippedURL.appendingPathComponent("swiftmodule/arch/Target.swiftsourceinfo"),
|
||||
unzippedURL.appendingPathComponent("swiftmodule/arch/Target.swiftinterface"),
|
||||
])
|
||||
}
|
||||
|
||||
func testFailsPackageWhenSwiftmoduleRelatedFilesAreMissing() throws {
|
||||
// Creating only `Target.swiftmodule`, without `.swiftdoc`
|
||||
try fileManager.spt_createEmptyFile(swiftmoduleURL)
|
||||
|
||||
@@ -51,7 +51,8 @@ class PostbuildTests: FileXCTestCase {
|
||||
derivedSourcesDir: "",
|
||||
thinnedTargets: [],
|
||||
action: .build,
|
||||
modeMarkerPath: ""
|
||||
modeMarkerPath: "",
|
||||
overlayHeadersPath: ""
|
||||
)
|
||||
private var network = RemoteNetworkClientImpl(
|
||||
NetworkClientFake(fileManager: .default),
|
||||
|
||||
@@ -62,7 +62,8 @@ class PrebuildTests: FileXCTestCase {
|
||||
forceCached: false,
|
||||
compilationHistoryFile: compilationHistory,
|
||||
turnOffRemoteCacheOnFirstTimeout: true,
|
||||
targetName: ""
|
||||
targetName: "",
|
||||
overlayHeadersPath: ""
|
||||
)
|
||||
contextCached = PrebuildContext(
|
||||
targetTempDir: sampleURL,
|
||||
@@ -74,7 +75,8 @@ class PrebuildTests: FileXCTestCase {
|
||||
forceCached: true,
|
||||
compilationHistoryFile: compilationHistory,
|
||||
turnOffRemoteCacheOnFirstTimeout: true,
|
||||
targetName: ""
|
||||
targetName: "",
|
||||
overlayHeadersPath: ""
|
||||
)
|
||||
organizer = ArtifactOrganizerFake(artifactRoot: artifactsRoot, unzippedExtension: "unzip")
|
||||
globalCacheSwitcher = InMemoryGlobalCacheSwitcher()
|
||||
@@ -238,7 +240,8 @@ class PrebuildTests: FileXCTestCase {
|
||||
forceCached: false,
|
||||
compilationHistoryFile: compilationHistory,
|
||||
turnOffRemoteCacheOnFirstTimeout: true,
|
||||
targetName: ""
|
||||
targetName: "",
|
||||
overlayHeadersPath: ""
|
||||
)
|
||||
|
||||
let prebuild = Prebuild(
|
||||
@@ -268,7 +271,8 @@ class PrebuildTests: FileXCTestCase {
|
||||
forceCached: false,
|
||||
compilationHistoryFile: compilationHistory,
|
||||
turnOffRemoteCacheOnFirstTimeout: true,
|
||||
targetName: ""
|
||||
targetName: "",
|
||||
overlayHeadersPath: ""
|
||||
)
|
||||
metaContent = try generateMeta(fingerprint: generator.generate(), filekey: "1")
|
||||
let downloadedArtifactPackage = artifactsRoot.appendingPathComponent("1")
|
||||
@@ -330,7 +334,8 @@ class PrebuildTests: FileXCTestCase {
|
||||
forceCached: false,
|
||||
compilationHistoryFile: compilationHistory,
|
||||
turnOffRemoteCacheOnFirstTimeout: false,
|
||||
targetName: ""
|
||||
targetName: "",
|
||||
overlayHeadersPath: ""
|
||||
)
|
||||
try globalCacheSwitcher.enable(sha: "1")
|
||||
let prebuild = Prebuild(
|
||||
|
||||
@@ -280,6 +280,9 @@ class SwiftcTests: FileXCTestCase {
|
||||
let artifactSwiftSourceInfo = URL(
|
||||
fileURLWithPath: "/cachedArtifact/swiftmodule/archTest/Target.swiftsourceinfo"
|
||||
)
|
||||
let artifactSwiftInterfaceInfo = URL(
|
||||
fileURLWithPath: "/cachedArtifact/swiftmodule/archTest/Target.swiftinterface"
|
||||
)
|
||||
|
||||
artifactOrganizer = ArtifactOrganizerFake(artifactRoot: artifactRoot)
|
||||
let swiftc = Swiftc(
|
||||
@@ -303,12 +306,14 @@ class SwiftcTests: FileXCTestCase {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
@testable import XCRemoteCache
|
||||
import XCTest
|
||||
|
||||
class DependencyProcessorImplTests: XCTestCase {
|
||||
class DependencyProcessorImplTests: FileXCTestCase {
|
||||
|
||||
let processor = DependencyProcessorImpl(
|
||||
xcode: "/Xcode",
|
||||
@@ -70,6 +70,14 @@ class DependencyProcessorImplTests: XCTestCase {
|
||||
XCTAssertEqual(dependencies, [])
|
||||
}
|
||||
|
||||
func testDoesNotFilterOutOtherProductModulemap() throws {
|
||||
let dependencies = processor.process([
|
||||
"/ProductOther/some.modulemap",
|
||||
])
|
||||
|
||||
XCTAssertEqual(dependencies, [.init(url: "/ProductOther/some.modulemap", type: .unknown)])
|
||||
}
|
||||
|
||||
func testDoesNotFilterOutNonProductModulemap() throws {
|
||||
let dependencies = processor.process([
|
||||
"/Source/some.modulemap",
|
||||
@@ -109,4 +117,66 @@ class DependencyProcessorImplTests: XCTestCase {
|
||||
|
||||
XCTAssertEqual(dependencies, [.init(url: "/xxx/some", type: .unknown)])
|
||||
}
|
||||
|
||||
func testFiltersOutIntermediateBySymlink() throws {
|
||||
let sampleDir = try prepareTempDir()
|
||||
|
||||
let intermediateDirReal = sampleDir.appendingPathComponent("Intermediate")
|
||||
let symlink = sampleDir.appendingPathComponent("symlink")
|
||||
let someFilename = "some"
|
||||
|
||||
let processor = DependencyProcessorImpl(
|
||||
xcode: "/Xcode",
|
||||
product: "/Product",
|
||||
source: "/Source",
|
||||
intermediate: intermediateDirReal,
|
||||
bundle: "/Bundle"
|
||||
)
|
||||
|
||||
let intermediateFileSymlink = createSymlink(filename: someFilename, sourceDir: symlink, destinationDir: intermediateDirReal)
|
||||
|
||||
let dependencies = processor.process([
|
||||
intermediateFileSymlink
|
||||
])
|
||||
|
||||
XCTAssertEqual(dependencies, [])
|
||||
}
|
||||
|
||||
func testDoesNotFilterOutSourceBySymlink() throws {
|
||||
let sampleDir = try prepareTempDir()
|
||||
|
||||
let sourceDirReal = sampleDir.appendingPathComponent("Source")
|
||||
let symlink = sampleDir.appendingPathComponent("symlink")
|
||||
let someFilename = "some"
|
||||
|
||||
let processor = DependencyProcessorImpl(
|
||||
xcode: "/Xcode",
|
||||
product: "/Product",
|
||||
source: sourceDirReal,
|
||||
intermediate: "/Intermediate",
|
||||
bundle: "/Bundle"
|
||||
)
|
||||
|
||||
let sourceFileSymlink = createSymlink(filename: someFilename, sourceDir: symlink, destinationDir: sourceDirReal)
|
||||
|
||||
let dependencies = processor.process([
|
||||
sourceFileSymlink
|
||||
])
|
||||
|
||||
XCTAssertEqual(dependencies, [.init(url: sourceFileSymlink, type: .source)])
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates Symlink from sourceDir to destinationDir and creates empty file inside it
|
||||
* return URL with symbolic link from sourceDir to destinationDir
|
||||
*/
|
||||
fileprivate func createSymlink(filename: String, sourceDir: URL, destinationDir: URL) -> URL {
|
||||
let fileMng = FileManager.default
|
||||
|
||||
XCTAssertNoThrow(try fileMng.spt_forceSymbolicLink(at: sourceDir,
|
||||
withDestinationURL: destinationDir))
|
||||
XCTAssertNoThrow(try fileMng.spt_createEmptyFile(destinationDir.appendingPathComponent(filename)))
|
||||
|
||||
return sourceDir.appendingPathComponent(filename)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) 2021 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
@testable import XCRemoteCache
|
||||
import XCTest
|
||||
|
||||
class OverlayDependenciesRemapperTests: XCTestCase {
|
||||
private let overlayReader = OverlayReaderFake(
|
||||
mappings: [.init(virtual: "/file.h", local: "/Intermediate/Some/file.h")]
|
||||
)
|
||||
|
||||
func testMappingFromLocalToGeneric() throws {
|
||||
let reader = try OverlayDependenciesRemapper(
|
||||
overlayReader: overlayReader
|
||||
)
|
||||
|
||||
let dependencies = reader.replace(localPaths: ["/Intermediate/Some/file.h"])
|
||||
XCTAssertEqual(dependencies, ["/file.h"])
|
||||
}
|
||||
|
||||
func testMappingFromGenericToLocal() throws {
|
||||
let reader = try OverlayDependenciesRemapper(
|
||||
overlayReader: overlayReader
|
||||
)
|
||||
|
||||
let dependencies = reader.replace(genericPaths: ["/file.h"])
|
||||
XCTAssertEqual(dependencies, ["/Intermediate/Some/file.h"])
|
||||
}
|
||||
|
||||
func testGenericDependenciesAreMerged() throws {
|
||||
|
||||
let reader = try OverlayDependenciesRemapper(
|
||||
overlayReader: overlayReader
|
||||
)
|
||||
|
||||
let dependencies = reader.replace(localPaths: ["/Intermediate/Some/file.h", "/file.h"])
|
||||
XCTAssertEqual(dependencies, ["/file.h"])
|
||||
}
|
||||
|
||||
func testLocalDependenciesAreMerged() throws {
|
||||
let reader = try OverlayDependenciesRemapper(
|
||||
overlayReader: overlayReader
|
||||
)
|
||||
|
||||
let dependencies = reader.replace(genericPaths: ["/Intermediate/Some/file.h", "/file.h"])
|
||||
XCTAssertEqual(dependencies, ["/Intermediate/Some/file.h"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) 2021 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
@testable import XCRemoteCache
|
||||
import XCTest
|
||||
|
||||
class JsonOverlayReaderTests: XCTestCase {
|
||||
private static let resourcesSubdirectory = "TestData/Dependencies/JsonOverlayReaderTests"
|
||||
|
||||
func testParsingWithSuccess() throws {
|
||||
let file = try pathForTestData(name: "overlayReaderDefault")
|
||||
let reader = JsonOverlayReader(file, mode: .strict, fileReader: FileManager.default)
|
||||
let mappings = try reader.provideMappings()
|
||||
|
||||
let expectedMappings = [
|
||||
OverlayMapping(virtual: "/DerivedDataProducts/Target1.framework/Headers/Target1.h", local: "/Path/Target1/Target1.h"),
|
||||
OverlayMapping(virtual: "/DerivedDataProducts/Target2.framework/Modules/module.modulemap", local: "/DerivedDataIntermediate/Target2.build/module.modulemap")
|
||||
]
|
||||
XCTAssertEqual(Set(mappings), Set(expectedMappings))
|
||||
}
|
||||
|
||||
func testFailsWithMissingFileForStrictMode() throws {
|
||||
let file: URL = "nonExiting"
|
||||
let reader = JsonOverlayReader(file, mode: .strict, fileReader: FileManager.default)
|
||||
|
||||
XCTAssertThrowsError(try reader.provideMappings())
|
||||
}
|
||||
|
||||
func testReturnsEmpptyMappingForMissingFileForBestEffortMode() throws {
|
||||
let file: URL = "nonExiting"
|
||||
let reader = JsonOverlayReader(file, mode: .bestEffort, fileReader: FileManager.default)
|
||||
|
||||
let mappings = try reader.provideMappings()
|
||||
|
||||
XCTAssertEqual(mappings, [])
|
||||
}
|
||||
|
||||
func testParsingEmptyOverlay() throws {
|
||||
let file = try pathForTestData(name: "overlayReaderEmpty")
|
||||
let reader = JsonOverlayReader(file, mode: .strict, fileReader: FileManager.default)
|
||||
let mappings = try reader.provideMappings()
|
||||
|
||||
XCTAssertEqual(mappings, [])
|
||||
}
|
||||
|
||||
private func pathForTestData(name: String) throws -> URL {
|
||||
return try XCTUnwrap(Bundle.module.url(forResource: name, withExtension: "json", subdirectory: JsonOverlayReaderTests.resourcesSubdirectory))
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"case-sensitive": "false",
|
||||
"roots":
|
||||
[
|
||||
{
|
||||
"contents":
|
||||
[
|
||||
{
|
||||
"external-contents": "/Path/Target1/Target1.h",
|
||||
"name": "Target1.h",
|
||||
"type": "file",
|
||||
},
|
||||
],
|
||||
"name": "/DerivedDataProducts/Target1.framework/Headers",
|
||||
"type": "directory",
|
||||
},
|
||||
{
|
||||
"contents":
|
||||
[
|
||||
{
|
||||
"external-contents": "/DerivedDataIntermediate/Target2.build/module.modulemap",
|
||||
"name": "module.modulemap",
|
||||
"type": "file",
|
||||
},
|
||||
],
|
||||
"name": "/DerivedDataProducts/Target2.framework/Modules",
|
||||
"type": "directory",
|
||||
},
|
||||
],
|
||||
"version": 0,
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"case-sensitive":"false","roots":[],"version":0}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2021 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import Foundation
|
||||
@testable import XCRemoteCache
|
||||
|
||||
class OverlayReaderFake: OverlayReader {
|
||||
private let mappings: [OverlayMapping]
|
||||
init(mappings: [OverlayMapping]) {
|
||||
self.mappings = mappings
|
||||
}
|
||||
|
||||
func provideMappings() throws -> [OverlayMapping] {
|
||||
return mappings
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,7 @@ An object that is passed to the `xcremotecache` can contain all properties suppo
|
||||
| `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` | ⬜️ |
|
||||
| `disable_certificate_verification` | A Boolean value that opts-in SSL certificate validation is disabled | `false` | ⬜️ |
|
||||
|
||||
## Uninstalling
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ module CocoapodsXCRemoteCacheModifier
|
||||
LLDB_INIT_PATH = "#{ENV['HOME']}/.lldbinit"
|
||||
FAT_ARCHIVE_NAME_INFIX = 'arm64-x86_64'
|
||||
|
||||
# List of plugins' user properties that should be copied to .rcinfo
|
||||
CUSTOM_CONFIGURATION_KEYS = [
|
||||
'enabled',
|
||||
'xcrc_location',
|
||||
@@ -37,7 +38,9 @@ module CocoapodsXCRemoteCacheModifier
|
||||
'final_target',
|
||||
'check_build_configuration',
|
||||
'check_platform',
|
||||
'modify_lldb_init'
|
||||
'modify_lldb_init',
|
||||
'prettify_meta_files',
|
||||
'disable_certificate_verification'
|
||||
]
|
||||
|
||||
class XCRemoteCache
|
||||
@@ -59,7 +62,8 @@ module CocoapodsXCRemoteCacheModifier
|
||||
'xccc_file' => "#{BIN_DIR}/xccc",
|
||||
'remote_commit_file' => "#{BIN_DIR}/arc.rc",
|
||||
'exclude_targets' => [],
|
||||
'prettify_meta_files' => false
|
||||
'prettify_meta_files' => false,
|
||||
'disable_certificate_verification' => false
|
||||
}
|
||||
@@configuration.merge! default_values.select { |k, v| !@@configuration.key?(k) }
|
||||
end
|
||||
@@ -78,8 +82,8 @@ module CocoapodsXCRemoteCacheModifier
|
||||
end
|
||||
|
||||
mode = @@configuration['mode']
|
||||
unless mode == 'consumer' || mode == 'producer'
|
||||
throw "Incorrect 'mode' value. Allowed values are ['consumer', 'producer'], but you provided '#{mode}'. A typo?"
|
||||
unless mode == 'consumer' || mode == 'producer' || mode == 'producer-fast'
|
||||
throw "Incorrect 'mode' value. Allowed values are ['consumer', 'producer', 'producer-fast'], but you provided '#{mode}'. A typo?"
|
||||
end
|
||||
|
||||
unless mode == 'consumer' || @@configuration.key?('final_target')
|
||||
@@ -99,7 +103,7 @@ module CocoapodsXCRemoteCacheModifier
|
||||
# @param repo_distance [Integer] distance from the git repo root to the target's $SRCROOT
|
||||
# @param xc_location [String] path to the dir with all XCRemoteCache binaries, relative to the repo root
|
||||
# @param xc_cc_path [String] path to the XCRemoteCache clang wrapper, relative to the repo root
|
||||
# @param mode [String] mode name ('consumer', 'producer' etc.)
|
||||
# @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)
|
||||
@@ -110,6 +114,8 @@ module CocoapodsXCRemoteCacheModifier
|
||||
next if exclude_build_configurations.include?(config.name)
|
||||
if mode == 'consumer'
|
||||
config.build_settings['CC'] = ["$SRCROOT/#{parent_dir(xc_cc_path, repo_distance)}"]
|
||||
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"]
|
||||
@@ -140,7 +146,7 @@ module CocoapodsXCRemoteCacheModifier
|
||||
|
||||
# Move prebuild (last element) to the first position (to make it real 'prebuild')
|
||||
target.build_phases.rotate!(-1) if existing_prebuild_script.nil?
|
||||
elsif mode == 'producer'
|
||||
elsif mode == 'producer' || mode == 'producer-fast'
|
||||
# Delete existing prebuild build phase (to support switching between modes)
|
||||
target.build_phases.delete_if do |phase|
|
||||
if phase.respond_to?(:name)
|
||||
@@ -165,7 +171,7 @@ module CocoapodsXCRemoteCacheModifier
|
||||
postbuild_script.dependency_file = "$(TARGET_TEMP_DIR)/postbuild.d"
|
||||
|
||||
# Mark a sha as ready for a given platform and configuration when building the final_target
|
||||
if mode == 'producer' && target.name == final_target
|
||||
if (mode == 'producer' || mode == 'producer-fast') && target.name == final_target
|
||||
existing_mark_script = target.build_phases.detect do |phase|
|
||||
if phase.respond_to?(:name)
|
||||
phase.name != nil && phase.name.start_with?("[XCRC] Mark")
|
||||
@@ -190,8 +196,9 @@ module CocoapodsXCRemoteCacheModifier
|
||||
config.build_settings.delete('SWIFT_EXEC') if config.build_settings.key?('SWIFT_EXEC')
|
||||
config.build_settings.delete('LIBTOOL') if config.build_settings.key?('LIBTOOL')
|
||||
config.build_settings.delete('LD') if config.build_settings.key?('LD')
|
||||
# Add Fake src root for ObjC & Swift
|
||||
# Remove Fake src root for ObjC & Swift
|
||||
config.build_settings.delete('XCREMOTE_CACHE_FAKE_SRCROOT')
|
||||
config.build_settings.delete('XCRC_PLATFORM_PREFERRED_ARCH')
|
||||
remove_cflags!(config.build_settings, '-fdebug-prefix-map')
|
||||
remove_swiftflags!(config.build_settings, '-debug-prefix-map')
|
||||
end
|
||||
@@ -253,14 +260,14 @@ module CocoapodsXCRemoteCacheModifier
|
||||
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}"
|
||||
return if options.fetch('OTHER_CFLAGS',[]).include?(value)
|
||||
options['OTHER_CFLAGS'] = remove_cflags!(options, key) << "#{key}=#{value}"
|
||||
end
|
||||
|
||||
def self.remove_cflags!(options, key)
|
||||
cflags_arr = options.fetch('OTHER_CFLAGS', ['$(inherited)'])
|
||||
cflags_arr = [cflags_arr] if cflags_arr.kind_of? String
|
||||
options['OTHER_CFLAGS'] = cflags_arr.delete_if {|flag| flag.start_with?(" #{key}=") }
|
||||
options['OTHER_CFLAGS'] = cflags_arr.delete_if {|flag| flag.include?("#{key}=") }
|
||||
options['OTHER_CFLAGS']
|
||||
end
|
||||
|
||||
@@ -275,12 +282,27 @@ module CocoapodsXCRemoteCacheModifier
|
||||
end
|
||||
|
||||
# Uninstall the XCRemoteCache
|
||||
def self.disable_xcremotecache(user_project)
|
||||
def self.disable_xcremotecache(user_project, pods_project = nil)
|
||||
user_project.targets.each do |target|
|
||||
disable_xcremotecache_for_target(target)
|
||||
end
|
||||
user_project.save()
|
||||
|
||||
unless pods_project.nil?
|
||||
pods_project.native_targets.each do |target|
|
||||
disable_xcremotecache_for_target(target)
|
||||
end
|
||||
pods_proj_directory = pods_project.project_dir
|
||||
pods_project.root_object.project_references.each do |subproj_ref|
|
||||
generated_project = Xcodeproj::Project.open("#{pods_proj_directory}/#{subproj_ref[:project_ref].path}")
|
||||
generated_project.native_targets.each do |target|
|
||||
disable_xcremotecache_for_target(target)
|
||||
end
|
||||
generated_project.save()
|
||||
end
|
||||
pods_project.save()
|
||||
end
|
||||
|
||||
# Remove .lldbinit rewrite
|
||||
save_lldbinit_rewrite(nil) unless !@@configuration['modify_lldb_init']
|
||||
end
|
||||
@@ -337,7 +359,6 @@ module CocoapodsXCRemoteCacheModifier
|
||||
end
|
||||
|
||||
validate_configuration()
|
||||
|
||||
mode = @@configuration['mode']
|
||||
xccc_location = @@configuration['xccc_file']
|
||||
remote_commit_file = @@configuration['remote_commit_file']
|
||||
@@ -372,7 +393,9 @@ module CocoapodsXCRemoteCacheModifier
|
||||
# Always integrate XCRemoteCache to all Pods, in case it will be needed later
|
||||
unless installer_context.pods_project.nil?
|
||||
# Attach XCRemoteCache to Pods targets
|
||||
installer_context.pods_project.targets.each do |target|
|
||||
# Enable only for native targets which can have compilation steps
|
||||
installer_context.pods_project.native_targets.each do |target|
|
||||
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)
|
||||
@@ -382,6 +405,18 @@ module CocoapodsXCRemoteCacheModifier
|
||||
# Create .rcinfo into `Pods` directory as that .xcodeproj reads configuration from .xcodeproj location
|
||||
pods_proj_directory = installer_context.sandbox_root
|
||||
|
||||
# Attach XCRemoteCache to Generated Pods projects
|
||||
installer_context.pods_project.root_object.project_references.each do |subproj_ref|
|
||||
generated_project = Xcodeproj::Project.open("#{pods_proj_directory}/#{subproj_ref[:project_ref].path}")
|
||||
generated_project.native_targets.each do |target|
|
||||
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)
|
||||
end
|
||||
generated_project.save()
|
||||
end
|
||||
|
||||
# Manual Pods/.rcinfo generation
|
||||
|
||||
# all paths in .rcinfo are relative to the root so paths used in Pods.xcodeproj need to be aligned
|
||||
@@ -403,12 +438,12 @@ module CocoapodsXCRemoteCacheModifier
|
||||
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
|
||||
disable_xcremotecache(user_project)
|
||||
disable_xcremotecache(user_project, installer_context.pods_project)
|
||||
Pod::UI.puts "[XCRC] XCRemoteCache disabled - no artifacts available"
|
||||
next
|
||||
end
|
||||
rescue => error
|
||||
disable_xcremotecache(user_project)
|
||||
disable_xcremotecache(user_project, installer_context.pods_project)
|
||||
Pod::UI.puts "[XCRC] XCRemoteCache failed with an error: #{error}."
|
||||
next
|
||||
end
|
||||
@@ -433,7 +468,7 @@ module CocoapodsXCRemoteCacheModifier
|
||||
rescue Exception => e
|
||||
Pod::UI.puts "[XCRC] XCRemoteCache disabled with error: #{e}"
|
||||
puts e.full_message(highlight: true, order: :top)
|
||||
disable_xcremotecache(user_project)
|
||||
disable_xcremotecache(user_project, installer_context.pods_project)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
# limitations under the License.
|
||||
|
||||
module CocoapodsXcremotecache
|
||||
VERSION = "0.0.4"
|
||||
VERSION = "0.0.6"
|
||||
end
|
||||
|
||||
+12
-1
@@ -25,7 +25,18 @@ The generated Xcode project contains schemes for each output application (like `
|
||||
|
||||
#### Running tests in Xcode
|
||||
|
||||
All unit tests are placed in `XCRemoteCacheTests`. To run them from Xcode, just pick any application scheme and run tests (⌘+U).
|
||||
All unit tests are placed in `XCRemoteCacheTests`. To run them from Xcode, just pick any application scheme and run tests (⌘+U).
|
||||
|
||||
#### Running E2E tests
|
||||
|
||||
E2E tests build a CocoaPods plugin, locally build both `producer` and `consumer` modes and verify 100% hit rate. All `Podfile` templates are placed in [e2eTests/tests](../e2eTests/tests). As a backend server, nginx server with a sample [nginx.conf](../e2eTests/nginx/nginx.conf) configuration is used. The sample server exposes local location `/tmp/cache` under http://localhost:8080.
|
||||
|
||||
To run tests locally, install `nginx` (e.g. `brew install nginx`) and call:
|
||||
|
||||
```bash
|
||||
rake 'build[release]'
|
||||
rake e2e_only
|
||||
```
|
||||
|
||||
## Project organization
|
||||
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 55;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
3695D9CC27A3218C007F3792 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3695D9CB27A3218C007F3792 /* AppDelegate.swift */; };
|
||||
3695D9CE27A3218C007F3792 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3695D9CD27A3218C007F3792 /* SceneDelegate.swift */; };
|
||||
3695D9D027A3218C007F3792 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3695D9CF27A3218C007F3792 /* ViewController.swift */; };
|
||||
3695D9D327A3218C007F3792 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3695D9D127A3218C007F3792 /* Main.storyboard */; };
|
||||
3695D9D527A3218D007F3792 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3695D9D427A3218D007F3792 /* Assets.xcassets */; };
|
||||
3695D9D827A3218D007F3792 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3695D9D627A3218D007F3792 /* LaunchScreen.storyboard */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
3695D9C827A3218C007F3792 /* XCRemoteCacheSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = XCRemoteCacheSample.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3695D9CB27A3218C007F3792 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
3695D9CD27A3218C007F3792 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||
3695D9CF27A3218C007F3792 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
|
||||
3695D9D227A3218C007F3792 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
3695D9D427A3218D007F3792 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
3695D9D727A3218D007F3792 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
3695D9D927A3218D007F3792 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
3695D9C527A3218C007F3792 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
3695D9BF27A3218C007F3792 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3695D9CA27A3218C007F3792 /* XCRemoteCacheSample */,
|
||||
3695D9C927A3218C007F3792 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3695D9C927A3218C007F3792 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3695D9C827A3218C007F3792 /* XCRemoteCacheSample.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3695D9CA27A3218C007F3792 /* XCRemoteCacheSample */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3695D9CB27A3218C007F3792 /* AppDelegate.swift */,
|
||||
3695D9CD27A3218C007F3792 /* SceneDelegate.swift */,
|
||||
3695D9CF27A3218C007F3792 /* ViewController.swift */,
|
||||
3695D9D127A3218C007F3792 /* Main.storyboard */,
|
||||
3695D9D427A3218D007F3792 /* Assets.xcassets */,
|
||||
3695D9D627A3218D007F3792 /* LaunchScreen.storyboard */,
|
||||
3695D9D927A3218D007F3792 /* Info.plist */,
|
||||
);
|
||||
path = XCRemoteCacheSample;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
3695D9C727A3218C007F3792 /* XCRemoteCacheSample */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 3695D9DC27A3218D007F3792 /* Build configuration list for PBXNativeTarget "XCRemoteCacheSample" */;
|
||||
buildPhases = (
|
||||
3695D9C427A3218C007F3792 /* Sources */,
|
||||
3695D9C527A3218C007F3792 /* Frameworks */,
|
||||
3695D9C627A3218C007F3792 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = XCRemoteCacheSample;
|
||||
productName = XCRemoteCacheSample;
|
||||
productReference = 3695D9C827A3218C007F3792 /* XCRemoteCacheSample.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
3695D9C027A3218C007F3792 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1320;
|
||||
LastUpgradeCheck = 1320;
|
||||
TargetAttributes = {
|
||||
3695D9C727A3218C007F3792 = {
|
||||
CreatedOnToolsVersion = 13.2.1;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 3695D9C327A3218C007F3792 /* Build configuration list for PBXProject "XCRemoteCacheSample" */;
|
||||
compatibilityVersion = "Xcode 13.0";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 3695D9BF27A3218C007F3792;
|
||||
productRefGroup = 3695D9C927A3218C007F3792 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
3695D9C727A3218C007F3792 /* XCRemoteCacheSample */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
3695D9C627A3218C007F3792 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
3695D9D827A3218D007F3792 /* LaunchScreen.storyboard in Resources */,
|
||||
3695D9D527A3218D007F3792 /* Assets.xcassets in Resources */,
|
||||
3695D9D327A3218C007F3792 /* Main.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
3695D9C427A3218C007F3792 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
3695D9D027A3218C007F3792 /* ViewController.swift in Sources */,
|
||||
3695D9CC27A3218C007F3792 /* AppDelegate.swift in Sources */,
|
||||
3695D9CE27A3218C007F3792 /* SceneDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
3695D9D127A3218C007F3792 /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
3695D9D227A3218C007F3792 /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3695D9D627A3218D007F3792 /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
3695D9D727A3218D007F3792 /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
3695D9DA27A3218D007F3792 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.2;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
3695D9DB27A3218D007F3792 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.2;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
3695D9DD27A3218D007F3792 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = XCRemoteCacheSample/Info.plist;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||
INFOPLIST_KEY_UIMainStoryboardFile = Main;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.xcremotecache.XCRemoteCacheSample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
3695D9DE27A3218D007F3792 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = XCRemoteCacheSample/Info.plist;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||
INFOPLIST_KEY_UIMainStoryboardFile = Main;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.xcremotecache.XCRemoteCacheSample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
3695D9C327A3218C007F3792 /* Build configuration list for PBXProject "XCRemoteCacheSample" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
3695D9DA27A3218D007F3792 /* Debug */,
|
||||
3695D9DB27A3218D007F3792 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
3695D9DC27A3218D007F3792 /* Build configuration list for PBXNativeTarget "XCRemoteCacheSample" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
3695D9DD27A3218D007F3792 /* Debug */,
|
||||
3695D9DE27A3218D007F3792 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 3695D9C027A3218C007F3792 /* Project object */;
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2021 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: UISceneSession Lifecycle
|
||||
|
||||
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
||||
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="ViewController" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>Default Configuration</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||
<key>UISceneStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2021 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import UIKit
|
||||
|
||||
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
var window: UIWindow?
|
||||
|
||||
|
||||
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
||||
}
|
||||
|
||||
func sceneDidDisconnect(_ scene: UIScene) {
|
||||
}
|
||||
|
||||
func sceneDidBecomeActive(_ scene: UIScene) {
|
||||
}
|
||||
|
||||
func sceneWillResignActive(_ scene: UIScene) {
|
||||
}
|
||||
|
||||
func sceneWillEnterForeground(_ scene: UIScene) {
|
||||
}
|
||||
|
||||
func sceneDidEnterBackground(_ scene: UIScene) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) 2021 Spotify AB.
|
||||
//
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import UIKit
|
||||
|
||||
class ViewController: UIViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# Nginx configuration used for E2E tests cache server
|
||||
# Listens HTTP on port 8080
|
||||
|
||||
|
||||
worker_processes 1;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
|
||||
http {
|
||||
default_type application/octet-stream;
|
||||
|
||||
server {
|
||||
listen 8080;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
root html;
|
||||
index index.html index.htm;
|
||||
}
|
||||
|
||||
location /cache/ {
|
||||
# The path to the directory where nginx should store the cache contents.
|
||||
root /tmp/cache;
|
||||
# Allow PUT
|
||||
dav_methods PUT;
|
||||
create_full_put_path on;
|
||||
# The maximum size of a single file.
|
||||
client_max_body_size 1G;
|
||||
allow all;
|
||||
}
|
||||
sendfile on;
|
||||
|
||||
keepalive_timeout 65;
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root html;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
plugin 'cocoapods-xcremotecache'
|
||||
|
||||
target 'XCRemoteCacheSample' do
|
||||
use_frameworks!
|
||||
|
||||
pod 'Firebase/Analytics'
|
||||
pod 'ReactiveSwift'
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
plugin 'cocoapods-xcremotecache'
|
||||
|
||||
install! 'cocoapods', :generate_multiple_pod_projects => true
|
||||
|
||||
|
||||
target 'XCRemoteCacheSample' do
|
||||
use_frameworks!
|
||||
|
||||
pod 'Firebase/Analytics'
|
||||
pod 'ReactiveSwift'
|
||||
end
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
require 'json'
|
||||
|
||||
desc 'Support for E2E tests: building XCRemoteCache-enabled xcodeproj using xcodebuild'
|
||||
namespace :e2e do
|
||||
COCOAPODS_DIR = 'cocoapods-plugin'
|
||||
COCOAPODS_GEMSPEC_FILENAME = "cocoapods-xcremotecache.gemspec"
|
||||
E2E_COCOAPODS_SAMPLE_DIR = 'e2eTests/XCRemoteCacheSample'
|
||||
GIT_REMOTE_NAME = 'self'
|
||||
# Location of the remote address that points to itself
|
||||
GIT_REMOTE_ADDRESS = '.'
|
||||
GIT_BRANCH = 'e2e-test-branch'
|
||||
LOG_NAME = "xcodebuild.log"
|
||||
DERIVED_DATA_PATH = './DerivedData'
|
||||
NGINX_ROOT_DIR = '/tmp/cache'
|
||||
XCRC_BINARIES = 'XCRC'
|
||||
SHARED_COCOAPODS_CONFIG = {
|
||||
'cache_addresses' => ['http://localhost:8080/cache/pods'],
|
||||
'primary_repo' => GIT_REMOTE_ADDRESS,
|
||||
'primary_branch' => GIT_BRANCH,
|
||||
'mode' => 'consumer',
|
||||
'final_target' => 'XCRemoteCacheSample',
|
||||
'artifact_maximum_age' => 0
|
||||
}
|
||||
|
||||
Stats = Struct.new(:hits, :misses, :hit_rate)
|
||||
|
||||
# run E2E tests
|
||||
task :run => [:run_cocoapods]
|
||||
|
||||
# run E2E tests for CocoaPods-powered projects
|
||||
task :run_cocoapods do
|
||||
install_cocoapods_plugin
|
||||
start_nginx
|
||||
configure_git
|
||||
|
||||
# Run scenarios for all Podfile scenarios
|
||||
for podfile_path in Dir.glob('e2eTests/**/*.Podfile')
|
||||
run_cocoapods_scenario(podfile_path)
|
||||
end
|
||||
# Revert all side effects
|
||||
clean
|
||||
end
|
||||
|
||||
# Build and install a plugin
|
||||
def self.install_cocoapods_plugin
|
||||
Dir.chdir(COCOAPODS_DIR) do
|
||||
gemfile_path = "cocoapods-xcremotecache.gem"
|
||||
system("gem build #{COCOAPODS_GEMSPEC_FILENAME} -o #{gemfile_path}")
|
||||
system("gem install #{gemfile_path}")
|
||||
end
|
||||
end
|
||||
|
||||
def self.start_nginx
|
||||
# Start nginx server
|
||||
system('nginx -c $PWD/e2eTests/nginx/nginx.conf')
|
||||
puts('starting nginx')
|
||||
# Call cleanup on exit
|
||||
at_exit { puts('resetting ngingx'); system('nginx -s stop') }
|
||||
end
|
||||
|
||||
# Create a new branch out of a current commit and
|
||||
# add remote that points to itself
|
||||
def self.configure_git
|
||||
system("git checkout -B #{GIT_BRANCH}")
|
||||
system("git remote add #{GIT_REMOTE_NAME} #{GIT_REMOTE_ADDRESS} && git fetch -q #{GIT_REMOTE_NAME}")
|
||||
# Revert new remote on exit
|
||||
at_exit { system("git remote remove #{GIT_REMOTE_NAME}")}
|
||||
end
|
||||
|
||||
def self.pre_producer_setup
|
||||
clean_git
|
||||
clean_server
|
||||
# Link prebuilt binaries to the Project
|
||||
system("ln -s $(pwd)/releases #{E2E_COCOAPODS_SAMPLE_DIR}/#{XCRC_BINARIES}")
|
||||
end
|
||||
|
||||
def self.pre_consumer_setup
|
||||
clean_git
|
||||
# Link prebuilt binaries to the Project
|
||||
system("ln -s $(pwd)/releases #{E2E_COCOAPODS_SAMPLE_DIR}/#{XCRC_BINARIES}")
|
||||
end
|
||||
|
||||
def self.clean_server
|
||||
system("rm -rf #{NGINX_ROOT_DIR}")
|
||||
end
|
||||
|
||||
# Revert any local changes in the test project
|
||||
def self.clean_git
|
||||
system("git clean -xdf #{E2E_COCOAPODS_SAMPLE_DIR}")
|
||||
end
|
||||
|
||||
# Cleans all extra locations that a test creates
|
||||
def self.clean
|
||||
clean_git
|
||||
clean_server
|
||||
end
|
||||
|
||||
# xcremotecache configuration to add to Podfile
|
||||
def self.cocoapods_configuration_string(extra_configs = {})
|
||||
configuration_lines = ['xcremotecache({']
|
||||
all_properties = SHARED_COCOAPODS_CONFIG.merge(extra_configs)
|
||||
config_lines = all_properties.map {|key, value| " \"#{key}\" => #{value.inspect},"}
|
||||
configuration_lines.push(*config_lines)
|
||||
configuration_lines << '})'
|
||||
configuration_lines.join("\n")
|
||||
end
|
||||
|
||||
def self.dump_podfile(config, source)
|
||||
# Create producer Podfile
|
||||
File.open("#{E2E_COCOAPODS_SAMPLE_DIR}/Podfile", 'w') do |f|
|
||||
# Copy podfile template
|
||||
File.foreach(source) { |line| f.puts line }
|
||||
f.write(config)
|
||||
end
|
||||
end
|
||||
|
||||
def self.build_project(extra_args = {})
|
||||
system('pod install')
|
||||
xcodebuild_args = {
|
||||
'workspace' => 'XCRemoteCacheSample.xcworkspace',
|
||||
'scheme' => 'XCRemoteCacheSample',
|
||||
'configuration' => 'Debug',
|
||||
'sdk' => 'iphonesimulator',
|
||||
'destination' => 'generic/platform=iOS Simulator',
|
||||
'derivedDataPath' => DERIVED_DATA_PATH,
|
||||
}.merge(extra_args)
|
||||
xcodebuild_vars = {
|
||||
'EXCLUDED_ARCHS' => 'arm64 i386'
|
||||
}
|
||||
args = ['xcodebuild']
|
||||
args.push(*xcodebuild_args.map {|k,v| "-#{k} '#{v}'"})
|
||||
args.push(*xcodebuild_vars.map {|k,v| "#{k}='#{v}'"})
|
||||
args.push('clean build')
|
||||
args.push("> #{LOG_NAME}")
|
||||
puts 'Building a project with xcodebuild...'
|
||||
system(args.join(' '))
|
||||
unless $?.success?
|
||||
system("tail #{LOG_NAME}")
|
||||
raise "xcodebuild failed."
|
||||
end
|
||||
end
|
||||
|
||||
def self.read_stats
|
||||
stats_json_string = JSON.parse(`#{XCRC_BINARIES}/xcprepare stats --format json`)
|
||||
misses = stats_json_string.fetch('miss_count', 0)
|
||||
hits = stats_json_string.fetch('hit_count', 0)
|
||||
all_targets = misses + hits
|
||||
raise "Failure: No XCRemoteCache targets invoked" if all_targets == 0
|
||||
hit_rate = hits * 100 / all_targets
|
||||
Stats.new(hits, misses, hit_rate)
|
||||
end
|
||||
|
||||
# validate 100% hit rate
|
||||
def self.valide_hit_rate
|
||||
status = read_stats()
|
||||
raise "Failure: Hit rate is only #{status.hit_rate}% (#{status.hits}/#{status.all_targets})" if status.misses > 0
|
||||
all_targets = status.misses + status.hits
|
||||
puts("Hit rate: #{status.hit_rate}% (#{status.hits}/#{all_targets})")
|
||||
end
|
||||
|
||||
def self.run_cocoapods_scenario(template_path)
|
||||
producer_configuration = cocoapods_configuration_string({'mode' => 'producer'})
|
||||
consumer_configuration = cocoapods_configuration_string()
|
||||
|
||||
puts("****** Scenario: #{template_path}")
|
||||
|
||||
# Run producer build
|
||||
pre_producer_setup
|
||||
dump_podfile(producer_configuration, template_path)
|
||||
puts('Building producer ...')
|
||||
Dir.chdir(E2E_COCOAPODS_SAMPLE_DIR) do
|
||||
build_project
|
||||
# reset XCRemoteCache stats
|
||||
system("#{XCRC_BINARIES}/xcprepare stats --reset --format json")
|
||||
end
|
||||
|
||||
# Run consumer build
|
||||
pre_consumer_setup
|
||||
dump_podfile(consumer_configuration, template_path)
|
||||
puts('Building consumer ...')
|
||||
Dir.chdir(E2E_COCOAPODS_SAMPLE_DIR) do
|
||||
build_project({'derivedDataPath' => "#{DERIVED_DATA_PATH}_consumer"})
|
||||
valide_hit_rate
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user