diff --git a/Sources/PackageLoading/ManifestJSONParser.swift b/Sources/PackageLoading/ManifestJSONParser.swift index e677ae24e1b..36ecd6b71f0 100644 --- a/Sources/PackageLoading/ManifestJSONParser.swift +++ b/Sources/PackageLoading/ManifestJSONParser.swift @@ -38,6 +38,10 @@ enum ManifestJSONParser { struct Result { var name: String var defaultLocalization: String? + var defaultSwiftSettings: [TargetBuildSettingDescription.Setting]? = [] + var defaultCSettings: [TargetBuildSettingDescription.Setting]? = [] + var defaultCXXSettings: [TargetBuildSettingDescription.Setting]? = [] + var defaultLinkerSettings: [TargetBuildSettingDescription.Setting]? = [] var platforms: [PlatformDescription] = [] var targets: [TargetDescription] = [] var pkgConfig: String? @@ -107,6 +111,10 @@ enum ManifestJSONParser { return Result( name: input.package.name, defaultLocalization: input.package.defaultLocalization?.tag, + defaultSwiftSettings: try input.package.defaultSwiftSettings?.map { try .init($0) }, + defaultCSettings: try input.package.defaultCSettings?.map { try .init($0) }, + defaultCXXSettings: try input.package.defaultCXXSettings?.map { try .init($0) }, + defaultLinkerSettings: try input.package.defaultLinkerSettings?.map { try .init($0) }, platforms: try input.package.platforms.map { try Self.parsePlatforms($0) } ?? [], targets: try input.package.targets.map { try Self.parseTarget(target: $0, identityResolver: identityResolver) }, pkgConfig: input.package.pkgConfig, @@ -197,6 +205,13 @@ enum ManifestJSONParser { let pluginUsages = target.pluginUsages?.map { TargetDescription.PluginUsage.init($0) } + let explictSettings = TargetDescription.ExplicitSettings( + swift: target.swiftSettings != nil, + c: target.cSettings != nil, + cxx: target.cxxSettings != nil, + linker: target.linkerSettings != nil + ) + return try TargetDescription( name: target.name, dependencies: dependencies, @@ -212,6 +227,7 @@ enum ManifestJSONParser { providers: providers, pluginCapability: pluginCapability, settings: try Self.parseBuildSettings(target), + explicitSettings: explictSettings, checksum: target.checksum, pluginUsages: pluginUsages ) @@ -795,6 +811,8 @@ extension TargetBuildSettingDescription.Kind { } return .defaultIsolation(isolation) + case "inherited": + return .inherited default: throw InternalError("invalid build setting \(name)") } diff --git a/Sources/PackageLoading/ManifestLoader.swift b/Sources/PackageLoading/ManifestLoader.swift index c3ee04f840e..e9d9f4dca90 100644 --- a/Sources/PackageLoading/ManifestLoader.swift +++ b/Sources/PackageLoading/ManifestLoader.swift @@ -361,6 +361,14 @@ public final class ManifestLoader: ManifestLoaderProtocol { )) } + let defaultSwiftSettings = parsedManifest.defaultSwiftSettings ?? [] + let defaultCSettings = parsedManifest.defaultCSettings ?? [] + let defaultCXXSettings = parsedManifest.defaultCXXSettings ?? [] + let defaultLinkerSettings = parsedManifest.defaultLinkerSettings ?? [] + + let defaultSettings: [TargetBuildSettingDescription.Setting] = + defaultSwiftSettings + defaultCSettings + defaultCXXSettings + defaultLinkerSettings + let manifest = Manifest( displayName: parsedManifest.name, packageIdentity: packageIdentity, @@ -368,6 +376,7 @@ public final class ManifestLoader: ManifestLoaderProtocol { packageKind: packageKind, packageLocation: packageLocation, defaultLocalization: parsedManifest.defaultLocalization, + defaultSettings: defaultSettings, platforms: parsedManifest.platforms, version: packageVersion?.version, revision: packageVersion?.revision, diff --git a/Sources/PackageLoading/PackageBuilder.swift b/Sources/PackageLoading/PackageBuilder.swift index e1b910486bd..3c2954d40ce 100644 --- a/Sources/PackageLoading/PackageBuilder.swift +++ b/Sources/PackageLoading/PackageBuilder.swift @@ -1086,6 +1086,55 @@ public final class PackageBuilder { } } + private func resolvedSettings(for target: TargetDescription) throws -> [TargetBuildSettingDescription.Setting] { + var resolved: [TargetBuildSettingDescription.Setting] = [] + + // first, validate defaults + for setting in manifest.defaultSettings ?? [] { + if case .unsafeFlags = setting.kind { + throw ModuleError.invalidManifestConfig( + self.identity.description, "default settings cannot contain unsafe flags" + ) + } + } + + // copy over all existing settings, substituting in defaults if inherited is encountered. + for setting in target.settings { + switch setting.kind { + case .inherited: + if setting.condition != nil { + throw ModuleError.invalidManifestConfig( + self.identity.description, "inherited settings cannot use conditions" + ) + } + + let defaults = manifest.defaultSettings?.filter({ $0.tool == setting.tool }) ?? [] + resolved.append(contentsOf: defaults) + default: + resolved.append(setting) + } + } + + // Now, apply the defaults if nothing explicit is present. + if !target.explicitSettings.swift { + resolved.append(contentsOf: manifest.defaultSettings?.filter({ $0.tool == .swift }) ?? []) + } + + if !target.explicitSettings.c { + resolved.append(contentsOf: manifest.defaultSettings?.filter({ $0.tool == .c }) ?? []) + } + + if !target.explicitSettings.cxx { + resolved.append(contentsOf: manifest.defaultSettings?.filter({ $0.tool == .cxx }) ?? []) + } + + if !target.explicitSettings.linker { + resolved.append(contentsOf: manifest.defaultSettings?.filter({ $0.tool == .linker }) ?? []) + } + + return resolved + } + /// Creates build setting assignment table for the given target. func buildSettings( for target: TargetDescription?, @@ -1103,7 +1152,7 @@ public final class PackageBuilder { table.add(versionAssignment, for: .SWIFT_VERSION) // Process each setting. - for setting in target.settings { + for setting in try resolvedSettings(for: target) { if let traits = setting.condition?.traits, traits.intersection(self.enabledTraits.names).isEmpty { // The setting is currently not enabled so we should skip it continue @@ -1341,6 +1390,8 @@ public final class PackageBuilder { } values = ["-default-isolation", isolation.rawValue] + case .inherited: + throw InternalError("inherited cannot be in resolved setttings") } // Create an assignment for this setting. diff --git a/Sources/PackageModel/Manifest/Manifest.swift b/Sources/PackageModel/Manifest/Manifest.swift index de069dc15ef..de37d269c7c 100644 --- a/Sources/PackageModel/Manifest/Manifest.swift +++ b/Sources/PackageModel/Manifest/Manifest.swift @@ -74,6 +74,9 @@ public final class Manifest: Sendable { /// The declared package dependencies. public let dependencies: [PackageDependency] + /// The defaults to be used when resolving settings for all targets. + public let defaultSettings: [TargetBuildSettingDescription.Setting]? + /// The targets declared in the manifest. public let targets: [TargetDescription] @@ -116,6 +119,7 @@ public final class Manifest: Sendable { packageKind: PackageReference.Kind, packageLocation: String, defaultLocalization: String?, + defaultSettings: [TargetBuildSettingDescription.Setting] = [], platforms: [PlatformDescription], version: TSCUtility.Version?, revision: String?, @@ -137,6 +141,7 @@ public final class Manifest: Sendable { self.packageKind = packageKind self.packageLocation = packageLocation self.defaultLocalization = defaultLocalization + self.defaultSettings = defaultSettings self.platforms = platforms self.version = version self.revision = revision @@ -633,7 +638,7 @@ extension Manifest: Encodable { case name, path, url, version, targetMap, toolsVersion, pkgConfig, providers, cLanguageStandard, cxxLanguageStandard, swiftLanguageVersions, dependencies, products, targets, traits, platforms, packageKind, revision, - defaultLocalization + defaultLocalization, defaultSettings } /// Coding user info key for dump-package command. @@ -656,6 +661,7 @@ extension Manifest: Encodable { try container.encode(self.toolsVersion, forKey: .toolsVersion) try container.encode(self.defaultLocalization, forKey: .defaultLocalization) + try container.encode(self.defaultSettings, forKey: .defaultSettings) try container.encode(self.pkgConfig, forKey: .pkgConfig) try container.encode(self.providers, forKey: .providers) try container.encode(self.cLanguageStandard, forKey: .cLanguageStandard) diff --git a/Sources/PackageModel/Manifest/TargetBuildSettingDescription.swift b/Sources/PackageModel/Manifest/TargetBuildSettingDescription.swift index 559a8d2ecec..c6e1f94d320 100644 --- a/Sources/PackageModel/Manifest/TargetBuildSettingDescription.swift +++ b/Sources/PackageModel/Manifest/TargetBuildSettingDescription.swift @@ -60,6 +60,8 @@ public enum TargetBuildSettingDescription { case defaultIsolation(DefaultIsolation) + case inherited + public var isUnsafeFlags: Bool { switch self { case .unsafeFlags(let flags): @@ -67,7 +69,7 @@ public enum TargetBuildSettingDescription { return !flags.isEmpty case .headerSearchPath, .define, .linkedLibrary, .linkedFramework, .interoperabilityMode, .enableUpcomingFeature, .enableExperimentalFeature, .strictMemorySafety, .swiftLanguageMode, - .treatAllWarnings, .treatWarning, .enableWarning, .disableWarning, .defaultIsolation: + .treatAllWarnings, .treatWarning, .enableWarning, .disableWarning, .defaultIsolation, .inherited: return false } } @@ -93,5 +95,34 @@ public enum TargetBuildSettingDescription { self.kind = kind self.condition = condition } + + public func overridesDefault(_ defaultSetting: Setting) -> Bool { + guard tool == defaultSetting.tool else { + return false + } + + switch (kind, defaultSetting.kind) { + case (.defaultIsolation, .defaultIsolation): + return true + case (.interoperabilityMode, .interoperabilityMode): + return true + case (.swiftLanguageMode, .swiftLanguageMode): + return true + case (.treatAllWarnings, .treatAllWarnings): + return true + case (.unsafeFlags, .unsafeFlags): + return true + case (.define, .define): + return true + case (.treatWarning(let value, _), .treatWarning(let defaultValue, _)): + return value == defaultValue + case (.enableWarning(let value), .disableWarning(let defaultValue)): + return value == defaultValue + case (.disableWarning(let value), .enableWarning(let defaultValue)): + return value == defaultValue + default: + return false + } + } } } diff --git a/Sources/PackageModel/Manifest/TargetDescription.swift b/Sources/PackageModel/Manifest/TargetDescription.swift index f75de84fd13..e16b0a5e850 100644 --- a/Sources/PackageModel/Manifest/TargetDescription.swift +++ b/Sources/PackageModel/Manifest/TargetDescription.swift @@ -183,6 +183,25 @@ public struct TargetDescription: Hashable, Encodable, Sendable { /// The target-specific build settings declared in this target. public let settings: [TargetBuildSettingDescription.Setting] + /// Models the presence of explicitly-defined settings for each tool type. + public let explicitSettings: ExplicitSettings + + public struct ExplicitSettings: Hashable, Codable, Sendable { + public var swift: Bool + public var c: Bool + public var cxx: Bool + public var linker: Bool + + public init(swift: Bool, c: Bool, cxx: Bool, linker: Bool) { + self.swift = swift + self.c = c + self.cxx = cxx + self.linker = linker + } + + public static let all = Self.init(swift: true, c: true, cxx: true, linker: true) + public static let none = Self.init(swift: false, c: false, cxx: false, linker: false) + } /// The binary target checksum. public let checksum: String? @@ -209,6 +228,7 @@ public struct TargetDescription: Hashable, Encodable, Sendable { providers: [SystemPackageProviderDescription]? = nil, pluginCapability: PluginCapability? = nil, settings: [TargetBuildSettingDescription.Setting] = [], + explicitSettings: ExplicitSettings = .all, checksum: String? = nil, pluginUsages: [PluginUsage]? = nil ) throws { @@ -445,6 +465,43 @@ public struct TargetDescription: Hashable, Encodable, Sendable { ) } } + // ensure that settings and settings presense detection are consistent + if settings.filter({ $0.tool == .swift}).isEmpty == false && explicitSettings.swift == false { + throw Error.disallowedPropertyInTarget( + targetName: name, + targetType: targetType, + propertyName: "explicitSettings", + value: "swift" + ) + } + + if settings.filter({ $0.tool == .c}).isEmpty == false && explicitSettings.c == false { + throw Error.disallowedPropertyInTarget( + targetName: name, + targetType: targetType, + propertyName: "explicitSettings", + value: "c" + ) + } + + if settings.filter({ $0.tool == .cxx}).isEmpty == false && explicitSettings.cxx == false { + throw Error.disallowedPropertyInTarget( + targetName: name, + targetType: targetType, + propertyName: "explicitSettings", + value: "cxx" + ) + } + + if settings.filter({ $0.tool == .linker}).isEmpty == false && explicitSettings.linker == false { + throw Error.disallowedPropertyInTarget( + targetName: name, + targetType: targetType, + propertyName: "explicitSettings", + value: "linker" + ) + } + self.name = name self.dependencies = dependencies self.path = path @@ -461,6 +518,7 @@ public struct TargetDescription: Hashable, Encodable, Sendable { self.settings = settings self.checksum = checksum self.pluginUsages = pluginUsages + self.explicitSettings = explicitSettings } } diff --git a/Sources/PackageModel/ManifestSourceGeneration.swift b/Sources/PackageModel/ManifestSourceGeneration.swift index 9c9a19a3a2d..2bea59f84df 100644 --- a/Sources/PackageModel/ManifestSourceGeneration.swift +++ b/Sources/PackageModel/ManifestSourceGeneration.swift @@ -727,6 +727,8 @@ fileprivate extension SourceCodeFragment { params.append(SourceCodeFragment(from: condition)) } self.init(enum: setting.kind.name, subnodes: params) + case .inherited: + self.init(enum: setting.kind.name, subnodes: []) } } @@ -1215,6 +1217,8 @@ extension TargetBuildSettingDescription.Kind { return "disableWarning" case .defaultIsolation: return "defaultIsolation" + case .inherited: + return "inherited" } } } diff --git a/Sources/Runtimes/PackageDescription/BuildSettings.swift b/Sources/Runtimes/PackageDescription/BuildSettings.swift index 0f4074dbef5..370fb534a48 100644 --- a/Sources/Runtimes/PackageDescription/BuildSettings.swift +++ b/Sources/Runtimes/PackageDescription/BuildSettings.swift @@ -289,6 +289,20 @@ public struct CSetting: Sendable { return CSetting( name: "disableWarning", value: [name], condition: condition) } + + /// Inherit default settings. + /// + /// - Since: First available in PackageDescription 6.5. + /// + /// This setting is a placeholder that will be substituted with any value set by `defaultCSettings`. + @available(_PackageDescription, introduced: 6.2) + public static func inherited() -> CSetting { + return CSetting( + name: "inherited", + value: ["placeholder"], + condition: nil + ) + } } /// A CXX-language build setting. @@ -445,6 +459,20 @@ public struct CXXSetting: Sendable { return CXXSetting( name: "disableWarning", value: [name], condition: condition) } + + /// Inherit default settings. + /// + /// - Since: First available in PackageDescription 6.5. + /// + /// This setting is a placeholder that will be substituted with any value set by `defaultCXXSettings`. + @available(_PackageDescription, introduced: 6.2) + public static func inherited() -> CXXSetting { + return CXXSetting( + name: "inherited", + value: ["placeholder"], + condition: nil + ) + } } /// A Swift language build setting. @@ -714,6 +742,20 @@ public struct SwiftSetting: Sendable { return SwiftSetting( name: "defaultIsolation", value: [isolationString], condition: condition) } + + /// Inherit default settings. + /// + /// - Since: First available in PackageDescription 6.5. + /// + /// This setting is a placeholder that will be substituted with any value set by `defaultSwiftSettings`. + @available(_PackageDescription, introduced: 6.2) + public static func inherited() -> SwiftSetting { + return SwiftSetting( + name: "inherited", + value: ["placeholder"], + condition: nil + ) + } } /// A linker build setting. @@ -778,4 +820,18 @@ public struct LinkerSetting: Sendable { public static func unsafeFlags(_ flags: [String], _ condition: BuildSettingCondition? = nil) -> LinkerSetting { return LinkerSetting(name: "unsafeFlags", value: flags, condition: condition) } + + /// Inherit default settings. + /// + /// - Since: First available in PackageDescription 6.5. + /// + /// This setting is a placeholder that will be substituted with any value set by `defaultLinkerSettings`. + @available(_PackageDescription, introduced: 6.2) + public static func inherited() -> LinkerSetting { + return LinkerSetting( + name: "inherited", + value: ["placeholder"], + condition: nil + ) + } } diff --git a/Sources/Runtimes/PackageDescription/PackageDescription.swift b/Sources/Runtimes/PackageDescription/PackageDescription.swift index 60e2d769583..d119a844893 100644 --- a/Sources/Runtimes/PackageDescription/PackageDescription.swift +++ b/Sources/Runtimes/PackageDescription/PackageDescription.swift @@ -114,6 +114,19 @@ public final class Package { set { swiftLanguageModes = newValue } } + /// The default Swift language settings merged with all per-target settings. + @available(_PackageDescription, introduced: 6.5) + public var defaultSwiftSettings: [SwiftSetting]? + + @available(_PackageDescription, introduced: 6.5) + public var defaultCSettings: [CSetting]? + + @available(_PackageDescription, introduced: 6.5) + public var defaultCXXSettings: [CXXSetting]? + + @available(_PackageDescription, introduced: 6.5) + public var defaultLinkerSettings: [LinkerSetting]? + /// The C language standard to use for all C targets in this package. public var cLanguageStandard: CLanguageStandard? @@ -315,8 +328,12 @@ public final class Package { dependencies: [Dependency] = [], targets: [Target] = [], swiftLanguageModes: [SwiftLanguageMode]? = nil, + defaultSwiftSettings: [SwiftSetting]? = nil, cLanguageStandard: CLanguageStandard? = nil, - cxxLanguageStandard: CXXLanguageStandard? = nil + defaultCSettings: [CSetting]? = nil, + cxxLanguageStandard: CXXLanguageStandard? = nil, + defaultCXXSettings: [CXXSetting]? = nil, + defaultLinkerSettings: [LinkerSetting]? = nil ) { self.name = name self.defaultLocalization = defaultLocalization @@ -328,8 +345,12 @@ public final class Package { self.targets = targets self.traits = [] self.swiftLanguageModes = swiftLanguageModes + self.defaultSwiftSettings = defaultSwiftSettings self.cLanguageStandard = cLanguageStandard + self.defaultCSettings = defaultCSettings self.cxxLanguageStandard = cxxLanguageStandard + self.defaultCXXSettings = defaultCXXSettings + self.defaultLinkerSettings = defaultLinkerSettings registerExitHandler() } @@ -362,8 +383,12 @@ public final class Package { dependencies: [Dependency] = [], targets: [Target] = [], swiftLanguageModes: [SwiftLanguageMode]? = nil, + defaultSwiftSettings: [SwiftSetting]? = nil, cLanguageStandard: CLanguageStandard? = nil, - cxxLanguageStandard: CXXLanguageStandard? = nil + defaultCSettings: [CSetting]? = nil, + cxxLanguageStandard: CXXLanguageStandard? = nil, + defaultCXXSettings: [CXXSetting]? = nil, + defaultLinkerSettings: [LinkerSetting]? = nil ) { self.name = name self.defaultLocalization = defaultLocalization @@ -375,8 +400,12 @@ public final class Package { self.dependencies = dependencies self.targets = targets self.swiftLanguageModes = swiftLanguageModes + self.defaultSwiftSettings = defaultSwiftSettings self.cLanguageStandard = cLanguageStandard + self.defaultCSettings = defaultCSettings self.cxxLanguageStandard = cxxLanguageStandard + self.defaultCXXSettings = defaultCXXSettings + self.defaultLinkerSettings = defaultLinkerSettings registerExitHandler() } diff --git a/Sources/Runtimes/PackageDescription/PackageDescriptionSerialization.swift b/Sources/Runtimes/PackageDescription/PackageDescriptionSerialization.swift index d37021c7b04..8a1e6e3e506 100644 --- a/Sources/Runtimes/PackageDescription/PackageDescriptionSerialization.swift +++ b/Sources/Runtimes/PackageDescription/PackageDescriptionSerialization.swift @@ -294,6 +294,10 @@ enum Serialization { let name: String let platforms: [SupportedPlatform]? let defaultLocalization: LanguageTag? + let defaultSwiftSettings: [SwiftSetting]? + let defaultCSettings: [CSetting]? + let defaultCXXSettings: [CXXSetting]? + let defaultLinkerSettings: [LinkerSetting]? let pkgConfig: String? let providers: [SystemPackageProvider]? let targets: [Target] diff --git a/Sources/Runtimes/PackageDescription/PackageDescriptionSerializationConversion.swift b/Sources/Runtimes/PackageDescription/PackageDescriptionSerializationConversion.swift index 5d4a0336362..9e29045da23 100644 --- a/Sources/Runtimes/PackageDescription/PackageDescriptionSerializationConversion.swift +++ b/Sources/Runtimes/PackageDescription/PackageDescriptionSerializationConversion.swift @@ -401,6 +401,10 @@ extension Serialization.Package { self.name = package.name self.platforms = package.platforms?.map { .init($0) } self.defaultLocalization = package.defaultLocalization.map { .init($0) } + self.defaultSwiftSettings = package.defaultSwiftSettings?.map { .init($0) } + self.defaultCSettings = package.defaultCSettings?.map { .init($0) } + self.defaultCXXSettings = package.defaultCXXSettings?.map { .init($0) } + self.defaultLinkerSettings = package.defaultLinkerSettings?.map { .init($0) } self.pkgConfig = package.pkgConfig self.providers = package.providers?.map { .init($0) } self.targets = package.targets.map { .init($0) } diff --git a/Sources/Workspace/Workspace+Registry.swift b/Sources/Workspace/Workspace+Registry.swift index 8cc7514e3cb..19077950d0b 100644 --- a/Sources/Workspace/Workspace+Registry.swift +++ b/Sources/Workspace/Workspace+Registry.swift @@ -292,6 +292,7 @@ extension Workspace { providers: target.providers, pluginCapability: target.pluginCapability, settings: target.settings, + explicitSettings: target.explicitSettings, checksum: target.checksum, pluginUsages: target.pluginUsages ) diff --git a/Sources/_InternalTestSupport/ManifestExtensions.swift b/Sources/_InternalTestSupport/ManifestExtensions.swift index 3891c033a11..ed91d5cd9de 100644 --- a/Sources/_InternalTestSupport/ManifestExtensions.swift +++ b/Sources/_InternalTestSupport/ManifestExtensions.swift @@ -21,6 +21,7 @@ extension Manifest { displayName: String, path: AbsolutePath = .root, defaultLocalization: String? = nil, + defaultSettings: [TargetBuildSettingDescription.Setting] = [], platforms: [PlatformDescription] = [], version: TSCUtility.Version? = nil, toolsVersion: ToolsVersion = .v4, @@ -42,6 +43,7 @@ extension Manifest { packageIdentity: .plain(displayName.lowercased()), packageLocation: path.pathString, defaultLocalization: defaultLocalization, + defaultSettings: defaultSettings, platforms: platforms, version: version, toolsVersion: toolsVersion, @@ -226,6 +228,7 @@ extension Manifest { packageIdentity: PackageIdentity, packageLocation: String? = nil, defaultLocalization: String? = nil, + defaultSettings: [TargetBuildSettingDescription.Setting] = [], platforms: [PlatformDescription] = [], version: TSCUtility.Version? = nil, toolsVersion: ToolsVersion, @@ -247,6 +250,7 @@ extension Manifest { packageKind: packageKind, packageLocation: packageLocation ?? path.pathString, defaultLocalization: defaultLocalization, + defaultSettings: defaultSettings, platforms: platforms, version: version, revision: .none, diff --git a/Tests/PackageLoadingTests/DefaultSettingsTests.swift b/Tests/PackageLoadingTests/DefaultSettingsTests.swift new file mode 100644 index 00000000000..764fba3936f --- /dev/null +++ b/Tests/PackageLoadingTests/DefaultSettingsTests.swift @@ -0,0 +1,548 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2026 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 Basics +import PackageLoading +import PackageModel +import _InternalTestSupport +import Testing + +struct DefaultLoadingTests { + @Test( + .tags( + Tag.Feature.TargetSettings + ) + ) + func defaultSettingsManifestLoading() async throws { + let content = """ + import PackageDescription + let package = Package( + name: "Foo", + products: [], + targets: [ + .target( + name: "Foo", + ), + .target( + name: "Bar", + swiftSettings: [ + .swiftLanguageMode(.v6), + ] + ), + .target( + name: "Baz", + cSettings: [ + .inherited() + ], + cxxSettings: [ + .inherited() + ], + swiftSettings: [ + .inherited() + ], + linkerSettings: [ + .inherited() + ], + ), + ], + defaultSwiftSettings: [ + .swiftLanguageMode(.v5), + ], + defaultCSettings: [ + .headerSearchPath("foo"), + ], + defaultCXXSettings: [ + .headerSearchPath("bar"), + ], + defaultLinkerSettings: [ + .linkedLibrary("mylib"), + ], + ) + """ + + let observability = ObservabilitySystem.makeForTesting() + let (manifest, validationDiagnostics) = try await PackageDescriptionLoadingTests + .loadAndValidateManifest( + content, + toolsVersion: .v6_2, + packageKind: .fileSystem(.root), + manifestLoader: ManifestLoader( + toolchain: try! UserToolchain.default + ), + observabilityScope: observability.topScope + ) + try expectDiagnostics(validationDiagnostics) { results in + results.checkIsEmpty() + } + try expectDiagnostics(observability.diagnostics) { results in + results.checkIsEmpty() + } + + let expected: [TargetBuildSettingDescription.Setting] = [ + .init(tool: .swift, kind: .swiftLanguageMode(.v5)), + .init(tool: .c, kind: .headerSearchPath("foo")), + .init(tool: .cxx, kind: .headerSearchPath("bar")), + .init(tool: .linker, kind: .linkedLibrary("mylib")), + ] + + #expect(manifest.defaultSettings == expected) + } + + @Test + func swiftToolResolution() throws { + let fs = InMemoryFileSystem(emptyFiles: + "/Sources/A/a.swift", + "/Sources/B/b.swift", + "/Sources/C/c.swift", + "/Sources/D/d.swift", + ) + + let manifest = Manifest.createRootManifest( + displayName: "pkg", + defaultSettings: [ + .init(tool: .swift, kind: .defaultIsolation(.MainActor)) + ], + toolsVersion: .v6_2, + targets: [ + try TargetDescription( + name: "A", + explicitSettings: .none + ), + try TargetDescription( + name: "B", + settings: [], + explicitSettings: .all + ), + try TargetDescription( + name: "C", + settings: [ + .init(tool: .swift, kind: .defaultIsolation(.nonisolated)) + ], + explicitSettings: .init(swift: true, c: false, cxx: false, linker: false) + ), + try TargetDescription( + name: "D", + settings: [ + .init(tool: .swift, kind: .inherited), + .init(tool: .swift, kind: .defaultIsolation(.nonisolated)) + ], + explicitSettings: .init(swift: true, c: false, cxx: false, linker: false) + ), + ] + ) + + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("A") { package in + let macosDebugScope = BuildSettings.Scope( + package.target.buildSettings, + environment: BuildEnvironment(platform: .macOS, configuration: .debug) + ) + #expect(macosDebugScope.evaluate(.OTHER_SWIFT_FLAGS).contains("-default-isolation")) + #expect(macosDebugScope.evaluate(.OTHER_SWIFT_FLAGS).contains("MainActor")) + } + + try package.checkModule("B") { package in + let macosDebugScope = BuildSettings.Scope( + package.target.buildSettings, + environment: BuildEnvironment(platform: .macOS, configuration: .debug) + ) + #expect(macosDebugScope.evaluate(.OTHER_SWIFT_FLAGS).contains("-default-isolation") == false) + #expect(macosDebugScope.evaluate(.OTHER_SWIFT_FLAGS).contains("MainActor") == false) + } + + try package.checkModule("C") { package in + let macosDebugScope = BuildSettings.Scope( + package.target.buildSettings, + environment: BuildEnvironment(platform: .macOS, configuration: .debug) + ) + #expect(macosDebugScope.evaluate(.OTHER_SWIFT_FLAGS).contains("-default-isolation")) + #expect(macosDebugScope.evaluate(.OTHER_SWIFT_FLAGS).contains("nonisolated")) + #expect(macosDebugScope.evaluate(.OTHER_SWIFT_FLAGS).contains("MainActor") == false) + } + + try package.checkModule("D") { package in + let macosDebugScope = BuildSettings.Scope( + package.target.buildSettings, + environment: BuildEnvironment(platform: .macOS, configuration: .debug) + ) + #expect(macosDebugScope.evaluate(.OTHER_SWIFT_FLAGS).contains("-default-isolation")) + #expect(macosDebugScope.evaluate(.OTHER_SWIFT_FLAGS).contains("nonisolated")) + #expect(macosDebugScope.evaluate(.OTHER_SWIFT_FLAGS).contains("MainActor")) + } + } + } + + @Test + func cToolResolution() throws { + let fs = InMemoryFileSystem(emptyFiles: + "/Sources/A/a.c", + "/Sources/B/b.c", + "/Sources/C/c.c", + "/Sources/D/d.c", + ) + + let manifest = Manifest.createRootManifest( + displayName: "pkg", + defaultSettings: [ + .init(tool: .c, kind: .headerSearchPath("foo")) + ], + toolsVersion: .v6_2, + targets: [ + try TargetDescription( + name: "A", + publicHeadersPath: ".", + explicitSettings: .none + ), + try TargetDescription( + name: "B", + publicHeadersPath: ".", + settings: [], + explicitSettings: .all + ), + try TargetDescription( + name: "C", + publicHeadersPath: ".", + settings: [ + .init(tool: .c, kind: .headerSearchPath("bar")), + ], + explicitSettings: .init(swift: false, c: true, cxx: false, linker: false) + ), + try TargetDescription( + name: "D", + publicHeadersPath: ".", + settings: [ + .init(tool: .c, kind: .inherited), + .init(tool: .c, kind: .headerSearchPath("bar")), + ], + explicitSettings: .init(swift: false, c: true, cxx: false, linker: false) + ), + ] + ) + + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("A") { package in + let macosDebugScope = BuildSettings.Scope( + package.target.buildSettings, + environment: BuildEnvironment(platform: .macOS, configuration: .debug) + ) + #expect(macosDebugScope.evaluate(.HEADER_SEARCH_PATHS).contains("foo")) + } + + try package.checkModule("B") { package in + let macosDebugScope = BuildSettings.Scope( + package.target.buildSettings, + environment: BuildEnvironment(platform: .macOS, configuration: .debug) + ) + #expect(macosDebugScope.evaluate(.HEADER_SEARCH_PATHS).contains("foo") == false) + } + + try package.checkModule("C") { package in + let macosDebugScope = BuildSettings.Scope( + package.target.buildSettings, + environment: BuildEnvironment(platform: .macOS, configuration: .debug) + ) + #expect(macosDebugScope.evaluate(.HEADER_SEARCH_PATHS).contains("foo") == false) + #expect(macosDebugScope.evaluate(.HEADER_SEARCH_PATHS).contains("bar")) + } + + try package.checkModule("D") { package in + let macosDebugScope = BuildSettings.Scope( + package.target.buildSettings, + environment: BuildEnvironment(platform: .macOS, configuration: .debug) + ) + #expect(macosDebugScope.evaluate(.HEADER_SEARCH_PATHS).contains("foo")) + #expect(macosDebugScope.evaluate(.HEADER_SEARCH_PATHS).contains("bar")) + } + } + } + + @Test + func cxxToolResolution() throws { + let fs = InMemoryFileSystem(emptyFiles: + "/Sources/A/a.c", + "/Sources/B/b.c", + "/Sources/C/c.c", + "/Sources/D/d.c", + ) + + let manifest = Manifest.createRootManifest( + displayName: "pkg", + defaultSettings: [ + .init(tool: .cxx, kind: .headerSearchPath("foo")) + ], + toolsVersion: .v6_2, + targets: [ + try TargetDescription( + name: "A", + publicHeadersPath: ".", + explicitSettings: .none + ), + try TargetDescription( + name: "B", + publicHeadersPath: ".", + settings: [], + explicitSettings: .all + ), + try TargetDescription( + name: "C", + publicHeadersPath: ".", + settings: [ + .init(tool: .cxx, kind: .headerSearchPath("bar")), + ], + explicitSettings: .init(swift: false, c: false, cxx: true, linker: false) + ), + try TargetDescription( + name: "D", + publicHeadersPath: ".", + settings: [ + .init(tool: .cxx, kind: .inherited), + .init(tool: .cxx, kind: .headerSearchPath("bar")), + ], + explicitSettings: .init(swift: false, c: false, cxx: true, linker: false) + ), + ] + ) + + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("A") { package in + let macosDebugScope = BuildSettings.Scope( + package.target.buildSettings, + environment: BuildEnvironment(platform: .macOS, configuration: .debug) + ) + #expect(macosDebugScope.evaluate(.HEADER_SEARCH_PATHS).contains("foo")) + } + + try package.checkModule("B") { package in + let macosDebugScope = BuildSettings.Scope( + package.target.buildSettings, + environment: BuildEnvironment(platform: .macOS, configuration: .debug) + ) + #expect(macosDebugScope.evaluate(.HEADER_SEARCH_PATHS).contains("foo") == false) + } + + try package.checkModule("C") { package in + let macosDebugScope = BuildSettings.Scope( + package.target.buildSettings, + environment: BuildEnvironment(platform: .macOS, configuration: .debug) + ) + #expect(macosDebugScope.evaluate(.HEADER_SEARCH_PATHS).contains("foo") == false) + #expect(macosDebugScope.evaluate(.HEADER_SEARCH_PATHS).contains("bar")) + } + + try package.checkModule("D") { package in + let macosDebugScope = BuildSettings.Scope( + package.target.buildSettings, + environment: BuildEnvironment(platform: .macOS, configuration: .debug) + ) + #expect(macosDebugScope.evaluate(.HEADER_SEARCH_PATHS).contains("foo")) + #expect(macosDebugScope.evaluate(.HEADER_SEARCH_PATHS).contains("bar")) + } + } + } + + @Test + func linkerToolResolution() throws { + let fs = InMemoryFileSystem(emptyFiles: + "/Sources/A/a.c", + "/Sources/B/b.c", + "/Sources/C/c.c", + "/Sources/D/d.c", + ) + + let manifest = Manifest.createRootManifest( + displayName: "pkg", + defaultSettings: [ + .init(tool: .linker, kind: .linkedLibrary("mylib")) + ], + toolsVersion: .v6_2, + targets: [ + try TargetDescription( + name: "A", + publicHeadersPath: ".", + explicitSettings: .none + ), + try TargetDescription( + name: "B", + publicHeadersPath: ".", + settings: [], + explicitSettings: .all + ), + try TargetDescription( + name: "C", + publicHeadersPath: ".", + settings: [ + .init(tool: .linker, kind: .linkedLibrary("yourlib")), + ], + explicitSettings: .init(swift: false, c: false, cxx: false, linker: true) + ), + try TargetDescription( + name: "D", + publicHeadersPath: ".", + settings: [ + .init(tool: .linker, kind: .inherited), + .init(tool: .linker, kind: .linkedLibrary("yourlib")), + ], + explicitSettings: .init(swift: false, c: false, cxx: false, linker: true) + ), + + ] + ) + + try PackageBuilderTester(manifest, in: fs) { package, _ in + try package.checkModule("A") { package in + let macosDebugScope = BuildSettings.Scope( + package.target.buildSettings, + environment: BuildEnvironment(platform: .macOS, configuration: .debug) + ) + #expect(macosDebugScope.evaluate(.LINK_LIBRARIES).contains("mylib")) + } + + try package.checkModule("B") { package in + let macosDebugScope = BuildSettings.Scope( + package.target.buildSettings, + environment: BuildEnvironment(platform: .macOS, configuration: .debug) + ) + #expect(macosDebugScope.evaluate(.LINK_LIBRARIES).contains("mylib") == false) + } + + try package.checkModule("C") { package in + let macosDebugScope = BuildSettings.Scope( + package.target.buildSettings, + environment: BuildEnvironment(platform: .macOS, configuration: .debug) + ) + #expect(macosDebugScope.evaluate(.LINK_LIBRARIES).contains("mylib") == false) + #expect(macosDebugScope.evaluate(.LINK_LIBRARIES).contains("yourlib")) + } + + try package.checkModule("D") { package in + let macosDebugScope = BuildSettings.Scope( + package.target.buildSettings, + environment: BuildEnvironment(platform: .macOS, configuration: .debug) + ) + #expect(macosDebugScope.evaluate(.LINK_LIBRARIES).contains("mylib")) + #expect(macosDebugScope.evaluate(.LINK_LIBRARIES).contains("yourlib")) + } + } + } + + @Test + func defaultUnsafeFlagsAreRejected() throws { + let fs = InMemoryFileSystem(emptyFiles: + "/Sources/A/a.swift", + "/Sources/B/b.swift", + "/Sources/C/c.swift", + "/Sources/D/d.swift", + ) + + let manifest = Manifest.createRootManifest( + displayName: "pkg", + defaultSettings: [ + .init(tool: .swift, kind: .unsafeFlags(["anything"])) + ], + toolsVersion: .v6_2, + targets: [ + try TargetDescription( + name: "A", + publicHeadersPath: ".", + explicitSettings: .none + ), + try TargetDescription( + name: "B", + publicHeadersPath: ".", + settings: [], + explicitSettings: .all + ), + try TargetDescription( + name: "C", + publicHeadersPath: ".", + settings: [ + .init(tool: .swift, kind: .unsafeFlags(["another"])) + ], + explicitSettings: .init(swift: true, c: false, cxx: false, linker: false) + ), + try TargetDescription( + name: "D", + publicHeadersPath: ".", + settings: [ + .init(tool: .swift, kind: .inherited), + .init(tool: .swift, kind: .unsafeFlags(["another"])) + ], + explicitSettings: .init(swift: true, c: false, cxx: false, linker: false) + ), + ] + ) + + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in + diagnostics.check( + diagnostic: "configuration of package '\(package.packageIdentity)' is invalid; default settings cannot contain unsafe flags", + severity: .error + ) + } + } + + @Test + func emptyDefaultsAreAccepted() throws { + let fs = InMemoryFileSystem(emptyFiles: + "/Sources/A/a.swift", + ) + + let manifest = Manifest.createRootManifest( + displayName: "pkg", + defaultSettings: [ + ], + toolsVersion: .v6_2, + targets: [ + try TargetDescription( + name: "A", + settings: [ + .init(tool: .swift, kind: .inherited) + ], + explicitSettings: .init(swift: true, c: false, cxx: false, linker: false) + ), + ] + ) + + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in + try package.checkModule("A") { module in + } + } + } + + @Test + func inheritanceWithoutSwiftDefaultsIsRejected() throws { + let fs = InMemoryFileSystem(emptyFiles: + "/Sources/A/a.swift", + ) + + let manifest = Manifest.createRootManifest( + displayName: "pkg", + toolsVersion: .v6_2, + targets: [ + try TargetDescription( + name: "A", + settings: [ + .init(tool: .swift, kind: .inherited) + ], + explicitSettings: .init(swift: true, c: false, cxx: false, linker: false) + ), + ] + ) + + try PackageBuilderTester(manifest, in: fs) { package, diagnostics in + diagnostics.check( + diagnostic: "configuration of package '\(package.packageIdentity)' is invalid; inheritance cannot be used without default values", + severity: .error + ) + } + } + +}