Skip to content
Open
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
2 changes: 1 addition & 1 deletion Sources/SWBApplePlatform/InterfaceBuilderCompiler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public class IbtoolCompilerSpec : GenericCompilerSpec, IbtoolCompilerSupport, @u
specialArgs += minimumDeploymentTargetArguments(cbc, delegate)

// Get the strings file paths and regions.
let stringsFiles = stringsFilesAndRegions(cbc)
let stringsFiles = stringsFilesAndRegions(cbc, delegate)

// Define the inputs, including the strings files from any variant groups.
let inputs = cbc.inputs.map({ $0.absolutePath }) + stringsFiles.map({ $0.stringsFile })
Expand Down
16 changes: 14 additions & 2 deletions Sources/SWBApplePlatform/InterfaceBuilderShared.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public protocol IbtoolCompilerSupport {
func minimumDeploymentTargetArguments(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate) -> [String]

/// Get the string files paths and regions.
func stringsFilesAndRegions(_ cbc: CommandBuildContext) -> [(stringsFile: Path, region: String)]
func stringsFilesAndRegions(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate) -> [(stringsFile: Path, region: String)]
}

extension IbtoolCompilerSupport {
Expand Down Expand Up @@ -55,13 +55,25 @@ extension IbtoolCompilerSupport {
return minimumDeploymentTargetArguments
}

public func stringsFilesAndRegions(_ cbc: CommandBuildContext) -> [(stringsFile: Path, region: String)] {
public func stringsFilesAndRegions(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate) -> [(stringsFile: Path, region: String)] {
var result = [(Path, String)]()

// Check if we should filter localizations based on the setting BUILD_ONLY_KNOWN_LOCALIZATIONS:
let shouldFilter = cbc.scope.evaluate(BuiltinMacros.BUILD_ONLY_KNOWN_LOCALIZATIONS)
let allowedLocalizations = shouldFilter ? cbc.producer.project?.knownLocalizations : nil

for ftb in cbc.inputs {
if let buildFile = ftb.buildFile {
if case .reference(let guid) = buildFile.buildableItem, case let variantGroup as VariantGroup = cbc.producer.lookupReference(for: guid) {
for ref in variantGroup.children {
if let fileRef = ref as? FileReference, let region = fileRef.regionVariantName {
// Filter by knownLocalizations if BUILD_ONLY_KNOWN_LOCALIZATIONS is enabled:
if let allowedLocalizations, !allowedLocalizations.contains(region) {
// Skip this .strings file.
delegate.note("Skipping .lproj directory '\(region).lproj' because '\(region)' is not in project's known localizations (BUILD_ONLY_KNOWN_LOCALIZATIONS is enabled)")
continue
}

if let fileType = cbc.producer.lookupFileType(reference: fileRef), let stringFileType = cbc.producer.lookupFileType(identifier: "text.plist.strings"), fileType.conformsTo(stringFileType) {
let absolutePath = cbc.producer.filePathResolver.resolveAbsolutePath(fileRef)
result.append((absolutePath, region))
Expand Down
30 changes: 29 additions & 1 deletion Sources/SWBApplePlatform/XCStringsCompiler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,35 @@ public final class XCStringsCompilerSpec: GenericCompilerSpec, SpecIdentifierTyp

/// Generates a task for compiling the .xcstrings to .strings/dict files.
private func constructCatalogCompilationTask(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate) async {
let commandLine = await commandLineFromTemplate(cbc, delegate, optionContext: discoveredCommandLineToolSpecInfo(cbc.producer, cbc.scope, delegate)).map(\.asString)
let isRunningInstallloc = cbc.scope.evaluate(BuiltinMacros.BUILD_COMPONENTS).contains("installLoc")

/// Custom lookup function to overwrite `XCSTRINGS_LANGUAGES_TO_COMPILE`
/// when `BUILD_ONLY_KNOWN_LOCALIZATIONS` is enabled for a regular build.
func lookup(_ macro: MacroDeclaration) -> MacroExpression? {
switch macro {
case BuiltinMacros.XCSTRINGS_LANGUAGES_TO_COMPILE:
guard !isRunningInstallloc else {
// No need to intercept anything for installloc, its
// language specification should always take precedence.
return nil
}
if cbc.scope.evaluate(BuiltinMacros.BUILD_ONLY_KNOWN_LOCALIZATIONS), var knownLocalizations = cbc.producer.project?.knownLocalizations {

knownLocalizations.removeAll(where: { $0 == "Base" })
if !knownLocalizations.isEmpty {
delegate.note("XCStrings will compile languages for known regions: \(knownLocalizations.joined(separator: ", "))")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just heads up that messages like this are going to show up for every xcstrings file, although in this particular case the specific file is not provided. Not saying it needs to be provided, but may appear as duplicate messages. Also for IB Compiler, etc.

}

// Only build the languages specified by the project:
return cbc.scope.namespace.parseLiteralStringList(knownLocalizations)
}
default:
break
}
return nil
}

let commandLine = await commandLineFromTemplate(cbc, delegate, optionContext: discoveredCommandLineToolSpecInfo(cbc.producer, cbc.scope, delegate), lookup: lookup).map(\.asString)

// We can't know our precise outputs statically because we don't know what languages are in the xcstrings file,
// nor do we know if any strings have variations (which would require one or more .stringsdict outputs).
Expand Down
57 changes: 57 additions & 0 deletions Sources/SWBCore/FileToBuild.swift
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,61 @@ public extension RegionVariable {
let installLocLanguages = Set(scope.evaluate(BuiltinMacros.INSTALLLOC_LANGUAGE))
return installLocLanguages.isEmpty || installLocLanguages.contains(regionVariantName)
}

/// When the build setting `BUILD_ONLY_KNOWN_LOCALIZATIONS` is active,
/// this region must be present in the project's known localizations
/// or else the file should not be built.
///
/// `delegate` is used to emit a note about skipping this file if the
/// method returns `false`.
///
/// - Returns: `true` if this file can be built because
/// `BUILD_ONLY_KNOWN_LOCALIZATIONS` is disabled, or the file's region
/// is present in the project's supported localizations.
/// If this file should not be built, `false` is returned.
func buildSettingAllowsBuildingLocale(_ scope: MacroEvaluationScope, in project: Project?, _ delegate: (any DiagnosticProducingDelegate)?) -> Bool {

guard let project else {
// We can't find the project. Allow the file to build:
return true
}

// Don't apply the BUILD_ONLY_KNOWN_LOCALIZATIONS setting
// for installloc, because installloc's languages have priority:
let isInstallloc = scope.evaluate(BuiltinMacros.BUILD_COMPONENTS).contains("installLoc")
guard !isInstallloc else {
// We're in the installloc build phase, and we're not interested
// in changing its behavior. Allow the flow to continue:
return true
}

guard scope.evaluate(BuiltinMacros.BUILD_ONLY_KNOWN_LOCALIZATIONS) else {
// Build setting is off, this should build:
return true
}

guard let knownLocalizations = project.knownLocalizations, !knownLocalizations.isEmpty else {
// We can't read the supported localizations, allow it to build:
return true
}

guard let regionVariantName else {
// Unable to get a region name, allow it to built:
return true
}

if regionVariantName == "mul" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if it is Base?

// Allow mul.lproj (multi-lingual) which is used when xcstrings are paired with IB files:
return true
}

if knownLocalizations.contains(regionVariantName) {
// This is a known locale, allow it to build:
return true
}

// This region is not supported, so it shouldn't build.
delegate?.note("Skipping .lproj directory '\(regionVariantName).lproj' because '\(regionVariantName)' is not in project's known localizations (BUILD_ONLY_KNOWN_LOCALIZATIONS is enabled)")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might want to look into specifying file paths for diagnostics like this. I don't mean in the message, but rather as part of the diag instance.

return false
}
}
4 changes: 4 additions & 0 deletions Sources/SWBCore/ProjectModel/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public final class Project: ProjectModelItem, PIFObject, Hashable, Encodable
public let buildConfigurations: [BuildConfiguration]
public let defaultConfigurationName: String
public let developmentRegion: String?
public let knownLocalizations: [String]?
public let classPrefix: String
public let appPreferencesBuildSettings: [String: PropertyListItem]
public let isPackage: Bool
Expand Down Expand Up @@ -100,6 +101,7 @@ public final class Project: ProjectModelItem, PIFObject, Hashable, Encodable
self.buildConfigurations = model.buildConfigurations.map{ BuildConfiguration($0, pifLoader) }
self.defaultConfigurationName = model.defaultConfigurationName
self.developmentRegion = model.developmentRegion
self.knownLocalizations = model.knownLocalizations
self.classPrefix = model.classPrefix
self.appPreferencesBuildSettings = BuildConfiguration.convertMacroBindingSourceToPlistDictionary(model.appPreferencesBuildSettings)

Expand Down Expand Up @@ -147,6 +149,8 @@ public final class Project: ProjectModelItem, PIFObject, Hashable, Encodable
// The development region name is required.
developmentRegion = try Self.parseOptionalValueForKeyAsString(PIFKey_Project_developmentRegion, pifDict: pifDict)

knownLocalizations = try Self.parseOptionalValueForKeyAsArrayOfStrings(PIFKey_Project_knownRegions, pifDict: pifDict)

classPrefix = try Self.parseOptionalValueForKeyAsString(PIFKey_Project_classPrefix, pifDict: pifDict) ?? ""

// Get the application preferences build settings.
Expand Down
2 changes: 2 additions & 0 deletions Sources/SWBCore/Settings/BuiltinMacros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1000,6 +1000,7 @@ public final class BuiltinMacros {
public static let SHARED_FRAMEWORKS_FOLDER_PATH = BuiltinMacros.declarePathMacro("SHARED_FRAMEWORKS_FOLDER_PATH")
public static let SHARED_SUPPORT_FOLDER_PATH = BuiltinMacros.declarePathMacro("SHARED_SUPPORT_FOLDER_PATH")
public static let STRING_CATALOG_GENERATE_SYMBOLS = BuiltinMacros.declareBooleanMacro("STRING_CATALOG_GENERATE_SYMBOLS")
public static let BUILD_ONLY_KNOWN_LOCALIZATIONS = BuiltinMacros.declareBooleanMacro("BUILD_ONLY_KNOWN_LOCALIZATIONS")
public static let STRINGS_FILE_INPUT_ENCODING = BuiltinMacros.declareStringMacro("STRINGS_FILE_INPUT_ENCODING")
public static let STRINGS_FILE_OUTPUT_ENCODING = BuiltinMacros.declareStringMacro("STRINGS_FILE_OUTPUT_ENCODING")
public static let STRINGS_FILE_OUTPUT_FILENAME = BuiltinMacros.declareStringMacro("STRINGS_FILE_OUTPUT_FILENAME")
Expand Down Expand Up @@ -2202,6 +2203,7 @@ public final class BuiltinMacros {
SPECIALIZATION_SDK_OPTIONS,
SRCROOT,
STRING_CATALOG_GENERATE_SYMBOLS,
BUILD_ONLY_KNOWN_LOCALIZATIONS,
STRINGSDATA_DIR,
STRINGS_FILE_INPUT_ENCODING,
STRINGS_FILE_OUTPUT_ENCODING,
Expand Down
7 changes: 7 additions & 0 deletions Sources/SWBCore/Specs/CoreBuildSystem.xcspec
Original file line number Diff line number Diff line change
Expand Up @@ -3696,6 +3696,13 @@ When this setting is enabled:
Category = "Localization";
Description = "When enabled, symbols will be generated for manually-managed strings in String Catalogs.";
},
{ Name = BUILD_ONLY_KNOWN_LOCALIZATIONS;
Type = Boolean;
DefaultValue = NO;
DisplayName = "Build Known Localizations Only";
Category = "Localization";
Description = "When enabled, only builds content for languages explicitly supported by the project.";
},

// rdar://108915072 (Move Siri Category and relevant configs out of CoreBuildSystem)
// Siri Settings
Expand Down
3 changes: 3 additions & 0 deletions Sources/SWBCore/TaskGeneration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ public protocol CommandProducer: PlatformBuildContext, SpecLookupContext, Refere
/// The configured target the command is being produced for, if any.
var configuredTarget: ConfiguredTarget? { get }

/// The project the command is being produced for, if any.
var project: Project? { get }

/// The product type being built. (Only `StandardTarget`s have product types.)
var productType: ProductTypeSpec? { get }

Expand Down
4 changes: 4 additions & 0 deletions Sources/SWBProjectModel/IDE/IDESwiftPackageExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ extension PIF.Project : PIFRepresentable {
dict[PIFKey_Project_developmentRegion] = developmentRegion
}

if let knownLocalizations {
dict[PIFKey_Project_knownRegions] = knownLocalizations
}

return dict
}
}
Expand Down
1 change: 1 addition & 0 deletions Sources/SWBProjectModel/PIFGenerationModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public enum PIF {
public let id: String
public let name: String
public var developmentRegion: String?
public var knownLocalizations: [String]?
public var path: String
public let mainGroup: Group
public var buildConfigs: [BuildConfig]
Expand Down
1 change: 1 addition & 0 deletions Sources/SWBProtocol/PIFKeyConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public let PIFKey_Project_targets = "targets"
public let PIFKey_Project_groupTree = "groupTree"
public let PIFKey_Project_defaultConfigurationName = "defaultConfigurationName"
public let PIFKey_Project_developmentRegion = "developmentRegion"
public let PIFKey_Project_knownRegions = "knownRegions"
public let PIFKey_Project_classPrefix = "classPrefix"
public let PIFKey_Project_appPreferencesBuildSettings = "appPreferencesBuildSettings"

Expand Down
10 changes: 7 additions & 3 deletions Sources/SWBProtocol/ProjectModel/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ public struct Project: Sendable {
public let buildConfigurations: [BuildConfiguration]
public let defaultConfigurationName: String
public let developmentRegion: String?
public let knownLocalizations: [String]?
public let classPrefix: String
public let appPreferencesBuildSettings: [BuildConfiguration.MacroBindingSource]

public init(guid: String, isPackage: Bool, xcodeprojPath: Path, sourceRoot: Path, targetSignatures: [String], groupTree: FileGroup, buildConfigurations: [BuildConfiguration], defaultConfigurationName: String, developmentRegion: String?, classPrefix: String, appPreferencesBuildSettings: [BuildConfiguration.MacroBindingSource] = []) {
public init(guid: String, isPackage: Bool, xcodeprojPath: Path, sourceRoot: Path, targetSignatures: [String], groupTree: FileGroup, buildConfigurations: [BuildConfiguration], defaultConfigurationName: String, developmentRegion: String?, knownLocalizations: [String]?, classPrefix: String, appPreferencesBuildSettings: [BuildConfiguration.MacroBindingSource] = []) {
self.guid = guid
self.isPackage = isPackage
self.xcodeprojPath = xcodeprojPath
Expand All @@ -35,6 +36,7 @@ public struct Project: Sendable {
self.buildConfigurations = buildConfigurations
self.defaultConfigurationName = defaultConfigurationName
self.developmentRegion = developmentRegion
self.knownLocalizations = knownLocalizations
self.classPrefix = classPrefix
self.appPreferencesBuildSettings = appPreferencesBuildSettings
}
Expand All @@ -44,7 +46,7 @@ public struct Project: Sendable {

extension Project: Serializable {
public func serialize<T: Serializer>(to serializer: T) {
serializer.serializeAggregate(11) {
serializer.serializeAggregate(12) {
serializer.serialize(guid)
serializer.serialize(isPackage)
serializer.serialize(xcodeprojPath)
Expand All @@ -54,13 +56,14 @@ extension Project: Serializable {
serializer.serialize(buildConfigurations)
serializer.serialize(defaultConfigurationName)
serializer.serialize(developmentRegion)
serializer.serialize(knownLocalizations)
serializer.serialize(classPrefix)
serializer.serialize(appPreferencesBuildSettings)
}
}

public init(from deserializer: any Deserializer) throws {
try deserializer.beginAggregate(11)
try deserializer.beginAggregate(12)
self.guid = try deserializer.deserialize()
self.isPackage = try deserializer.deserialize()
self.xcodeprojPath = try deserializer.deserialize()
Expand All @@ -70,6 +73,7 @@ extension Project: Serializable {
self.buildConfigurations = try deserializer.deserialize()
self.defaultConfigurationName = try deserializer.deserialize()
self.developmentRegion = try deserializer.deserialize()
self.knownLocalizations = try deserializer.deserialize()
self.classPrefix = try deserializer.deserialize()
self.appPreferencesBuildSettings = try deserializer.deserialize()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ final class ResourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBa
guard isXCStrings || group.isValidLocalizedContent(scope) else { return }
}

// Check if we should filter localizations based on BUILD_ONLY_KNOWN_LOCALIZATIONS:
guard group.buildSettingAllowsBuildingLocale(scope, in: context.project, delegate) else {
return
}

// Compute the path to the effective localized directories (.lproj) in the resources and temp resources directories to define the output file for the tool.

let assetPackInfo = context.onDemandResourcesAssetPack(for: group)
Expand Down Expand Up @@ -172,6 +177,11 @@ final class ResourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBa
}

await appendGeneratedTasks(&tasks) { delegate in
// Check if we should filter localizations based on BUILD_ONLY_KNOWN_LOCALIZATIONS:
guard ftb.buildSettingAllowsBuildingLocale(scope, in: context.project, delegate) else {
return
}

// Compute the output path, taking the region into account.
let assetPackInfo = context.onDemandResourcesAssetPack(for: FileToBuildGroup(nil, files: [ftb], action: nil))
let outputDir = assetPackInfo?.path ?? buildFilesContext.resourcesDir
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1612,6 +1612,13 @@ package final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, F
if isForInstallLoc {
// For installLoc, we really only care about valid localized content from the sources task producer
tasks = tasks.filter { $0.inputs.contains(where: { $0.path.isValidLocalizedContent(scope) || $0.path.fileExtension == "xcstrings" }) }
} else if scope.evaluate(BuiltinMacros.BUILD_ONLY_KNOWN_LOCALIZATIONS) {
// For non-installLoc builds, filter based on BUILD_ONLY_KNOWN_LOCALIZATIONS:
tasks = tasks.filter { task in
task.inputs.allSatisfy { input in
input.path.buildSettingAllowsBuildingLocale(scope, in: context.project, nil)
}
}
}

// Create a task to validate dependencies if that feature is enabled.
Expand Down Expand Up @@ -1706,6 +1713,10 @@ package final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, F
guard isXCStrings || group.isValidLocalizedContent(scope) else { return }
}

guard group.buildSettingAllowsBuildingLocale(scope, in: context.project, delegate) else {
return
}

// Compute the resources directory.
let resourcesDir = buildFilesContext.resourcesDir.join(group.regionVariantPathComponent)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public class TaskProducerContext: StaleFileRemovalContext, BuildFileResolution
var phase: TaskProducerPhase = .none

/// The project this context is for.
let project: Project?
public let project: Project?

/// The high-level global build information.
package let globalProductPlan: GlobalProductPlan
Expand Down
3 changes: 3 additions & 0 deletions Sources/SWBTestSupport/DummyCommandProducer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ package struct MockCommandProducer: CommandProducer, Sendable {
package let platform: Platform?
package let sdk: SDK?
package let sdkVariant: SDKVariant?
package var project: SWBCore.Project? {
return nil
}
package var specRegistry: SpecRegistry {
return core.specRegistry
}
Expand Down
Loading
Loading