1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
|
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
import SWBUtil
public import SWBCore
public import SWBMacro
public class IbtoolCompilerSpec : GenericCompilerSpec, IbtoolCompilerSupport, @unchecked Sendable {
/// The info object collects information across the build phase so that an ibtool task doesn't try to produce a ~device output which is already being explicitly produced from another input.
private final class BuildPhaseInfo: BuildPhaseInfoForToolSpec {
var allInputFilenames = Set<String>()
func addToContext(_ ftb: FileToBuild) {
// Only collect info about files we want to match against.
// FIXME: We should be using FileTypeSpec.conformsTo() here, but we don't have a good way in this context to look up the file type.
guard ftb.fileType.identifier == "file.xib" else {
return
}
allInputFilenames.insert(ftb.absolutePath.basenameWithoutSuffix)
}
func filterOutputFiles(_ outputs: [any PlannedNode], inputs: [Path]) -> [any PlannedNode] {
// This filter is operating on basenames-without-suffix, since ibtool is going to transform xibs into nibs.
return outputs.filter {
let outputFilename = $0.path.basenameWithoutSuffix
let inputFilenames = Set(inputs.map { $0.basenameWithoutSuffix })
// If this output filename is the same as one of our own input filenames, then we keep it.
guard !inputFilenames.contains(outputFilename) else {
return true
}
// If this output filename is among any of the input filenames that *aren't* one of our own inputs, then we remove it.
let otherInputFilenames = allInputFilenames.subtracting(inputFilenames)
guard !otherInputFilenames.contains(outputFilename) else {
return false
}
// If this output filename is something like ~ipad~ipad or ~iphone~iphone then we remove it (just to avoid ugliness).
guard !outputFilename.contains("~ipad~") && !outputFilename.contains("~iphone~") else {
return false
}
// Otherwise we keep it.
return true
}
}
}
public override func newBuildPhaseInfo() -> (any BuildPhaseInfoForToolSpec)? {
return BuildPhaseInfo()
}
/// Override to compute the special arguments.
public override func constructTasks(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate) async {
var specialArgs = [String]()
specialArgs += targetDeviceArguments(cbc, delegate)
specialArgs += minimumDeploymentTargetArguments(cbc, delegate)
// Get the strings file paths and regions.
let stringsFiles = stringsFilesAndRegions(cbc)
// Define the inputs, including the strings files from any variant groups.
let inputs = cbc.inputs.map({ $0.absolutePath }) + stringsFiles.map({ $0.stringsFile })
// Compute and declare the outputs.
var outputs = evaluatedOutputs(cbc, delegate) ?? []
if let buildPhaseInfo = cbc.buildPhaseInfo as? BuildPhaseInfo {
outputs = buildPhaseInfo.filterOutputFiles(outputs, inputs: inputs)
}
for output in outputs {
delegate.declareOutput(FileToBuild(absolutePath: output.path, inferringTypeUsing: cbc.producer))
}
// FIXME: Add the output paths for the .strings files. I think this is a little tricky because ibtool will lay down strings files for .xibs, but not for .storyboards; instead the storyboard linker will put the .strings files in place. I'm not certain about that, though.
// Add the additional outputs defined by the spec. These are not declared as outputs but should be processed by the tool separately.
let additionalEvaluatedOutputsResult = await additionalEvaluatedOutputs(cbc, delegate)
outputs += additionalEvaluatedOutputsResult.outputs.map { output in
if let fileTypeIdentifier = output.fileType, let fileType = cbc.producer.lookupFileType(identifier: fileTypeIdentifier) {
delegate.declareOutput(FileToBuild(absolutePath: output.path, fileType: fileType))
}
return delegate.createNode(output.path)
}
if let infoPlistContent = additionalEvaluatedOutputsResult.generatedInfoPlistContent {
delegate.declareGeneratedInfoPlistContent(infoPlistContent)
}
// Construct the command line.
func lookup(_ macro: MacroDeclaration) -> MacroExpression? {
switch macro {
case BuiltinMacros.IBC_REGIONS_AND_STRINGS_FILES:
return stringsFiles.count > 0 ? cbc.scope.table.namespace.parseLiteralStringList(stringsFiles.map({ "\($0.region):\($0.stringsFile.str)" })) : nil
default:
return nil
}
}
let commandLine = await commandLineFromTemplate(cbc, delegate, optionContext: discoveredCommandLineToolSpecInfo(cbc.producer, cbc.scope, delegate), specialArgs: specialArgs, lookup: lookup)
delegate.createTask(
type: self,
dependencyData: nil,
payload: nil,
ruleInfo: defaultRuleInfo(cbc, delegate),
additionalSignatureData: "",
commandLine: commandLine,
additionalOutput: [],
environment: environmentFromSpec(cbc, delegate),
workingDirectory: cbc.producer.defaultWorkingDirectory,
inputs: inputs.map(delegate.createNode),
outputs: outputs,
mustPrecede: [],
action: createTaskAction(cbc, delegate),
execDescription: resolveExecutionDescription(cbc, delegate),
preparesForIndexing: false,
enableSandboxing: enableSandboxing,
llbuildControlDisabled: true,
additionalTaskOrderingOptions: []
)
}
override public func customOutputParserType(for task: any ExecutableTask) -> (any TaskOutputParser.Type)? {
return InterfaceBuilderCompilerOutputParser.self
}
public override func discoveredCommandLineToolSpecInfo(_ producer: any CommandProducer, _ scope: MacroEvaluationScope, _ delegate: any CoreClientTargetDiagnosticProducingDelegate) async -> (any DiscoveredCommandLineToolSpecInfo)? {
do {
return try await discoveredIbtoolToolInfo(producer, delegate, at: self.resolveExecutablePath(producer, scope.ibtoolExecutablePath()))
} catch {
delegate.error(error)
return nil
}
}
}
public final class IbtoolCompilerSpecNIB: IbtoolCompilerSpec, SpecIdentifierType, @unchecked Sendable {
public static let identifier = "com.apple.xcode.tools.ibtool.compiler"
}
public final class IbtoolCompilerSpecStoryboard: IbtoolCompilerSpec, SpecIdentifierType, @unchecked Sendable {
public static let identifier = "com.apple.xcode.tools.ibtool.storyboard.compiler"
override public func environmentFromSpec(_ cbc: CommandBuildContext, _ delegate: any DiagnosticProducingDelegate, lookup: ((MacroDeclaration) -> MacroExpression?)? = nil) -> [(String, String)] {
let cachingEnabled = cbc.scope.evaluate(BuiltinMacros.ENABLE_GENERIC_TASK_CACHING)
var environment: [(String, String)] = super.environmentFromSpec(cbc, delegate)
if cachingEnabled {
environment.append(("IBToolNeverDeque", "1"))
}
return environment
}
override public func createTaskAction(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate) -> (any PlannedTaskAction)? {
if cbc.scope.evaluate(BuiltinMacros.ENABLE_GENERIC_TASK_CACHING), let casOptions = try? CASOptions.create(cbc.scope, .generic) {
return delegate.taskActionCreationDelegate.createGenericCachingTaskAction(
enableCacheDebuggingRemarks: cbc.scope.evaluate(BuiltinMacros.GENERIC_TASK_CACHE_ENABLE_DIAGNOSTIC_REMARKS),
enableTaskSandboxEnforcement: !cbc.scope.evaluate(BuiltinMacros.DISABLE_TASK_SANDBOXING),
sandboxDirectory: cbc.scope.evaluate(BuiltinMacros.TEMP_SANDBOX_DIR),
extraSandboxSubdirectories: [],
developerDirectory: cbc.scope.evaluate(BuiltinMacros.DEVELOPER_DIR),
casOptions: casOptions)
} else {
return nil
}
}
override public func commandLineFromTemplate(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate, optionContext: (any DiscoveredCommandLineToolSpecInfo)?, specialArgs: [String] = [], lookup: ((MacroDeclaration) -> MacroExpression?)? = nil) -> [CommandLineArgument] {
var commandLine = super.commandLineFromTemplate(cbc, delegate, optionContext: optionContext, specialArgs: specialArgs, lookup: lookup)
guard let primaryOutput = evaluatedOutputs(cbc, delegate)?.first else {
delegate.error("Unable to determine primary output for storyboard compilation")
return []
}
commandLine.append(contentsOf: [.literal("--compilation-directory"), .parentPath(primaryOutput.path)])
return commandLine
}
}
|