Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
49854e6
Add Sample standalone app
polac24 May 29, 2022
2626189
Add Standalone scenario
polac24 May 29, 2022
51580ea
Add .rcinfo
polac24 May 29, 2022
bc44a50
Add importing from ObjC
polac24 May 29, 2022
7937ddb
Isolate standalone test
polac24 May 29, 2022
636b34d
Run standalone test
polac24 May 29, 2022
59ffdaf
Exclude StandaloneSampleApp from SwiftLint
polac24 May 30, 2022
2825e7e
Add FileDependenciesRemapper
polac24 May 30, 2022
fbf826d
Integrate remapper to the producer
polac24 May 30, 2022
78c31f5
Integrate remapper to the consumer
polac24 May 30, 2022
c28681b
Change a place where producer replacement is done
polac24 May 30, 2022
b20c08f
Add tests for UnzippedArtifactProcessor
polac24 Jun 3, 2022
f8eb83e
Prereview cleanup
polac24 Jun 4, 2022
428c3a2
Add -Swift.h decorator
polac24 Jun 4, 2022
1c8cf27
Modify sample project to copy md5 of -Swift.h
polac24 Jun 4, 2022
7eb44d4
Add postbuild right after compilation
polac24 Jun 4, 2022
c9661af
Try to use md5 for -Swift.h fingerprint
polac24 Jun 4, 2022
a18835b
Do not use local cache for E2E StandaloneApp
polac24 Jun 5, 2022
4669b4a
Fix failed cocoapods support
polac24 Jun 5, 2022
d0c479e
Print output to stdout in cocoapods
polac24 Jun 5, 2022
3f09f42
Persist -Swift.h trailing empty line
polac24 Jun 6, 2022
f3c44c1
Fix swiftlint errors
polac24 Jun 6, 2022
986746b
Add -Swift.h decorator for frameworks
polac24 Jun 6, 2022
2ff39e2
Add unit tests for -Swift.h override
polac24 Jun 6, 2022
424d68d
Fix linter and tests errors
polac24 Jun 6, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ excluded:
- fastlane/
- DerivedData/
- e2eTests/XCRemoteCacheSample/Pods
- e2eTests/StandaloneSampleApp

attributes:
always_on_same_line:
Expand Down
4 changes: 4 additions & 0 deletions Sources/XCRemoteCache/Artifacts/ArtifactCreator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -52,6 +53,7 @@ class BuildArtifactCreator: ArtifactSwiftProductsBuilderImpl, ArtifactCreator {
modulesFolderPath: String,
dSYMPath: URL,
metaWriter: MetaWriter,
artifactProcessor: ArtifactProcessor,
fileManager: FileManager
) {
self.buildDir = buildDir
Expand All @@ -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)
}

Expand All @@ -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
Expand Down
11 changes: 9 additions & 2 deletions Sources/XCRemoteCache/Artifacts/ArtifactOrganizer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}

Expand Down Expand Up @@ -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
}
Expand Down
80 changes: 80 additions & 0 deletions Sources/XCRemoteCache/Artifacts/ArtifactProcessor.swift
Original file line number Diff line number Diff line change
@@ -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
/// Coule 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.isEmpty })
}

/// 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 isEmpty: Bool {
lastPathComponent.hasPrefix(".")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
81 changes: 81 additions & 0 deletions Sources/XCRemoteCache/Artifacts/FileDependenciesRemapper.swift
Original file line number Diff line number Diff line change
@@ -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 (placeholders-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 should not be used in bynary files, only text-based
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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
14 changes: 14 additions & 0 deletions Sources/XCRemoteCache/Commands/Postbuild/Postbuild.swift
Original file line number Diff line number Diff line change
Expand Up @@ -238,11 +238,25 @@ class Postbuild {
let moduleSwiftProductURL = context.productsDir
.appendingPathComponent(context.modulesFolderPath)
.appendingPathComponent("\(modulename).swiftmodule")
let objcHeaderSwiftProductURL = context.derivedSourcesDir
.appendingPathComponent("\(modulename)-Swift.h")
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)
}
Expand Down
10 changes: 9 additions & 1 deletion Sources/XCRemoteCache/Commands/Postbuild/PostbuildContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
}
}
}
10 changes: 9 additions & 1 deletion Sources/XCRemoteCache/Commands/Postbuild/XCPostbuild.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand Down Expand Up @@ -201,6 +208,7 @@ public class XCPostbuild {
switch context.mode {
case .consumer:
let artifactOrganizerFactory = ThinningConsumerZipArtifactsOrganizerFactory(
processors: [],
fileManager: fileManager
)
let swiftProductsLocationProvider =
Expand Down
Loading