Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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