Skip to content

Commit

Permalink
Merge pull request #215 from polac24/xcode-15-lightweight-support
Browse files Browse the repository at this point in the history
Add support for Xcode 15
  • Loading branch information
polac24 authored Jun 14, 2023
2 parents b74e002 + 53e7ddd commit afb1f9e
Show file tree
Hide file tree
Showing 10 changed files with 362 additions and 31 deletions.
9 changes: 7 additions & 2 deletions Sources/XCRemoteCache/Commands/Postbuild/XCPostbuild.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,14 @@ public class XCPostbuild {
let fileReaderFactory: (URL) -> DependenciesReader = {
FileDependenciesReader($0, accessor: fileManager)
}
let assetsFileDependenciesFactory: (URL) -> DependenciesReader = {
AssetsFileDependenciesReader($0, dirAccessor: fileManager)
}
let dependenciesReader = TargetDependenciesReader(
context.compilationTempDir,
fileDependeciesReaderFactory: fileReaderFactory,
compilationOutputDir: context.compilationTempDir,
assetsCatalogOutputDir: context.targetTempDir,
fileDependenciesReaderFactory: fileReaderFactory,
assetsDependenciesReaderFactory: assetsFileDependenciesFactory,
dirScanner: fileManager
)
var remappers: [DependenciesRemapper] = []
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) 2023 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

import Foundation

/// Decides if an input to the compilation step should allow reusing the cached artifact
protocol AllowedInputDeterminer {
/// Decides if the input file is allowed to be compiled, even not specified in the dependency list
func allowedNonDependencyInput(file: URL) -> Bool
}

class FilenameBasedAllowedInputDeterminer: AllowedInputDeterminer {
private let filenames: [String]

init(_ filenames: [String]) {
self.filenames = filenames
}

func allowedNonDependencyInput(file: URL) -> Bool {
return filenames.contains(file.lastPathComponent)
}
}
13 changes: 10 additions & 3 deletions Sources/XCRemoteCache/Commands/Swiftc/Swiftc.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class Swiftc: SwiftcProtocol {
private let dependenciesWriterFactory: (URL, FileManager) -> DependenciesWriter
private let touchFactory: (URL, FileManager) -> Touch
private let plugins: [SwiftcProductGenerationPlugin]
private let allowedInputDeterminer: AllowedInputDeterminer

init(
inputFileListReader: ListReader,
Expand All @@ -70,7 +71,8 @@ class Swiftc: SwiftcProtocol {
fileManager: FileManager,
dependenciesWriterFactory: @escaping (URL, FileManager) -> DependenciesWriter,
touchFactory: @escaping (URL, FileManager) -> Touch,
plugins: [SwiftcProductGenerationPlugin]
plugins: [SwiftcProductGenerationPlugin],
allowedInputDeterminer: AllowedInputDeterminer
) {
self.inputFileListReader = inputFileListReader
self.markerReader = markerReader
Expand All @@ -84,6 +86,7 @@ class Swiftc: SwiftcProtocol {
self.dependenciesWriterFactory = dependenciesWriterFactory
self.touchFactory = touchFactory
self.plugins = plugins
self.allowedInputDeterminer = allowedInputDeterminer
}

// swiftlint:disable:next function_body_length
Expand All @@ -96,13 +99,17 @@ class Swiftc: SwiftcProtocol {

let inputFilesInputs = try inputFileListReader.listFilesURLs()
let markerAllowedFiles = try markerReader.listFilesURLs()
let allDependencies = Set(markerAllowedFiles + inputFilesInputs)
let cachedDependenciesWriterFactory = CachedFileDependenciesWriterFactory(
dependencies: markerAllowedFiles,
dependencies: Array(allDependencies),
fileManager: fileManager,
writerFactory: dependenciesWriterFactory
)
// Verify all input files to be present in a marker fileList
let disallowedInputs = try inputFilesInputs.filter { try !allowedFilesListScanner.contains($0) }
let disallowedInputs = try inputFilesInputs.filter { file in
try !allowedFilesListScanner.contains(file) &&
!allowedInputDeterminer.allowedNonDependencyInput(file: file)
}

if !disallowedInputs.isEmpty {
// New file (disallowedFile) added without modifying the rest of the feature. Fallback to swiftc and
Expand Down
6 changes: 5 additions & 1 deletion Sources/XCRemoteCache/Commands/Swiftc/XCSwiftc.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ public class XCSwiftAbstract<InputArgs> {
retrieveIgnoredCommands: [swiftcCommand]
)
let shellOut = ProcessShellOut()
// Always allow an input file from the actool generation step
// As of Xcode15, the filename is confirmed to be static
let allowedInputDeterminer = FilenameBasedAllowedInputDeterminer(["GeneratedAssetSymbols.swift"])

let swiftc = Swiftc(
inputFileListReader: fileListReader,
Expand All @@ -165,7 +168,8 @@ public class XCSwiftAbstract<InputArgs> {
fileManager: fileManager,
dependenciesWriterFactory: dependenciesWriterFactory,
touchFactory: touchFactory,
plugins: []
plugins: [],
allowedInputDeterminer: allowedInputDeterminer
)
let orchestrator = SwiftcOrchestrator(
mode: context.mode,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright (c) 2023 Spotify AB.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

import Foundation

/// Parser for `assetcatalog_dependencies` file: an output of the `actool`
/// that lists all dependencies of this command
class AssetsFileDependenciesReader: DependenciesReader {
private let file: URL
private let dirAccessor: DirAccessor

public init(_ file: URL, dirAccessor: DirAccessor) {
self.file = file
self.dirAccessor = dirAccessor
}

public func findDependencies() throws -> [String] {
return try Array(findAllDependencies())
}

public func findInputs() throws -> [String] {
// XCRemoteCache doesn't use it yet
exit(1, "TODO: implement")
}

public func readFilesAndDependencies() throws -> [String: [String]] {
return try ["": findAllDependencies()]
}

private func findAllDependencies() throws -> [String] {
let fileData = try getFileData()
// all dependency files are separated by the \0 byte
// each path has a file type prefix:
// 0x10 - directory
// 0x40 - file
// We only care about dirs, as *.xcassets is a folder
let pathDatas = fileData.split(separator: 0x0)
let paths = pathDatas
.filter { !$0.isEmpty && $0.first == 0x10 }
.map { String(data: $0.dropFirst(), encoding: .utf8)! }
.map(URL.init(fileURLWithPath:))
let xcassetsPaths = paths.filter { path in
path.pathExtension == "xcassets"
}
return try xcassetsPaths.flatMap { try findAssetsContentJsons(xcasset: $0) }
}

private func findAssetsContentJsons(xcasset: URL) throws -> [String] {
return try dirAccessor.recursiveItems(at: xcasset).filter { url in
url.lastPathComponent == "Contents.json"
}.map(\.path)
}

private func getFileData() throws -> Data {
guard let fileData = try dirAccessor.contents(atPath: file.path) else {
throw DependenciesReaderError.readingError
}
return fileData
}

}
44 changes: 33 additions & 11 deletions Sources/XCRemoteCache/Dependencies/TargetDepdenciesReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,35 @@ import Foundation

/// Reads and aggregates all compilation dependencies from a single directory
class TargetDependenciesReader: DependenciesReader {
private let directory: URL
// As of Xcode15, the filename is static
private static let assetsDependenciesFilename = "assetcatalog_dependencies"
private let compilationDirectory: URL
private let assetsCatalogOutputDir: URL
private let dirScanner: DirScanner
private let fileDependeciesReaderFactory: (URL) -> DependenciesReader
private let fileDependenciesReaderFactory: (URL) -> DependenciesReader
private let assetsDependenciesReaderFactory: (URL) -> DependenciesReader

public init(
_ directory: URL,
fileDependeciesReaderFactory: @escaping (URL) -> DependenciesReader,
compilationOutputDir: URL,
assetsCatalogOutputDir: URL,
fileDependenciesReaderFactory: @escaping (URL) -> DependenciesReader,
assetsDependenciesReaderFactory: @escaping (URL) -> DependenciesReader,
dirScanner: DirScanner
) {
self.directory = directory
self.compilationDirectory = compilationOutputDir
self.assetsCatalogOutputDir = assetsCatalogOutputDir
self.dirScanner = dirScanner
self.fileDependeciesReaderFactory = fileDependeciesReaderFactory
self.fileDependenciesReaderFactory = fileDependenciesReaderFactory
self.assetsDependenciesReaderFactory = assetsDependenciesReaderFactory
}

// Optimized way of finding dependencies only for files that have corresponding .o file on a disk
// includes also inputs to the `actool` assets generator
public func findDependencies() throws -> [String] {
// Not calling `readFilesAndDependencies` as it may unnecessary call expensive `findDependencies()` for
// files that eventually will not be considered
let allURLs = try dirScanner.items(at: directory)
let mergedDependencies = try allURLs.reduce(Set<String>()) { (prev: Set<String>, file) in
let allCompilationOutputURLs = try dirScanner.items(at: compilationDirectory)
var mergedDependencies = try allCompilationOutputURLs.reduce(Set<String>()) { (prev: Set<String>, file) in
// include only these .d files that either have corresponding .o file (incremental) or end
// with '-master' (whole-module)
// Otherwise .d is probably just a leftover from previous builds
Expand All @@ -53,20 +62,33 @@ class TargetDependenciesReader: DependenciesReader {
return prev
}

return try prev.union(fileDependeciesReaderFactory(file).findDependencies())
return try prev.union(fileDependenciesReaderFactory(file).findDependencies())
}
// include also dependencies from optional assets compilation (`actool`)
try mergedDependencies.formUnion(findAssetsCatalogDependencies())
return Array(mergedDependencies).sorted()
}

// finds all assets compilation's dependencies, which are always appended to the list of
// files to compare on the consumer side (in the fingerprint comparison)
private func findAssetsCatalogDependencies() throws -> Set<String> {
let expectedAssetsDepsFile = assetsCatalogOutputDir
.appendingPathComponent(Self.assetsDependenciesFilename)
guard try dirScanner.itemType(atPath: expectedAssetsDepsFile.path) == .file else {
return []
}
return try Set(assetsDependenciesReaderFactory(expectedAssetsDepsFile).findDependencies())
}

public func findInputs() throws -> [String] {
fatalError("TODO: implement")
}

public func readFilesAndDependencies() throws -> [String: [String]] {
let allURLs = try dirScanner.items(at: directory)
let allURLs = try dirScanner.items(at: compilationDirectory)
return try allURLs.reduce([String: [String]]()) { prev, file in
var new = prev
new[file.path] = try fileDependeciesReaderFactory(file).findDependencies()
new[file.path] = try fileDependenciesReaderFactory(file).findDependencies()
return new
}
}
Expand Down
Loading

0 comments on commit afb1f9e

Please sign in to comment.