Compare commits

..

18 Commits

Author SHA1 Message Date
Bartosz Polaczyk 64472d58bf Merge pull request #225 from polac24/fix-spaces-libtool
Support spaces in libtools paths
2023-08-17 08:54:47 +02:00
Bartosz Polaczyk 3d96dddc91 Fix linting 2023-08-16 14:23:13 +02:00
Bartosz Polaczyk deb6adcb70 Support spaces in libtools paths 2023-08-16 13:13:14 +02:00
Bartosz Polaczyk f8c854d007 Merge pull request #214 from polac24/up-to-date-meta
Ensure up-to-date meta json in the unzipped artifact
2023-08-03 15:48:53 +02:00
Bartosz Polaczyk 7fe04517e7 Merge pull request #222 from polac24/libtool-mode-fix
Improve XCLibtool's argument parsing
2023-08-03 12:51:29 +02:00
Bartosz Polaczyk 5029f9c73b Improve Libtool libraries argument parsing 2023-08-02 22:10:03 +02:00
Bartosz Polaczyk ed256234f1 Merge pull request #221 from grigorye/fix/xclibtool-static-broken-markers
Fixed xclibtool not accounting the artifacts retrieved from cache in certain scenarios.
2023-08-02 21:00:45 +02:00
Grigory Entin fd6e1da054 Fixed -static flag treated as an libtool input (library). 2023-08-02 04:26:23 +02:00
Grigory Entin 574129788c Fixed libtool executable accounted as its own argument. 2023-08-02 04:26:23 +02:00
Bartosz Polaczyk 6a1335ea97 Merge pull request #218 from polac24/support-
[Cocoapods] Ensure the source build phase exist before enabling xcrc
2023-08-01 10:50:27 +02:00
Bartosz Polaczyk be87c3779e Bump version 2023-07-29 23:11:58 -07:00
Bartosz Polaczyk 2c23632732 fix module 2023-07-29 22:55:23 -07:00
Bartosz Polaczyk b048393eb0 Define module of the source phase 2023-07-29 22:51:52 -07:00
Bartosz Polaczyk 8955cb2750 Bail out if source build phase phase doesnt exist 2023-07-29 22:46:54 -07:00
Bartosz Polaczyk 587840ddbc Fix linter 2023-06-12 19:44:08 -07:00
Bartosz Polaczyk 42a56b4f5b Add tests for ZipArtifactOrganizer 2023-06-12 19:23:36 -07:00
Bartosz Polaczyk 6a98cb4127 Add unit tests for updater 2023-06-12 18:59:35 -07:00
Bartosz Polaczyk d5e95962b5 Keep fresh meta in active artifact dir 2023-06-12 17:59:05 -07:00
12 changed files with 291 additions and 18 deletions
@@ -0,0 +1,72 @@
// Copyright (c) 2023 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
enum ArtifactMetaUpdaterError: Error {
/// The prebuild plugin execution was called but the local
/// path to the artifact directory is still unknown
/// Might happen that the artifact processor didn't invoke the updater's
/// .process() after downloading/activating an artifact
case artifactLocationIsUnknown
}
/// Updates the meta file in an unzipped artifact directory, by placing an up-to-date
/// and remapped meta file. Updating the meta in the artifact allows reusing existing
/// artifacts it a new meta.json schema has been released to the meta format, while
/// artifacts are still backward-compatible
class ArtifactMetaUpdater: ArtifactProcessor {
private var artifactLocation: URL?
private let metaWriter: MetaWriter
private let fileRemapper: FileDependenciesRemapper
init(
fileRemapper: FileDependenciesRemapper,
metaWriter: MetaWriter
) {
self.metaWriter = metaWriter
self.fileRemapper = fileRemapper
}
/// Remembers the artifact location, used later in the plugin
/// - Parameter url: artifact's root directory
func process(rawArtifact url: URL) throws {
// Storing the location of the just downloaded/activated artifact
// Note, the `url` location already includes a meta (generated by producer
// while compiling and building an artifact)
artifactLocation = url
}
func process(localArtifact url: URL) throws {
// No need to do anything in the postbuild
}
}
extension ArtifactMetaUpdater: ArtifactConsumerPrebuildPlugin {
/// Updates the meta json file in a local, unzipped, artifact location. It also remaps
/// all paths so other steps (like actool or postbuild) don't have to do it again
func run(meta: MainArtifactMeta) throws {
guard let artifactLocation = artifactLocation else {
throw ArtifactMetaUpdaterError.artifactLocationIsUnknown
}
let metaURL = try metaWriter.write(meta, locationDir: artifactLocation)
try fileRemapper.remap(fromGeneric: metaURL)
}
}
@@ -47,6 +47,8 @@ protocol ArtifactOrganizer {
}
class ZipArtifactOrganizer: ArtifactOrganizer {
static let activeArtifactLocation = "active"
private let cacheDir: URL
// all processors that should "prepare" the unzipped raw artifact
private let artifactProcessors: [ArtifactProcessor]
@@ -63,7 +65,7 @@ class ZipArtifactOrganizer: ArtifactOrganizer {
}
func getActiveArtifactLocation() -> URL {
return cacheDir.appendingPathComponent("active")
return cacheDir.appendingPathComponent(Self.self.activeArtifactLocation)
}
func getActiveArtifactFilekey() throws -> String {
@@ -90,20 +92,27 @@ class ZipArtifactOrganizer: ArtifactOrganizer {
let destinationURL = artifact.deletingPathExtension()
guard fileManager.fileExists(atPath: destinationURL.path) == false else {
infoLog("Skipping artifact, already existing at \(destinationURL)")
try runArtifactProcessors(artifactLocation: destinationURL)
return destinationURL
}
// Uzipping to a temp file first to never leave the uncompleted zip in the final location
// Unzipping to a temp file first to never leave the uncompleted zip in the final location
// when the command was interrupted (internal crash or `kill -9` signal)
let tempDestination = destinationURL.appendingPathExtension("tmp")
try Zip.unzipFile(artifact, destination: tempDestination, overwrite: true, password: nil)
try artifactProcessors.forEach { processor in
try processor.process(rawArtifact: tempDestination)
}
try fileManager.moveItem(at: tempDestination, to: destinationURL)
try runArtifactProcessors(artifactLocation: destinationURL)
return destinationURL
}
/// Iterates all processor when an artifact has been just downloaded or reused from already downloaded
/// and previously processed location
private func runArtifactProcessors(artifactLocation: URL) throws {
try artifactProcessors.forEach { processor in
try processor.process(rawArtifact: artifactLocation)
}
}
func activate(extractedArtifact: URL) throws {
let activeLocationURL = getActiveArtifactLocation()
try fileManager.spt_forceSymbolicLink(at: activeLocationURL, withDestinationURL: extractedArtifact)
@@ -158,13 +158,17 @@ public class XCPrebuild {
fileAccessor: fileManager
)
let artifactProcessor = UnzippedArtifactProcessor(fileRemapper: fileRemapper, dirScanner: fileManager)
let metaUpdater = ArtifactMetaUpdater(
fileRemapper: fileRemapper,
metaWriter: JsonMetaWriter(fileWriter: fileManager, pretty: true)
)
let organizer = ZipArtifactOrganizer(
targetTempDir: context.targetTempDir,
artifactProcessors: [artifactProcessor],
artifactProcessors: [artifactProcessor, metaUpdater],
fileManager: fileManager
)
let metaReader = JsonMetaReader(fileAccessor: fileManager)
var consumerPlugins: [ArtifactConsumerPrebuildPlugin] = []
var consumerPlugins: [ArtifactConsumerPrebuildPlugin] = [metaUpdater]
if config.thinningEnabled {
if context.moduleName == config.thinningTargetModuleName, let thinnedTarget = context.thinnedTargets {
@@ -22,12 +22,12 @@ import Foundation
/// Reads a list of files from a marker file
class FileMarkerReader: ListReader {
private let file: URL
private let fileManager: FileManager
private let fileReader: FileReader
private var cachedFiles: [URL]?
init(_ file: URL, fileManager: FileManager) {
init(_ file: URL, fileManager: FileReader) {
self.file = file
self.fileManager = fileManager
self.fileReader = fileManager
}
func listFilesURLs() throws -> [URL] {
@@ -45,6 +45,6 @@ class FileMarkerReader: ListReader {
}
func canRead() -> Bool {
return fileManager.fileExists(atPath: file.path)
return fileReader.fileExists(atPath: file.path)
}
}
+1 -1
View File
@@ -35,7 +35,7 @@ public class XCLibtoolMain {
let args = ProcessInfo().arguments
do {
let mode = try XCLibtoolHelper.buildMode(args: args)
let mode = try XCLibtoolHelper.buildMode(args: Array(args.dropFirst()))
try XCLibtool(mode).run()
} catch {
exit(1, "Failed with: \(error). Args: \(args)")
@@ -47,7 +47,9 @@ public class XCLibtoolHelper {
case "-dependency_info":
dependencyInfo = args[i + 1]
i += 1
case let input where ["", "a"].contains(URL(string: args[i])?.pathExtension):
case let input where input.starts(with: "/") &&
["", "a"].contains(URL(fileURLWithPath: input).pathExtension):
// Assume always absolute paths to the library
// Support for static frameworks (no extension) and static libraries (.a)
inputLibraries.append(input)
default:
@@ -0,0 +1,85 @@
// Copyright (c) 2023 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
@testable import XCRemoteCache
import XCTest
class ArtifactMetaUpdaterTests: XCTestCase {
private let accessorFake = FileAccessorFake(mode: .normal)
private var metaWriter: MetaWriter!
private var fileRemapper: FileDependenciesRemapper!
private var updater: ArtifactMetaUpdater!
private let sampleMeta = MainArtifactMeta(
dependencies: [],
fileKey: "abc",
rawFingerprint: "",
generationCommit: "",
targetName: "",
configuration: "",
platform: "",
xcode: "",
inputs: ["$(BASE)/myFile.swift"],
pluginsKeys: [:]
)
override func setUp() async throws {
metaWriter = JsonMetaWriter(
fileWriter: accessorFake,
pretty: true
)
fileRemapper = TextFileDependenciesRemapper(
remapper: StringDependenciesRemapper(
mappings: [
.init(generic: "$(BASE)", local: "/base")
]
),
fileAccessor: accessorFake
)
updater = ArtifactMetaUpdater(
fileRemapper: fileRemapper,
metaWriter: metaWriter
)
}
func testStoresInTheRawArtifact() throws {
try updater.process(rawArtifact: "/artifact")
try updater.run(meta: sampleMeta)
XCTAssertTrue(accessorFake.fileExists(atPath: "/artifact/abc.json"))
}
func testRewirtesMetaPaths() throws {
try updater.process(rawArtifact: "/artifact")
try updater.run(meta: sampleMeta)
let diskMetaData = try XCTUnwrap(accessorFake.contents(atPath: "/artifact/abc.json"))
let diskMeta = try JSONDecoder().decode(MainArtifactMeta.self, from: diskMetaData)
XCTAssertEqual(diskMeta.inputs, ["/base/myFile.swift"])
}
func testFailsIfProcessorTriggerIsNotCalledBeforeRunningAPlugin() throws {
XCTAssertThrowsError(try updater.run(meta: sampleMeta)) { error in
switch error {
case ArtifactMetaUpdaterError.artifactLocationIsUnknown: break
default:
XCTFail("Not expected error")
}
}
}
}
@@ -156,4 +156,39 @@ class ZipArtifactOrganizerTests: XCTestCase {
XCTAssertEqual(fileKey, expectedFileKey)
}
func testPrepareRunsProcessorsForAlreadyExistingArtifacts() throws {
let zipURL = try prepareZipFile(content: "Magic", fileName: "content.txt")
let artifactURL = zipURL.deletingPathExtension()
let processor = DestroyerArtifactProcessor(fileManager)
let organizer: ZipArtifactOrganizer = ZipArtifactOrganizer(
targetTempDir: workingDirectory,
artifactProcessors: [processor],
fileManager: fileManager
)
try fileManager.createDirectory(
at: artifactURL,
withIntermediateDirectories: true
)
let preparedArtifact = try organizer.prepare(artifact: zipURL)
XCTAssertFalse(fileManager.fileExists(atPath: preparedArtifact.path))
}
func testPrepareRunsProcessorsForNewlyUnzippedArtifacts() throws {
let zipURL = try prepareZipFile(content: "Magic", fileName: "content.txt")
let processor = DestroyerArtifactProcessor(fileManager)
let organizer: ZipArtifactOrganizer = ZipArtifactOrganizer(
targetTempDir: workingDirectory,
artifactProcessors: [processor],
fileManager: fileManager
)
let preparedArtifact = try organizer.prepare(artifact: zipURL)
XCTAssertFalse(fileManager.fileExists(atPath: preparedArtifact.path))
}
}
@@ -0,0 +1,37 @@
// Copyright (c) 2022 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import Foundation
@testable import XCRemoteCache
/// A Processor fake that deletes the artifact
class DestroyerArtifactProcessor: ArtifactProcessor {
private let dirAccesor: DirAccessor
init(_ dirAccesor: DirAccessor) {
self.dirAccesor = dirAccesor
}
func process(rawArtifact url: URL) throws {
try dirAccesor.removeItem(atPath: url.path)
}
func process(localArtifact url: URL) throws {
try dirAccesor.removeItem(atPath: url.path)
}
}
@@ -23,23 +23,23 @@ import XCTest
class XCLibtoolHelperTests: XCTestCase {
func testStaticFrameworkUniversalBinary() throws {
let mode = try XCLibtoolHelper.buildMode(
args: ["-o", "/universal/static", "/arch1/static", "arch2/static"]
args: ["-o", "/universal/static", "/arch1/static", "/arch2/static"]
)
XCTAssertEqual(mode, .createUniversalBinary(
output: "/universal/static",
inputs: ["/arch1/static", "arch2/static"]
inputs: ["/arch1/static", "/arch2/static"]
))
}
func testStaticLibraryUniversalBinary() throws {
let mode = try XCLibtoolHelper.buildMode(
args: ["-o", "/universal/static.a", "/arch1/static.a", "arch2/static.a"]
args: ["-o", "/universal/static.a", "/arch1/static.a", "/arch2/static.a"]
)
XCTAssertEqual(mode, .createUniversalBinary(
output: "/universal/static.a",
inputs: ["/arch1/static.a", "arch2/static.a"]
inputs: ["/arch1/static.a", "/arch2/static.a"]
))
}
@@ -63,4 +63,26 @@ class XCLibtoolHelperTests: XCTestCase {
}
}
}
func testExplicitStaticFrameworkUniversalBinary() throws {
let mode = try XCLibtoolHelper.buildMode(
args: ["-static", "-o", "/universal/static", "/arch1/static", "/arch2/static"]
)
XCTAssertEqual(mode, .createUniversalBinary(
output: "/universal/static",
inputs: ["/arch1/static", "/arch2/static"]
))
}
func testRecognizesPathsWithSpaces() throws {
let mode = try XCLibtoolHelper.buildMode(
args: ["-static", "-o", "/universal/static", "/arch/with space/static"]
)
XCTAssertEqual(mode, .createUniversalBinary(
output: "/universal/static",
inputs: ["/arch/with space/static"]
))
}
}
@@ -409,6 +409,11 @@ module CocoapodsXCRemoteCacheModifier
File.write(LLDB_INIT_PATH, lldbinit_lines.join("\n"), mode: "w")
end
# Contrary to AbstractTarget.source_build_phase, it only finds a build phase, without creating one if it doesn't exist
def self.find_source_build_phase(target)
target.build_phases.find { |bp| bp.class == Xcodeproj::Project::Object::PBXSourcesBuildPhase }
end
Pod::HooksManager.register('cocoapods-xcremotecache', :pre_install) do |installer_context|
# The main responsibility of that hook is forcing Pods regeneration when XCRemoteCache is enabled for the first time
# In the post_install hook, this plugin adds extra build settings and steps to all Pods targets, but only when XCRemoteCache
@@ -520,6 +525,8 @@ module CocoapodsXCRemoteCacheModifier
# Attach XCRemoteCache to Pods targets
# Enable only for native targets which can have compilation steps
installer_context.pods_project.native_targets.each do |target|
# Ensure the PBXSourcesBuildPhase exists as the flow would unnecessary create an empty source build phase otherwise
next if find_source_build_phase(target).nil?
next if target.source_build_phase.files_references.empty?
next if target.name.start_with?("Pods-")
next if target.name.end_with?("Tests")
@@ -13,5 +13,5 @@
# limitations under the License.
module CocoapodsXcremotecache
VERSION = "0.0.17"
VERSION = "0.0.18"
end