From 2187e3fcb790c19c8647628e9e10616f111a9624 Mon Sep 17 00:00:00 2001 From: yume190 Date: Mon, 21 Sep 2026 14:34:57 +0800 Subject: [PATCH 01/47] Resolve a dependency's condition, and hold the C-family side to a package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two more fixtures, and four generator bugs between them. `spm/DependencyCondition` depends on one package only on Linux and on another only behind a trait nobody enables. Both were linked anyway: a target dependency's condition was parsed away with the rest of the array it is dumped in. It is read now, and resolved where a setting's condition already was. A platform nothing in the graph builds is dropped even when the project's own platforms are unknown — no Apple toolchain builds Linux, and what the condition excludes says so in an `#error`. `spm/Clang` is a C-family target with its headers somewhere of its own, a private header search path, defines with and without a value, a C++ target it calls through interoperability, and a resource bundle. Three things were wrong: - `.define("C_VALUE", to: "7")` became `-DC_VALUE -D7`, two flags, one of them nonsense. It is `-DC_VALUE=7`. - Both language standards went onto every C-family rule, so an ObjC source was compiled with `-std=gnu++17` and clang refused it. The standard is now the one that fits what the target compiles, and a target that compiles both is named in the report instead: one rule takes one `-std`. - `.interoperabilityMode(.Cxx)` became `-cxx-interoperability-mode=Cxx`, which swiftc rejects. The mode is `default`. And the C-family bundle accessor looked for the bundle beside the binary only, where the Swift one also looks under `Resources`: on macOS that is where a bundle's resources are, so a C target never found its own. The workspace also answers two questions about itself now: `bazel list config` for the `--config=` it defines, and `bazel list trait` for the traits its packages declare and which of them are on. Bazel has no way to add a command, but its launcher does — Bazelisk runs `tools/bazel` and hands it the real binary in `BAZEL_REAL` — so the wrapper answers `list` without starting a server and passes everything else through. Both are read from the workspace as it is now, not written into it at generation time: a `.bazelrc` gets edited, and a manifest's traits change with the manifest. Both answer for any workspace, because no configuration and no trait are answers too. --- .github/workflows/swift.yml | 10 ++ Sources/Bazelize/Command.swift | 44 +++++ Sources/BazelizeKit/List/Listing.swift | 155 ++++++++++++++++++ .../BazelizeKit/SwiftPM/SwiftPM+Clang.swift | 60 +++++-- .../SwiftPM/SwiftPM+Generator.swift | 8 + .../SwiftPM/SwiftPM+Manifest.swift | 109 +++++++++--- .../SwiftPM/SwiftPM+PluginRule.swift | 50 ++++++ .../SwiftPM/SwiftPM+Resources.swift | 9 +- .../SwiftPM/SwiftPM+Settings.swift | 6 +- docs/SPM.md | 26 ++- docs/SPM_ZH.md | 24 ++- spm/Clang/Package.swift | 43 +++++ spm/Clang/Sources/CObject/CObject.m | 35 ++++ .../Sources/CObject/Resources/greeting.txt | 1 + spm/Clang/Sources/CObject/headers/CObject.h | 16 ++ .../Sources/CObject/internal/CInternal.h | 8 + spm/Clang/Sources/Consumer/Consumer.swift | 24 +++ spm/Clang/Sources/CxxLib/CxxLib.cpp | 7 + spm/Clang/Sources/CxxLib/include/CxxLib.hpp | 7 + spm/Clang/Tests/ClangTests/ClangTests.swift | 23 +++ spm/DependencyCondition/Extras/Package.swift | 14 ++ .../Extras/Sources/Extras/Extras.swift | 5 + .../LinuxOnly/Package.swift | 13 ++ .../Sources/LinuxOnly/LinuxOnly.swift | 7 + spm/DependencyCondition/Package.swift | 35 ++++ .../Sources/Always/Always.swift | 3 + .../Sources/Conditional/Conditional.swift | 5 + .../ConditionalTests/ConditionalTests.swift | 7 + spm/README.md | 9 +- 29 files changed, 718 insertions(+), 45 deletions(-) create mode 100644 Sources/BazelizeKit/List/Listing.swift create mode 100644 spm/Clang/Package.swift create mode 100644 spm/Clang/Sources/CObject/CObject.m create mode 100644 spm/Clang/Sources/CObject/Resources/greeting.txt create mode 100644 spm/Clang/Sources/CObject/headers/CObject.h create mode 100644 spm/Clang/Sources/CObject/internal/CInternal.h create mode 100644 spm/Clang/Sources/Consumer/Consumer.swift create mode 100644 spm/Clang/Sources/CxxLib/CxxLib.cpp create mode 100644 spm/Clang/Sources/CxxLib/include/CxxLib.hpp create mode 100644 spm/Clang/Tests/ClangTests/ClangTests.swift create mode 100644 spm/DependencyCondition/Extras/Package.swift create mode 100644 spm/DependencyCondition/Extras/Sources/Extras/Extras.swift create mode 100644 spm/DependencyCondition/LinuxOnly/Package.swift create mode 100644 spm/DependencyCondition/LinuxOnly/Sources/LinuxOnly/LinuxOnly.swift create mode 100644 spm/DependencyCondition/Package.swift create mode 100644 spm/DependencyCondition/Sources/Always/Always.swift create mode 100644 spm/DependencyCondition/Sources/Conditional/Conditional.swift create mode 100644 spm/DependencyCondition/Tests/ConditionalTests/ConditionalTests.swift diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index cb4106e..b5c8376 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -319,6 +319,8 @@ jobs: - name: BuildToolPlugin plugins: true - name: CommandPlugin + - name: Clang + - name: DependencyCondition - name: Macro - name: Trait - name: TargetSources @@ -361,6 +363,14 @@ jobs: working-directory: spm/${{ matrix.name }}/App run: PATH="$GITHUB_WORKSPACE:$PATH" bazel run //:plugins + # The workspace's own commands, which answer whatever the package is: + # a package with no trait and no configuration answers that. + - name: List Workspace + working-directory: spm/${{ matrix.name }}/App + run: | + PATH="$GITHUB_WORKSPACE:$PATH" bazel list config + PATH="$GITHUB_WORKSPACE:$PATH" bazel list trait + - name: Test Package working-directory: spm/${{ matrix.name }}/App run: bazel test //... diff --git a/Sources/Bazelize/Command.swift b/Sources/Bazelize/Command.swift index d6d8d19..4adb7e4 100644 --- a/Sources/Bazelize/Command.swift +++ b/Sources/Bazelize/Command.swift @@ -22,12 +22,56 @@ struct Command: AsyncParsableCommand { subcommands: [ GenerateCommand.self, PluginsCommand.self, + ListCommand.self, XcodeCommand.self, // RoadmapCommand.self, ], defaultSubcommand: GenerateCommand.self) } +// MARK: - ListCommand + +/// Answers a question about a generated workspace: `bazel run //list:config` +/// and `bazel run //list:trait` are the workspace's own way to ask them. +/// +/// Both answers are read from the workspace as it is now, not from something +/// written into it when it was generated: a `.bazelrc` gets edited, and a +/// manifest's traits change with the manifest. +struct ListCommand: AsyncParsableCommand { + enum Topic: String, ExpressibleByArgument, CaseIterable { + /// The `--config=` this workspace defines. + case config + /// The traits its packages declare, and which of them are on. + case trait + } + + static let configuration = CommandConfiguration( + commandName: "list", + abstract: "List what a generated workspace is built with.") + + @Argument(help: "config|trait") + var topic: Topic + + @Option(name: [.customLong("output", withSingleDash: false)], help: "PATH/TO/OUTPUT") + var output = "." + + @Option(name: [.customLong("local", withSingleDash: false)], help: "PATH/TO/LOCAL/PACKAGE") + var locals: [String] = [] + + func run() async throws { + let outputPath = Path.current + output + + switch topic { + case .config: + print(try Listing.config(output: outputPath)) + case .trait: + print(try await Listing.traits( + output: outputPath, + locals: locals.map { Path.current + $0 })) + } + } +} + // MARK: - PluginsCommand /// Runs the build tool plugins of a generated workspace, and nothing else. diff --git a/Sources/BazelizeKit/List/Listing.swift b/Sources/BazelizeKit/List/Listing.swift new file mode 100644 index 0000000..c7adfc7 --- /dev/null +++ b/Sources/BazelizeKit/List/Listing.swift @@ -0,0 +1,155 @@ +// +// Listing.swift +// +// +// What a generated workspace can be asked about itself. +// + +import Foundation +@preconcurrency import PathKit + +// MARK: - Listing + +/// The questions a generated workspace answers about itself. +/// +/// Both are read from the workspace as it is now rather than written into it +/// when it was generated: a `.bazelrc` is edited by hand, and a manifest's +/// traits change with the manifest. An answer that was true at generation time +/// and is false now is worse than no answer. +public enum Listing { + /// `bazel run //list:config`: the configurations this workspace defines, + /// and what every build gets whether it names one or not. + public static func config(output: Path) throws -> String { + let rc = try configurations(in: output) + + var lines: [String] = [] + if rc.configs.isEmpty { + lines.append("This workspace defines no --config.") + } else { + lines.append("Configurations of this workspace, as `--config=`:") + lines.append("") + for name in rc.configs.keys.sorted() { + lines.append(" \(name)") + for flag in rc.configs[name] ?? [] { + lines.append(" \(flag)") + } + } + } + + guard !rc.always.isEmpty else { return lines.joined(separator: "\n") } + + lines.append("") + lines.append("What every build gets, named or not:") + lines.append("") + for flag in rc.always { + lines.append(" \(flag)") + } + return lines.joined(separator: "\n") + } + + /// `bazel run //list:trait`: the traits of every package in the workspace, + /// and which of them this build has on. + public static func traits(output: Path, locals: [Path]) async throws -> String { + let workspace = try await SwiftPM.loadWorkspace(output: output, root: nil, locals: locals) + let enabled = SwiftPM.enabledTraits( + of: workspace.packages.map { (identity: $0.identity, manifest: $0.manifest) }, + directoryByIdentity: workspace.directoryByIdentity) + + let declaring = workspace.packages + .filter { !$0.manifest.traits.isEmpty } + .sorted { $0.directory < $1.directory } + + guard !declaring.isEmpty else { + return "No package in this workspace declares a trait." + } + + var lines = ["Traits of this workspace's packages:", ""] + for package in declaring { + let turnedOn = enabled[package.identity] ?? [] + lines.append(" \(package.directory)") + + for trait in package.manifest.traits.sorted(by: { $0.name < $1.name }) { + /// `default` is not a trait a target compiles with, it is the + /// list of traits a build that asks for nothing gets. + if trait.name == "default" { + let names = trait.enabledTraits.sorted().joined(separator: ", ") + lines.append(" default: \(names.isEmpty ? "none" : names)") + continue + } + + let enables = trait.enabledTraits.sorted().joined(separator: ", ") + lines.append( + " \(turnedOn.contains(trait.name) ? "on " : "off") \(trait.name)" + + (enables.isEmpty ? "" : " (enables \(enables))")) + } + } + + lines.append("") + lines.append(""" + A trait is on when the package makes it a default, or when something \ + that depends on the package asks for it by name. Change either in the \ + manifest, then generate the workspace again. + """) + return lines.joined(separator: "\n") + } + + /// What the workspace's `.bazelrc` says, and what the files it imports say: + /// the `build:` lines by name, and the ones that name no + /// configuration. + private static func configurations( + in output: Path) throws -> (configs: [String: [String]], always: [String]) + { + var configs: [String: [String]] = [:] + var always: [String] = [] + + for file in files(from: output + ".bazelrc", in: output) { + guard let contents: String = try? file.read() else { continue } + + for line in contents.split(separator: "\n") { + let statement = line.trimmingCharacters(in: .whitespaces) + guard !statement.hasPrefix("#") else { continue } + /// An import is how the file is put together, not something a + /// build is given. + guard !statement.hasPrefix("import "), !statement.hasPrefix("try-import ") else { + continue + } + + let parts = statement.split(separator: " ", maxSplits: 1) + guard + let command = parts.first, + let flags = parts.dropFirst().first?.trimmingCharacters(in: .whitespaces) + else { + continue + } + + let named = command.split(separator: ":", maxSplits: 1) + if named.count == 2 { + configs[String(named[1]), default: []].append("\(named[0]): \(flags)") + } else { + always.append("\(command): \(flags)") + } + } + } + + return (configs, always) + } + + /// A `.bazelrc` and the files it imports, which is where a generated + /// workspace keeps most of what it defines. + private static func files(from entry: Path, in output: Path) -> [Path] { + guard entry.isFile, let contents: String = try? entry.read() else { return [] } + + var files = [entry] + for line in contents.split(separator: "\n") { + let statement = line.trimmingCharacters(in: .whitespaces) + guard statement.hasPrefix("import ") || statement.hasPrefix("try-import ") else { continue } + + let path = statement + .split(separator: " ", maxSplits: 1)[1] + .trimmingCharacters(in: .whitespaces) + .replacingOccurrences(of: "%workspace%", with: output.string) + files += self.files(from: Path(path), in: output) + } + return files + } +} diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Clang.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Clang.swift index b571f1a..ace244c 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Clang.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Clang.swift @@ -98,6 +98,7 @@ extension SwiftPM.Generator { of: target, in: package, module: module, + compiled: compiled, resources: resources).nonEmpty, enable_modules: true, includes: includes( @@ -259,6 +260,7 @@ extension SwiftPM.Generator { of target: SwiftPM.PackageTarget, in package: SwiftPM.Package, module: String, + compiled: [String], resources: ResourceBundle?) -> [String] { var copts = ["-fmodule-name=\(module)"] + clangDefines(of: target) @@ -269,12 +271,7 @@ extension SwiftPM.Generator { copts.append("-include$(location \(header))") } - if let standard = package.manifest.cLanguageStandard { - copts.append("-std=\(standard)") - } - if let standard = package.manifest.cxxLanguageStandard { - copts.append("-std=\(standard)") - } + copts += standards(of: target, in: package, compiled: compiled) for setting in target.settings where setting.tool == "c" || setting.tool == "cxx" { guard setting.name == "unsafeFlags" else { continue } @@ -283,6 +280,47 @@ extension SwiftPM.Generator { return copts } + + /// `-std=`, for the language the target is actually written in. + /// + /// SwiftPM compiles each file with the standard of its own language; one + /// rule has one `copts`, and clang rejects a C standard for a C++ file as + /// firmly as the other way round. So the standard is the one that fits what + /// the target compiles, and a target that compiles both is named instead of + /// being given a flag that breaks half of it. + private func standards( + of target: SwiftPM.PackageTarget, + in package: SwiftPM.Package, + compiled: [String]) -> [String] + { + let extensions = Set(compiled) + let cxx = !extensions.isDisjoint(with: Self.cxxExtensions) + let c = !extensions.isDisjoint(with: Self.cExtensions) + + switch (c, cxx) { + case (true, false): + return package.manifest.cLanguageStandard.map { ["-std=\($0)"] } ?? [] + case (false, true): + return package.manifest.cxxLanguageStandard.map { ["-std=\($0)"] } ?? [] + case (true, true): + guard package.manifest.cLanguageStandard != nil || package.manifest.cxxLanguageStandard != nil else { + return [] + } + + let message = """ + \(package.directory)'s \(target.name) compiles C and C++ in one target, \ + and one rule takes one `-std`: the language standards the package \ + declares are left off. + """ + note(message) + return [] + case (false, false): + return [] + } + } + + private static let cExtensions: Set = ["c", "m"] + private static let cxxExtensions: Set = ["cc", "cpp", "cxx", "c++", "mm"] } extension SwiftPM.Generator { @@ -292,10 +330,12 @@ extension SwiftPM.Generator { /// Flags, not the `defines` attribute, for the same reason as a Swift target: /// the attribute would propagate into everything downstream. func clangDefines(of target: SwiftPM.PackageTarget) -> [String] { - let declared = target.settings.flatMap { setting -> [String] in - guard setting.name == "define" else { return [] } - guard setting.tool == "c" || setting.tool == "cxx" else { return [] } - return setting.values + let declared = target.settings.compactMap { setting -> String? in + guard setting.name == "define" else { return nil } + guard setting.tool == "c" || setting.tool == "cxx" else { return nil } + /// `.define("A", to: "1")` is dumped as two values, and is one flag: + /// `-DA=1`. + return setting.values.nonEmpty?.joined(separator: "=") } return (["SWIFT_PACKAGE"] + declared).map { "-D\($0)" } diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift index 49b023c..2383fdf 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift @@ -34,6 +34,13 @@ extension SwiftPM { /// the package asked for, and why. private(set) var notes: [String] = [] + /// Something a caller has to be told: it is logged where a run is + /// watched, and carried back for the report at the end of one. + func note(_ message: String) { + Log.codeGenerate.warning("\(message, privacy: .public)") + notes.append(message) + } + /// The plugins and tools Bazel already built, by target name. Empty /// while a workspace is being generated — nothing has been built yet — /// and filled by `//:plugins`, which has Bazel build them first. @@ -76,6 +83,7 @@ extension SwiftPM { } try writePluginRunner(locals: locals) + try writeListCommand(locals: locals) } /// A package that declares a platform version the project does not reach is diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Manifest.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Manifest.swift index 6cf3e42..059b612 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Manifest.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Manifest.swift @@ -57,6 +57,9 @@ extension SwiftPM { target.settings = target.settings.filter { setting in setting.applies(traits: traits, platforms: platforms) } + target.dependencies = target.dependencies.filter { dependency in + dependency.applies(traits: traits, platforms: platforms) + } return target } return resolved @@ -97,7 +100,7 @@ extension SwiftPM { let publicHeadersPath: String? var settings: [Setting] let resources: [Resource] - let dependencies: [TargetDependency] + var dependencies: [TargetDependency] /// The plugins the target asks to be run while it is built. let pluginUsages: [PluginUsage] /// A binary target's remote archive. @@ -156,25 +159,9 @@ extension SwiftPM { kind.values.first?.values ?? [] } - /// Whether the setting is one this build uses: a condition naming - /// traits needs one of them on, and a condition naming platforms needs - /// one of them built. - /// - /// A configuration is not one of these: which configuration a rule is - /// built in is decided when Bazel builds it, not when it is generated, - /// so a setting conditional on one is kept. + /// Whether the setting is one this build uses. func applies(traits: Set, platforms: Set) -> Bool { - guard let condition else { return true } - - if !condition.traits.isEmpty, condition.traits.allSatisfy({ !traits.contains($0) }) { - return false - } - if !condition.platformNames.isEmpty, !platforms.isEmpty, - condition.platformNames.allSatisfy({ !platforms.contains($0) }) - { - return false - } - return true + condition?.applies(traits: traits, platforms: platforms) ?? true } } @@ -241,33 +228,75 @@ extension SwiftPM { struct TargetDependency: Decodable { let kind: TargetDependencyKind + /// The last element of the array a dependency is dumped as: the + /// platforms it is limited to, and the traits that have to be on. + let condition: SettingCondition? init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: AnyKey.self) for key in container.allKeys { - let values = container.list(AnyDecodable.self, key.stringValue) - let strings = values.compactMap { $0.value as? String } + let values = container.list(DependencyElement.self, key.stringValue) + let strings = values.compactMap(\.name) guard let name = strings.first else { continue } + let kind: TargetDependencyKind switch key.stringValue { case "byName": kind = .byName(name) - return case "target": kind = .target(name) - return case "product": kind = .product(name: name, package: strings.dropFirst().first) - return default: continue } + + self.kind = kind + condition = values.compactMap(\.condition).last + return } throw DecodingError.dataCorrupted( .init(codingPath: decoder.codingPath, debugDescription: "Unknown target dependency")) } + + /// Whether the dependency is one this build links. + func applies(traits: Set, platforms: Set) -> Bool { + condition?.applies(traits: traits, platforms: platforms) ?? true + } + } + + /// One element of the array a target dependency is dumped as: a name, + /// `null`, the module aliases, or the condition. + struct DependencyElement: Decodable { + let name: String? + let condition: SettingCondition? + + init(from decoder: Decoder) throws { + if let single = try? decoder.singleValueContainer(), + let name = try? single.decode(String.self) + { + self.name = name + condition = nil + return + } + + name = nil + /// Module aliases are a dictionary too, so a dictionary is only a + /// condition when it names one of a condition's keys. + guard + let container = try? decoder.container(keyedBy: AnyKey.self), + container.allKeys.contains(where: { Self.conditionKeys.contains($0.stringValue) }) + else { + condition = nil + return + } + + condition = try? SettingCondition(from: decoder) + } + + private static let conditionKeys: Set = ["platformNames", "traits", "config"] } /// `{"fileSystem": [{...}]}` or `{"sourceControl": [{...}]}` @@ -358,6 +387,38 @@ extension SwiftPM { } } +extension SwiftPM.SettingCondition { + /// Whether what carries this condition is part of this build: a condition + /// naming traits needs one of them on, and a condition naming platforms + /// needs one of them built. + /// + /// `platforms` empty means the caller does not know which platforms the + /// project builds, which still rules out the platforms Bazelize never + /// builds for — Linux, Android, Windows and the rest are not what an Xcode + /// project or an Apple toolchain produces. + /// + /// A configuration is not one of these: which configuration a rule is + /// built in is decided when Bazel builds it, not when it is generated. + func applies(traits: Set, platforms: Set) -> Bool { + if !self.traits.isEmpty, self.traits.allSatisfy({ !traits.contains($0) }) { + return false + } + + let built = platforms.isEmpty ? Self.apple : platforms + if !platformNames.isEmpty, platformNames.allSatisfy({ !built.contains($0) }) { + return false + } + + return true + } + + /// The platforms an Apple toolchain builds, which is every platform that + /// can reach a generated rule. + private static let apple: Set = [ + "macos", "maccatalyst", "ios", "tvos", "watchos", "visionos", "driverkit", + ] +} + extension KeyedDecodingContainer where Key == SwiftPM.AnyKey { /// Absent, null and malformed all mean "not there": a manifest dump spans every /// tools version, and a key that does not apply is simply missing. diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginRule.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginRule.swift index b4d88ba..aba2951 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginRule.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginRule.swift @@ -98,6 +98,56 @@ extension SwiftPM.Generator { try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: script.string) } + /// `bazel list config` and `bazel list trait`: what a generated workspace + /// can be asked about itself. + /// + /// Bazel has no way to add a command, but its launcher does: Bazelisk runs + /// `tools/bazel` instead of Bazel itself and hands it the real binary in + /// `BAZEL_REAL`. So `list` is answered by the wrapper and everything else + /// goes straight through — no server starts to print a list. + /// + /// It is written for every workspace: a project with no packages still has + /// configurations, and packages that declare no trait is an answer too. + func writeListCommand(locals: [Path]) throws { + let directory = output + "tools" + try directory.mkpath() + + /// `tools` is a package of its own, so nothing globs the wrapper into + /// a rule of the workspace's root package. + try (directory + "BUILD").write("# The `bazel` launcher's wrapper lives here, and is not a build input.\n") + + let arguments = locals + .flatMap { local in ["--local", local.absolute().string.quoted] } + .joined(separator: " ") + + let script = directory + "bazel" + try script.write(""" + #!/bin/bash + # The `bazel` this workspace runs: `bazel list config` says what + # `--config=` it defines, `bazel list trait` says which traits its + # packages declare and which of them are on. Every other command is the + # one Bazel would have run. + set -euo pipefail + workspace="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + + if [[ "${1:-}" == "list" ]]; then + shift + exec bazelize list "$@" --output "$workspace" \(arguments) + fi + + if [[ -z "${BAZEL_REAL:-}" ]]; then + echo "tools/bazel ran without BAZEL_REAL: run Bazel through Bazelisk." >&2 + exit 1 + fi + + exec "$BAZEL_REAL" "$@" + + """) + + /// The launcher only runs a wrapper it can execute. + try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: script.string) + } + /// What a plugin needs built: the plugin itself, and the tools it runs. private var pluginBinaries: [PluginBinary] { var binaries: [PluginBinary] = [] diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Resources.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Resources.swift index c6ef147..c8a551e 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Resources.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Resources.swift @@ -349,9 +349,14 @@ extension SwiftPM.Generator { @end NSBundle *\(module)_SWIFTPM_MODULE_BUNDLE(void) { - NSArray *candidates = @[ + /// The same candidates the Swift accessor tries, in the same + /// order: on macOS a bundle's resources are under `Resources`, and + /// only a flat bundle has them beside the binary. + NSArray *candidates = @[ + [[NSBundle mainBundle] resourceURL] ?: [[NSBundle mainBundle] bundleURL], + [[NSBundle bundleForClass:[\(module)_BundleFinder class]] resourceURL] + ?: [[NSBundle bundleForClass:[\(module)_BundleFinder class]] bundleURL], [[NSBundle mainBundle] bundleURL], - [[NSBundle bundleForClass:[\(module)_BundleFinder class]] bundleURL], ]; for (NSURL *base in candidates) { diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Settings.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Settings.swift index 4fbe97f..dd0288f 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Settings.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Settings.swift @@ -35,8 +35,10 @@ extension SwiftPM.Generator { case "strictMemorySafety": return ["-strict-memory-safety"] case "interoperabilityMode": - guard let mode = setting.values.first else { return [] } - return ["-cxx-interoperability-mode=\(mode)"] + /// The manifest names the language; the compiler takes a mode. + /// `.C` is what it does anyway, and has no flag. + guard setting.values.first == "Cxx" else { return [] } + return ["-cxx-interoperability-mode=default"] case "unsafeFlags": return setting.values default: diff --git a/docs/SPM.md b/docs/SPM.md index 2991c24..484d58d 100644 --- a/docs/SPM.md +++ b/docs/SPM.md @@ -71,6 +71,8 @@ App/ ├── Package.resolved # kept: the only source of pins ├── config.bazelrc ├── BUILD +├── plugins.sh # what `bazel run //:plugins` runs +├── tools/bazel # what makes `bazel list` a command ├── Prebuilt/ ├── Targets// # unchanged └── Packages/ # ★ new @@ -83,6 +85,22 @@ App/ `Patches/` disappears entirely. +### What the workspace can be asked and told + +| command | what it does | +|---|---| +| `bazel run //:plugins` | builds this workspace's build tool plugins and their tools, runs them, and writes what they generate back into `Packages/*/Generated/` | +| `bazel list config` | the `--config=` this workspace defines, and the flags every build gets anyway | +| `bazel list trait` | the traits its packages declare, which are on, and why | + +`list` is not a Bazel command: `tools/bazel` is, which is the wrapper Bazelisk +runs instead of Bazel and hands the real binary in `BAZEL_REAL`. `list` is +answered there — no server starts to print a list — and every other command +goes straight through. Both answers are read from the workspace as it is now +rather than from something written into it at generation time, because a +`.bazelrc` gets edited and a manifest's traits change with the manifest. Both +answer for any workspace: no configuration and no trait are answers too. + ### How a package's sources get in Every package — remote or local — is a directory in this workspace holding a @@ -182,13 +200,14 @@ No test pins how a package's rules are produced either. | `.process` / `.copy` resources | `apple_resource_bundle` + `Generated/ResourceBundleAccessor.swift` | | `.embedInCode` resources | `Generated/EmbeddedResources.swift`: the bytes as `PackageResources`, and nothing in a bundle | | auto-discovered resources (xib/xcassets/metal/xcstrings/`.lproj`) | as above; a `.metal` file takes the target's headers into the resource group, because the bundler compiles them as Metal headers | -| `defines` | `-D` flags, not the `defines` attribute, which would propagate to every dependent | +| `defines` | `-D` flags, not the `defines` attribute, which would propagate to every dependent; `.define("A", to: "1")` is one flag, `-DA=1` | | `headerSearchPath` | `includes`, and the headers there stay inputs even when `exclude` drops the directory | | `linkedLibrary` / `linkedFramework` | `linkopts` | | `swiftLanguageMode` | `-swift-version` | | `enableUpcomingFeature` / `enableExperimentalFeature` | `-enable-upcoming-feature` / `-enable-experimental-feature` | | `defaultIsolation` | `-default-isolation ` | -| `interoperabilityMode` | `-cxx-interoperability-mode=` | +| `interoperabilityMode` | `-cxx-interoperability-mode=default` for `.Cxx`, nothing for `.C`, which is what the compiler does anyway | +| `cLanguageStandard` / `cxxLanguageStandard` | `-std=`, for the language the target is written in; a target that compiles both is named instead, because one rule takes one `-std` | | `strictMemorySafety` | `-strict-memory-safety` | | `unsafeFlags` | `copts` | | build tool plugin, own package | built by Bazel, run by bazelize (`bazel run //:plugins`); what it writes is globbed into the target that asked for it | @@ -196,7 +215,8 @@ No test pins how a package's rules are produced either. | command plugin | nothing: it runs when someone asks for it by name, never during a build | | macro target | `swift_compiler_plugin`, and `plugins` on whatever declares the macro | | traits (SE-0450) | resolved: a package gets its defaults unless a dependent names traits instead, and a setting conditional on a trait that is off is dropped | -| `.when(platforms:)` on a setting | dropped unless the project builds one of those platforms | +| `.when(platforms:)` on a setting or a dependency | dropped unless the project builds one of those platforms; a platform no Apple toolchain builds is always dropped | +| `.when(traits:)` on a dependency | dropped unless one of those traits is on | | `.when(configuration:)` on a setting | kept: which configuration a rule is built in is Bazel's answer, not the generator's | Two SwiftPM behaviours are matched on every generated `swift_library`: diff --git a/docs/SPM_ZH.md b/docs/SPM_ZH.md index 7568bf1..617b991 100644 --- a/docs/SPM_ZH.md +++ b/docs/SPM_ZH.md @@ -64,6 +64,8 @@ App/ ├── Package.resolved # 保留:pin 的唯一來源 ├── config.bazelrc ├── BUILD +├── plugins.sh # `bazel run //:plugins` 跑的就是它 +├── tools/bazel # 讓 `bazel list` 變成一個指令的東西 ├── Prebuilt/ ├── Targets// # 完全不變 └── Packages/ # ★ 新增 @@ -76,6 +78,20 @@ App/ `Patches/` 整組消失。 +### 這個 workspace 可以被問什麼 + +| 指令 | 做什麼 | +|---|---| +| `bazel run //:plugins` | 讓 Bazel 建這個 workspace 的 build tool plugin 與它們的工具、執行它們,把產生的檔案寫回 `Packages/*/Generated/` | +| `bazel list config` | 這個 workspace 定義了哪些 `--config=`,以及每次 build 一定會拿到的 flag | +| `bazel list trait` | 它的 package 宣告了哪些 trait、哪些是開的、為什麼 | + +`list` 不是 Bazel 的指令,`tools/bazel` 才是:Bazelisk 會執行這個 wrapper 而不是 +Bazel 本身,並把真正的執行檔放在 `BAZEL_REAL`。`list` 在那裡就回答完了——印一份清單 +不需要起一個 Bazel server——其他指令原封不動往下傳。兩個答案都是「現在」讀出來的, +不是產生當下寫死的:`.bazelrc` 會被人改,manifest 的 trait 也會跟著 manifest 變。 +兩個指令對任何 workspace 都答得出來:沒有 config、沒有 trait 也是答案。 + ### package 的原始碼怎麼進來 每個 package——遠端或本地——都是這個 workspace 裡的一個目錄,裡面放我們產生的 @@ -164,13 +180,14 @@ target 的 `deps` 需要改。測試也不釘 package 的規則是怎麼產生 | `.process` / `.copy` resources | `apple_resource_bundle` + `Generated/ResourceBundleAccessor.swift` | | `.embedInCode` resources | `Generated/EmbeddedResources.swift`:把 bytes 變成 `PackageResources`,bundle 裡什麼都不放 | | auto-discovered resources(xib/xcassets/metal/xcstrings/`.lproj`) | 同上;有 `.metal` 時該 target 的 header 也一起進 resource group,因為 bundler 會把它們當 Metal header 編 | -| `defines` | `-D` flag,不用 `defines` 屬性——那會往每個下游傳 | +| `defines` | `-D` flag,不用 `defines` 屬性——那會往每個下游傳;`.define("A", to: "1")` 是一個 flag:`-DA=1` | | `headerSearchPath` | `includes`,而且該目錄被 `exclude` 丟掉時 header 仍然留作輸入 | | `linkedLibrary` / `linkedFramework` | `linkopts` | | `swiftLanguageMode` | `-swift-version` | | `enableUpcomingFeature` / `enableExperimentalFeature` | `-enable-upcoming-feature` / `-enable-experimental-feature` | | `defaultIsolation` | `-default-isolation ` | -| `interoperabilityMode` | `-cxx-interoperability-mode=` | +| `interoperabilityMode` | `.Cxx` 給 `-cxx-interoperability-mode=default`;`.C` 什麼都不給,那本來就是編譯器的行為 | +| `cLanguageStandard` / `cxxLanguageStandard` | `-std=`,看 target 實際寫的是哪種語言;同時編 C 與 C++ 的 target 兩個都不給,並具名回報——一條規則只有一個 `-std` | | `strictMemorySafety` | `-strict-memory-safety` | | `unsafeFlags` | `copts` | | build tool plugin(自己的 package) | Bazel 建、bazelize 跑(`bazel run //:plugins`);它寫出來的東西由規則 glob 進「要求它的那個 target」 | @@ -178,7 +195,8 @@ target 的 `deps` 需要改。測試也不釘 package 的規則是怎麼產生 | command plugin | 不處理:它是有人指名才跑,build 永遠用不到 | | macro target | `swift_compiler_plugin`,並在宣告該 macro 的 target 上加 `plugins` | | traits(SE-0450) | 會解析:沒人指名就用 package 自己的預設 traits,有人指名就用指名的;條件在「沒開的 trait」上的 setting 直接丟掉 | -| setting 上的 `.when(platforms:)` | 專案沒有建那些平台就丟掉 | +| setting 或依賴上的 `.when(platforms:)` | 專案沒有建那些平台就丟掉;Apple toolchain 根本不建的平台一律丟掉 | +| 依賴上的 `.when(traits:)` | 那些 trait 沒開就丟掉 | | setting 上的 `.when(configuration:)` | 保留:規則是在哪個 configuration 建,是 Bazel 當下決定的,不是產生時 | 每個產生的 `swift_library` 都對齊兩個 SwiftPM 行為:`alwayslink`,因為 SwiftPM diff --git a/spm/Clang/Package.swift b/spm/Clang/Package.swift new file mode 100644 index 0000000..8932119 --- /dev/null +++ b/spm/Clang/Package.swift @@ -0,0 +1,43 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +/// The C-family side: where a target's public headers are, what it searches +/// for its own, the defines it compiles with, C++ interoperability, and the +/// resource bundle a C target reaches without importing anything. +let package = Package( + name: "Clang", + products: [ + .library(name: "Consumer", targets: ["Consumer"]), + ], + targets: [ + .target( + name: "CObject", + /// A C-family target with resources gets the same bundle a Swift + /// one does, reached through a header SwiftPM force-includes. + resources: [ + .process("Resources"), + ], + /// Not `include`: the headers are where the manifest says they are. + publicHeadersPath: "headers", + cSettings: [ + .headerSearchPath("internal"), + .define("C_FLAG"), + .define("C_VALUE", to: "7"), + ]), + .target(name: "CxxLib"), + .target( + name: "Consumer", + dependencies: ["CObject", "CxxLib"], + swiftSettings: [ + .interoperabilityMode(.Cxx), + ]), + .testTarget( + name: "ClangTests", + dependencies: ["Consumer"], + swiftSettings: [ + .interoperabilityMode(.Cxx), + ]), + ], + cLanguageStandard: .c11, + cxxLanguageStandard: .gnucxx17) diff --git a/spm/Clang/Sources/CObject/CObject.m b/spm/Clang/Sources/CObject/CObject.m new file mode 100644 index 0000000..342e247 --- /dev/null +++ b/spm/Clang/Sources/CObject/CObject.m @@ -0,0 +1,35 @@ +#import "CObject.h" + +#import "CInternal.h" + +@implementation CObject + ++ (int)value { +#ifdef C_VALUE + return C_VALUE; +#else + return 0; +#endif +} + ++ (BOOL)flag { +#ifdef C_FLAG + return YES; +#else + return NO; +#endif +} + ++ (int)internalValue { + return CInternalValue; +} + ++ (nullable NSString *)greeting { + NSURL *url = [SWIFTPM_MODULE_BUNDLE URLForResource:@"greeting" withExtension:@"txt"]; + if (url == nil) { return nil; } + + NSString *contents = [NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:nil]; + return [contents stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet]; +} + +@end diff --git a/spm/Clang/Sources/CObject/Resources/greeting.txt b/spm/Clang/Sources/CObject/Resources/greeting.txt new file mode 100644 index 0000000..947421a --- /dev/null +++ b/spm/Clang/Sources/CObject/Resources/greeting.txt @@ -0,0 +1 @@ +bundled diff --git a/spm/Clang/Sources/CObject/headers/CObject.h b/spm/Clang/Sources/CObject/headers/CObject.h new file mode 100644 index 0000000..79f097a --- /dev/null +++ b/spm/Clang/Sources/CObject/headers/CObject.h @@ -0,0 +1,16 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface CObject : NSObject +/// The value of a define the manifest gives a value to. ++ (int)value; +/// Whether a define the manifest states without one is defined. ++ (BOOL)flag; +/// What a header found through the target's own search path says. ++ (int)internalValue; +/// A resource, read out of the bundle the target was given. ++ (nullable NSString *)greeting; +@end + +NS_ASSUME_NONNULL_END diff --git a/spm/Clang/Sources/CObject/internal/CInternal.h b/spm/Clang/Sources/CObject/internal/CInternal.h new file mode 100644 index 0000000..101e9d5 --- /dev/null +++ b/spm/Clang/Sources/CObject/internal/CInternal.h @@ -0,0 +1,8 @@ +#ifndef C_INTERNAL_H +#define C_INTERNAL_H + +/// Only reachable because the manifest adds this directory as a header search +/// path; it is not one of the target's public headers. +static const int CInternalValue = 41; + +#endif diff --git a/spm/Clang/Sources/Consumer/Consumer.swift b/spm/Clang/Sources/Consumer/Consumer.swift new file mode 100644 index 0000000..1ca3366 --- /dev/null +++ b/spm/Clang/Sources/Consumer/Consumer.swift @@ -0,0 +1,24 @@ +import CObject +import CxxLib + +public enum Consumer { + public static var value: Int32 { + CObject.value() + } + + public static var flag: Bool { + CObject.flag() + } + + public static var internalValue: Int32 { + CObject.internalValue() + } + + public static var greeting: String? { + CObject.greeting() + } + + public static var twice: Int32 { + demo.twice(21) + } +} diff --git a/spm/Clang/Sources/CxxLib/CxxLib.cpp b/spm/Clang/Sources/CxxLib/CxxLib.cpp new file mode 100644 index 0000000..4dc5245 --- /dev/null +++ b/spm/Clang/Sources/CxxLib/CxxLib.cpp @@ -0,0 +1,7 @@ +#include "CxxLib.hpp" + +namespace demo { +int twice(int value) { + return value * 2; +} +} diff --git a/spm/Clang/Sources/CxxLib/include/CxxLib.hpp b/spm/Clang/Sources/CxxLib/include/CxxLib.hpp new file mode 100644 index 0000000..e01c7e7 --- /dev/null +++ b/spm/Clang/Sources/CxxLib/include/CxxLib.hpp @@ -0,0 +1,7 @@ +#pragma once + +namespace demo { +/// A C++ function, called from Swift only because the target that calls it is +/// compiled in C++ interoperability mode. +int twice(int value); +} diff --git a/spm/Clang/Tests/ClangTests/ClangTests.swift b/spm/Clang/Tests/ClangTests/ClangTests.swift new file mode 100644 index 0000000..e6f1ed0 --- /dev/null +++ b/spm/Clang/Tests/ClangTests/ClangTests.swift @@ -0,0 +1,23 @@ +import Consumer +import Testing + +@Test +func definesReachTheCTarget() { + #expect(Consumer.value == 7) + #expect(Consumer.flag) +} + +@Test +func aPrivateHeaderIsFoundThroughTheSearchPath() { + #expect(Consumer.internalValue == 41) +} + +@Test +func aCTargetReachesItsOwnBundle() { + #expect(Consumer.greeting == "bundled") +} + +@Test +func cxxInteroperabilityWorks() { + #expect(Consumer.twice == 42) +} diff --git a/spm/DependencyCondition/Extras/Package.swift b/spm/DependencyCondition/Extras/Package.swift new file mode 100644 index 0000000..61d9fb6 --- /dev/null +++ b/spm/DependencyCondition/Extras/Package.swift @@ -0,0 +1,14 @@ +// swift-tools-version: 6.1 + +import PackageDescription + +/// Depended on only when the trait that asks for it is on, and nothing turns +/// it on. +let package = Package( + name: "Extras", + products: [ + .library(name: "Extras", targets: ["Extras"]), + ], + targets: [ + .target(name: "Extras"), + ]) diff --git a/spm/DependencyCondition/Extras/Sources/Extras/Extras.swift b/spm/DependencyCondition/Extras/Sources/Extras/Extras.swift new file mode 100644 index 0000000..f2e8bc2 --- /dev/null +++ b/spm/DependencyCondition/Extras/Sources/Extras/Extras.swift @@ -0,0 +1,5 @@ +#error("This package is behind a trait nobody enabled, so building it is the bug this package is here to catch.") + +public enum Extras { + public static let value = 3 +} diff --git a/spm/DependencyCondition/LinuxOnly/Package.swift b/spm/DependencyCondition/LinuxOnly/Package.swift new file mode 100644 index 0000000..91e32f4 --- /dev/null +++ b/spm/DependencyCondition/LinuxOnly/Package.swift @@ -0,0 +1,13 @@ +// swift-tools-version: 6.1 + +import PackageDescription + +/// Depended on only when the platform being built is Linux. +let package = Package( + name: "LinuxOnly", + products: [ + .library(name: "LinuxOnly", targets: ["LinuxOnly"]), + ], + targets: [ + .target(name: "LinuxOnly"), + ]) diff --git a/spm/DependencyCondition/LinuxOnly/Sources/LinuxOnly/LinuxOnly.swift b/spm/DependencyCondition/LinuxOnly/Sources/LinuxOnly/LinuxOnly.swift new file mode 100644 index 0000000..ef8305a --- /dev/null +++ b/spm/DependencyCondition/LinuxOnly/Sources/LinuxOnly/LinuxOnly.swift @@ -0,0 +1,7 @@ +#if !os(Linux) +#error("This package is a Linux-only dependency, so building it here is the bug this package is here to catch.") +#endif + +public enum LinuxOnly { + public static let value = 2 +} diff --git a/spm/DependencyCondition/Package.swift b/spm/DependencyCondition/Package.swift new file mode 100644 index 0000000..6c5a7cb --- /dev/null +++ b/spm/DependencyCondition/Package.swift @@ -0,0 +1,35 @@ +// swift-tools-version: 6.1 + +import PackageDescription + +/// A dependency can be conditional: on the platform being built, or on a trait +/// being on. What the condition excludes must not be built at all. +/// +/// The conditional ones are packages of their own because a target of the +/// package being built is compiled whether anything depends on it or not — only +/// a dependency is left alone. +let package = Package( + name: "DependencyCondition", + products: [ + .library(name: "Conditional", targets: ["Conditional"]), + ], + traits: [ + .trait(name: "Extras"), + ], + dependencies: [ + .package(path: "LinuxOnly"), + .package(path: "Extras"), + ], + targets: [ + .target( + name: "Conditional", + dependencies: [ + "Always", + .product(name: "LinuxOnly", package: "LinuxOnly", condition: .when(platforms: [.linux])), + .product(name: "Extras", package: "Extras", condition: .when(traits: ["Extras"])), + ]), + .target(name: "Always"), + .testTarget( + name: "ConditionalTests", + dependencies: ["Conditional"]), + ]) diff --git a/spm/DependencyCondition/Sources/Always/Always.swift b/spm/DependencyCondition/Sources/Always/Always.swift new file mode 100644 index 0000000..4349d33 --- /dev/null +++ b/spm/DependencyCondition/Sources/Always/Always.swift @@ -0,0 +1,3 @@ +public enum Always { + public static let value = 1 +} diff --git a/spm/DependencyCondition/Sources/Conditional/Conditional.swift b/spm/DependencyCondition/Sources/Conditional/Conditional.swift new file mode 100644 index 0000000..7aa1524 --- /dev/null +++ b/spm/DependencyCondition/Sources/Conditional/Conditional.swift @@ -0,0 +1,5 @@ +import Always + +public enum Conditional { + public static let always = Always.value +} diff --git a/spm/DependencyCondition/Tests/ConditionalTests/ConditionalTests.swift b/spm/DependencyCondition/Tests/ConditionalTests/ConditionalTests.swift new file mode 100644 index 0000000..c20e703 --- /dev/null +++ b/spm/DependencyCondition/Tests/ConditionalTests/ConditionalTests.swift @@ -0,0 +1,7 @@ +import Conditional +import Testing + +@Test +func onlyTheUnconditionalDependencyIsLinked() { + #expect(Conditional.always == 1) +} diff --git a/spm/README.md b/spm/README.md index b3d3663..d114687 100644 --- a/spm/README.md +++ b/spm/README.md @@ -8,7 +8,9 @@ would not tell us anything. | package | what it holds bazelize to | |---|---| | `BuildToolPlugin` | a build tool plugin and the tool it runs: the test target only compiles through a source the plugin generates | +| `Clang` | a C-family target: public headers somewhere of its own, a private header search path, defines with and without a value, C++ interoperability, and the bundle a C target reaches without importing anything | | `CommandPlugin` | a plugin that is run on demand rather than while building, which nothing in a build may try to run | +| `DependencyCondition` | dependencies conditional on a platform and on a trait: what the condition excludes must not be built | | `Macro` | a macro target, loaded by the compiler while the target beside it is compiled | | `Trait` | the package's own traits, a default one, a dependency whose trait is turned on by name, and build settings conditional on each | | `TargetSources` | `sources:`, where a file beside the listed ones must not be compiled | @@ -28,11 +30,16 @@ bazelize --project . --output App cd App bazel run //:plugins # only the packages with a build tool plugin need this bazel test //... +bazel list config # what `--config=` the workspace defines +bazel list trait # which traits its packages declare, and which are on ``` `bazel run //:plugins` runs this workspace's build tool plugins and writes what they generate into `Packages//Generated/`. It is a separate step because a plugin is a program: Bazel builds it, and bazelize runs it as SwiftPM -would. `bazelize` has to be on `PATH` for that step. +would. + +`bazel list` is a command the generated `tools/bazel` adds, which Bazelisk runs +in Bazel's place. `bazelize` has to be on `PATH` for either. `App/` is generated, and is not checked in. From 0ecf648973fb9bf1b2d94196c49c7496d738c4c2 Mon Sep 17 00:00:00 2001 From: yume190 Date: Mon, 21 Sep 2026 14:58:20 +0800 Subject: [PATCH 02/47] Make a trait something the build decides, the way a configuration is MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A trait was resolved when the workspace was generated: what a trait turned off excluded was simply not written, and the only way to build the other way round was to edit a manifest and generate again. A configuration is not like that — `--config=Debug` is a thing a build says — and a trait is the same kind of thing. So every trait of every package is a `bool_flag` under `//Packages`, defaulting to what the manifest graph resolves it to, and `traits.bazelrc` carries a `--config` per trait: `--config=.` turns one on and `.-off` turns one off. A build that says nothing is the build SwiftPM would have made. What is conditional on a trait is then a `select` on that flag rather than a decision taken here — settings and dependencies alike, one `select` per condition, because two traits can be on at once and a `select` with two matching keys is an error. A condition naming several traits becomes a `config_setting_group`. Platforms stay resolved: a package rule is built through the transition of whatever pulls it in, so a platform is not something the rule can ask about. `bazel list trait` prints the `--config` beside each trait now, and `spm/DependencyCondition` is built both ways: with the trait off its package is not in the build at all, and with `--config=DependencyCondition.Extras` it is. CI runs both. `copts` and `linkopts` take a `Starlark.Value` for this, the way `deps` already did, because a `select` is not a list of strings. --- .github/workflows/swift.yml | 8 + Sources/BazelRules/Rules+Apple.swift | 2 +- Sources/BazelRules/Rules+Cc.swift | 4 +- Sources/BazelRules/Rules+Objc.swift | 4 +- Sources/BazelRules/Rules+Selects.swift | 40 +++++ Sources/BazelRules/Rules+Swift.swift | 34 ++-- Sources/BazelizeKit/Bazel/Bazel+BazelRC.swift | 24 +-- .../Codegen/Language/Codegen+Library.swift | 8 +- .../Language/Codegen+ObjcLibrary.swift | 6 +- .../Language/Codegen+SwiftLibrary.swift | 4 +- Sources/BazelizeKit/List/Listing.swift | 23 +-- .../BazelizeKit/SwiftPM/SwiftPM+Clang.swift | 59 +++--- .../SwiftPM/SwiftPM+Executable.swift | 8 +- .../SwiftPM/SwiftPM+Generator.swift | 42 ++++- .../BazelizeKit/SwiftPM/SwiftPM+Macro.swift | 6 +- .../SwiftPM/SwiftPM+Manifest.swift | 51 +++--- .../SwiftPM/SwiftPM+PluginRule.swift | 7 +- .../SwiftPM/SwiftPM+Settings.swift | 164 ++++++++++------- .../SwiftPM/SwiftPM+SystemLibrary.swift | 2 +- .../BazelizeKit/SwiftPM/SwiftPM+Test.swift | 8 +- .../BazelizeKit/SwiftPM/SwiftPM+Trait.swift | 169 ++++++++++++++++++ .../SwiftPM/SwiftPM+Workspace.swift | 18 +- .../Starlark/Value/Starlark+Value.swift | 8 + docs/SPM.md | 7 +- docs/SPM_ZH.md | 7 +- .../Extras/Sources/Extras/Extras.swift | 4 +- spm/DependencyCondition/Package.swift | 9 +- .../Sources/Conditional/Conditional.swift | 13 ++ .../ConditionalTests/ConditionalTests.swift | 13 +- spm/README.md | 1 + 30 files changed, 538 insertions(+), 215 deletions(-) create mode 100644 Sources/BazelRules/Rules+Selects.swift create mode 100644 Sources/BazelizeKit/SwiftPM/SwiftPM+Trait.swift diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index b5c8376..1ee94df 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -321,6 +321,9 @@ jobs: - name: CommandPlugin - name: Clang - name: DependencyCondition + # A trait is a flag, so the build that turns one on is a build to + # test as well: the dependency behind it is linked only then. + config: DependencyCondition.Extras - name: Macro - name: Trait - name: TargetSources @@ -374,3 +377,8 @@ jobs: - name: Test Package working-directory: spm/${{ matrix.name }}/App run: bazel test //... + + - name: Test Package With Trait + if: matrix.config + working-directory: spm/${{ matrix.name }}/App + run: bazel test //... --config=${{ matrix.config }} diff --git a/Sources/BazelRules/Rules+Apple.swift b/Sources/BazelRules/Rules+Apple.swift index f39c5df..e5619ea 100644 --- a/Sources/BazelRules/Rules+Apple.swift +++ b/Sources/BazelRules/Rules+Apple.swift @@ -1065,7 +1065,7 @@ extension Rules.Apple.General { deps: [Starlark.Label], avoid_deps: Starlark.Value? = nil, data: Starlark.Value? = nil, - linkopts: [String]? = nil, + linkopts: Starlark.Value? = nil, minimum_os_version: String? = nil, platform_type: String? = nil, sdk_dylibs: [String]? = nil, diff --git a/Sources/BazelRules/Rules+Cc.swift b/Sources/BazelRules/Rules+Cc.swift index 861ee47..9ce070e 100644 --- a/Sources/BazelRules/Rules+Cc.swift +++ b/Sources/BazelRules/Rules+Cc.swift @@ -48,9 +48,9 @@ extension Rules.Cc { srcs: Starlark.Value? = nil, hdrs: Starlark.Value? = nil, deps: Starlark.Value? = nil, - copts: [String]? = nil, + copts: Starlark.Value? = nil, includes: [String]? = nil, - linkopts: [String]? = nil, + linkopts: Starlark.Value? = nil, tags: [String]? = nil, textual_hdrs: Starlark.Value? = nil, visibility: Starlark.Statement.Argument.Visibility? = nil) diff --git a/Sources/BazelRules/Rules+Objc.swift b/Sources/BazelRules/Rules+Objc.swift index 80ec443..1297811 100644 --- a/Sources/BazelRules/Rules+Objc.swift +++ b/Sources/BazelRules/Rules+Objc.swift @@ -49,11 +49,11 @@ extension Rules.Objc { deps: Starlark.Value? = nil, data: Starlark.Value? = nil, alwayslink: Bool? = nil, - copts: [String]? = nil, + copts: Starlark.Value? = nil, defines: [String]? = nil, enable_modules: Bool? = nil, includes: [String]? = nil, - linkopts: [String]? = nil, + linkopts: Starlark.Value? = nil, module_map: Starlark.Label? = nil, module_name: String? = nil, non_arc_srcs: Starlark.Value? = nil, diff --git a/Sources/BazelRules/Rules+Selects.swift b/Sources/BazelRules/Rules+Selects.swift new file mode 100644 index 0000000..850b998 --- /dev/null +++ b/Sources/BazelRules/Rules+Selects.swift @@ -0,0 +1,40 @@ +// +// Rules+Selects.swift +// +// +// The skylib helper for a condition that several settings satisfy. +// + +import Foundation +import Starlark + +// MARK: - Rules.Selects + +/// https://github.com/bazelbuild/bazel-skylib/blob/main/docs/selects_doc.md +extension Rules { + public enum Selects: String, LoadableRule { + public var module: String { + "@bazel_skylib//lib:selects.bzl" + } + + /// The symbol the module exports; the rule below is a member of it. + case selects + } +} + +// MARK: - Rules.Selects.Call + +extension Rules.Selects { + public enum Call { + /// A `config_setting` that holds when any of the given ones does. + public static func config_setting_group( + name: String, + match_any: [String]) -> Starlark.Statement.Call + { + .init("selects.config_setting_group") { + "name" => name + "match_any" => match_any.map { Starlark.Label.named($0) } + } + } + } +} diff --git a/Sources/BazelRules/Rules+Swift.swift b/Sources/BazelRules/Rules+Swift.swift index 1740341..a925798 100644 --- a/Sources/BazelRules/Rules+Swift.swift +++ b/Sources/BazelRules/Rules+Swift.swift @@ -101,7 +101,7 @@ extension Rules.Swift { /// The Bazel target name. /// - `alwayslink: Bool` /// Forces the library to be linked even when the linker would otherwise discard it. Defaults to `true`. - /// - `copts: [String]?` + /// - `copts: Starlark.Value?` /// C or Clang compilation flags forwarded through the target graph. /// - `module_name: String?` /// Overrides the emitted Swift module name. @@ -117,7 +117,7 @@ extension Rules.Swift { /// Customizes the generated Objective-C compatibility header name. /// - `generates_header: Bool?` /// Enables emitting the generated Objective-C compatibility header. - /// - `linkopts: [String]?` + /// - `linkopts: Starlark.Value?` /// Linker flags passed through when linking downstream binaries or tests. /// - `linkstatic: Bool?` /// Prefers static rather than dynamic linkage when supported. @@ -133,7 +133,7 @@ extension Rules.Swift { name: String, alwayslink: Bool = true, always_include_developer_search_paths: Bool? = nil, - copts: [String]? = nil, + copts: Starlark.Value? = nil, module_name: String? = nil, package_name: String? = nil, plugins: Starlark.Value? = nil, @@ -143,7 +143,7 @@ extension Rules.Swift { defines: Starlark.Value? = nil, generated_header_name: String? = nil, generates_header: Bool? = nil, - linkopts: [String]? = nil, + linkopts: Starlark.Value? = nil, linkstatic: Bool? = nil, private_deps: Starlark.Value? = nil, swiftc_inputs: Starlark.Value? = nil, @@ -221,11 +221,11 @@ extension Rules.Swift { /// Parameters: /// - `name: String` /// The Bazel target name. - /// - `copts: [String]?` + /// - `copts: Starlark.Value?` /// C or Clang compilation flags forwarded through the target graph. /// - `deps: Starlark.Value?` /// Dependencies linked into the executable. - /// - `linkopts: [String]?` + /// - `linkopts: Starlark.Value?` /// Linker flags passed when linking the executable. /// - `module_name: String?` /// Overrides the emitted Swift module name. @@ -241,9 +241,9 @@ extension Rules.Swift { /// Repo-local convenience for emitting a `visibility` attribute. public static func swift_binary( name: String, - copts: [String]? = nil, + copts: Starlark.Value? = nil, deps: Starlark.Value? = nil, - linkopts: [String]? = nil, + linkopts: Starlark.Value? = nil, module_name: String? = nil, srcs: Starlark.Value? = nil, stamp: Int? = nil, @@ -300,7 +300,7 @@ extension Rules.Swift { /// The Bazel target name. /// - `args: [String]?` /// Command-line arguments passed when running the test. - /// - `copts: [String]?` + /// - `copts: Starlark.Value?` /// C or Clang compilation flags forwarded through the target graph. /// - `data: Starlark.Value?` /// Runtime data made available to the test. @@ -308,7 +308,7 @@ extension Rules.Swift { /// Dependencies linked into the test bundle. /// - `env: [String: String]?` /// Environment variables set when the test runs. - /// - `linkopts: [String]?` + /// - `linkopts: Starlark.Value?` /// Linker flags passed when linking the test bundle. /// - `module_name: String?` /// Overrides the emitted Swift module name. @@ -323,11 +323,11 @@ extension Rules.Swift { public static func swift_test( name: String, args: [String]? = nil, - copts: [String]? = nil, + copts: Starlark.Value? = nil, data: Starlark.Value? = nil, deps: Starlark.Value? = nil, env: [String: String]? = nil, - linkopts: [String]? = nil, + linkopts: Starlark.Value? = nil, module_name: String? = nil, srcs: Starlark.Value? = nil, stamp: Int? = nil, @@ -564,7 +564,7 @@ extension Rules.Swift { public static func swift_compiler_plugin( name: String, srcs: Starlark.Value? = nil, - copts: [String]? = nil, + copts: Starlark.Value? = nil, deps: Starlark.Value? = nil, module_name: String? = nil, tags: [String]? = nil, @@ -635,7 +635,7 @@ extension Rules.Swift { /// Public C-family headers published by this mixed-language target. /// - `includes: [String]?` /// Header search paths exported by the target. - /// - `linkopts: [String]?` + /// - `linkopts: Starlark.Value?` /// Linker options passed through to dependents. /// - `module_map: Starlark.Label?` /// Explicit Clang module map. @@ -676,7 +676,7 @@ extension Rules.Swift { additional_objc_compiler_inputs: Starlark.Value? = nil, always_include_developer_search_paths: Bool? = nil, alwayslink: Bool? = nil, - clang_copts: [String]? = nil, + clang_copts: Starlark.Value? = nil, clang_defines: Starlark.Value? = nil, clang_deps: Starlark.Value? = nil, clang_srcs: Starlark.Value? = nil, @@ -684,7 +684,7 @@ extension Rules.Swift { enable_modules: Bool? = nil, hdrs: Starlark.Value? = nil, includes: [String]? = nil, - linkopts: [String]? = nil, + linkopts: Starlark.Value? = nil, module_map: Starlark.Label? = nil, module_name: String? = nil, non_arc_srcs: Starlark.Value? = nil, @@ -692,7 +692,7 @@ extension Rules.Swift { private_deps: Starlark.Value? = nil, sdk_dylibs: [String]? = nil, sdk_frameworks: [String]? = nil, - swift_copts: [String]? = nil, + swift_copts: Starlark.Value? = nil, swift_defines: Starlark.Value? = nil, swift_plugins: Starlark.Value? = nil, swift_srcs: Starlark.Value? = nil, diff --git a/Sources/BazelizeKit/Bazel/Bazel+BazelRC.swift b/Sources/BazelizeKit/Bazel/Bazel+BazelRC.swift index 94c22c3..deab28a 100644 --- a/Sources/BazelizeKit/Bazel/Bazel+BazelRC.swift +++ b/Sources/BazelizeKit/Bazel/Bazel+BazelRC.swift @@ -83,7 +83,12 @@ extension Bazel { /// Bazel only reads `config.bazelrc` when the root `.bazelrc` imports it, so /// the generated flags are inert without this file. struct RootRC { - static let importLine = "import %workspace%/config.bazelrc" + /// What the root file has to import for the generated flags to be + /// read: the project's configurations, and the traits of its packages. + static let importLines = [ + "import %workspace%/config.bazelrc", + "import %workspace%/traits.bazelrc", + ] let path: Path @@ -91,18 +96,17 @@ extension Bazel { path = root + ".bazelrc" } - /// Creates `.bazelrc` when missing and otherwise appends the import once, - /// because the file may be hand-written and carry unrelated flags. + /// Creates `.bazelrc` when missing and otherwise appends each import + /// once, because the file may be hand-written and carry unrelated + /// flags. func ensureImport() throws { - guard let existing = try? String(contentsOfFile: path.string, encoding: .utf8) else { - try path.write(Self.importLine + "\n") - return - } - - guard !existing.components(separatedBy: .newlines).contains(Self.importLine) else { return } + let existing = (try? String(contentsOfFile: path.string, encoding: .utf8)) ?? "" + let lines = existing.components(separatedBy: .newlines) + let missing = Self.importLines.filter { !lines.contains($0) } + guard !missing.isEmpty else { return } let separator = existing.hasSuffix("\n") || existing.isEmpty ? "" : "\n" - try path.write(existing + separator + Self.importLine + "\n") + try path.write(existing + separator + missing.joined(separator: "\n") + "\n") } } } diff --git a/Sources/BazelizeKit/Codegen/Language/Codegen+Library.swift b/Sources/BazelizeKit/Codegen/Language/Codegen+Library.swift index 75114d0..799e056 100644 --- a/Sources/BazelizeKit/Codegen/Language/Codegen+Library.swift +++ b/Sources/BazelizeKit/Codegen/Language/Codegen+Library.swift @@ -66,12 +66,12 @@ extension Target { builder.call( Rules.Swift.Call.mixed_language_library( name: "\(name)_mixed", - clang_copts: [ + clang_copts: ([ "-fblocks", "-fobjc-arc", "-fPIC", "-fmodule-name=\(codegenModuleName)", - ] + clangDialectCopts + forceIncludeFlags, + ] + clangDialectCopts + forceIncludeFlags).starlark, clang_srcs: .build { srcs_c srcs_cpp @@ -96,11 +96,11 @@ extension Target { bridgingHeader }, includes: headerIncludes(project: project), - linkopts: sdkLinkopts, + linkopts: sdkLinkopts?.starlark, module_name: codegenModuleName, sdk_dylibs: dylibsSDK, sdk_frameworks: sdkFrameworks(project: project), - swift_copts: moduleSwiftCopts(project: project), + swift_copts: moduleSwiftCopts(project: project)?.starlark, swift_defines: defines(project: project), swift_srcs: .build { srcs_swift diff --git a/Sources/BazelizeKit/Codegen/Language/Codegen+ObjcLibrary.swift b/Sources/BazelizeKit/Codegen/Language/Codegen+ObjcLibrary.swift index 5885c5e..80699ef 100644 --- a/Sources/BazelizeKit/Codegen/Language/Codegen+ObjcLibrary.swift +++ b/Sources/BazelizeKit/Codegen/Language/Codegen+ObjcLibrary.swift @@ -40,15 +40,15 @@ extension Target { } copiedResourceGroups(project: project) }, - copts: [ + copts: ([ "-fblocks", "-fobjc-arc", "-fPIC", "-fmodule-name=\(codegenModuleName)", - ] + forceIncludeFlags, + ] + forceIncludeFlags).starlark, enable_modules: prefer(\.enableModules), includes: headerIncludes(project: project), - linkopts: sdkLinkopts, + linkopts: sdkLinkopts?.starlark, module_name: codegenModuleName, sdk_dylibs: dylibsSDK, sdk_frameworks: sdkFrameworks(project: project), diff --git a/Sources/BazelizeKit/Codegen/Language/Codegen+SwiftLibrary.swift b/Sources/BazelizeKit/Codegen/Language/Codegen+SwiftLibrary.swift index f7c4687..bce01c2 100644 --- a/Sources/BazelizeKit/Codegen/Language/Codegen+SwiftLibrary.swift +++ b/Sources/BazelizeKit/Codegen/Language/Codegen+SwiftLibrary.swift @@ -24,7 +24,7 @@ extension Target { builder.call( Rules.Swift.Call.swift_library( name: "\(name)_swift", - copts: swiftCopts(project: project), + copts: swiftCopts(project: project)?.starlark, module_name: codegenModuleName, srcs: .build { srcs_swift @@ -45,7 +45,7 @@ extension Target { copiedResourceGroups(project: project) }, defines: defines(project: project), - linkopts: sdkLinkopts, + linkopts: sdkLinkopts?.starlark, swiftc_inputs: .build { bridgingHeader definesHeader diff --git a/Sources/BazelizeKit/List/Listing.swift b/Sources/BazelizeKit/List/Listing.swift index c7adfc7..12a68d5 100644 --- a/Sources/BazelizeKit/List/Listing.swift +++ b/Sources/BazelizeKit/List/Listing.swift @@ -47,13 +47,10 @@ public enum Listing { return lines.joined(separator: "\n") } - /// `bazel run //list:trait`: the traits of every package in the workspace, - /// and which of them this build has on. + /// `bazel list trait`: the traits of every package in the workspace, which + /// of them a build gets by default, and what to say to change that. public static func traits(output: Path, locals: [Path]) async throws -> String { let workspace = try await SwiftPM.loadWorkspace(output: output, root: nil, locals: locals) - let enabled = SwiftPM.enabledTraits( - of: workspace.packages.map { (identity: $0.identity, manifest: $0.manifest) }, - directoryByIdentity: workspace.directoryByIdentity) let declaring = workspace.packages .filter { !$0.manifest.traits.isEmpty } @@ -65,7 +62,7 @@ public enum Listing { var lines = ["Traits of this workspace's packages:", ""] for package in declaring { - let turnedOn = enabled[package.identity] ?? [] + let turnedOn = workspace.traits[package.identity] ?? [] lines.append(" \(package.directory)") for trait in package.manifest.traits.sorted(by: { $0.name < $1.name }) { @@ -77,18 +74,22 @@ public enum Listing { continue } + let on = turnedOn.contains(trait.name) let enables = trait.enabledTraits.sorted().joined(separator: ", ") lines.append( - " \(turnedOn.contains(trait.name) ? "on " : "off") \(trait.name)" - + (enables.isEmpty ? "" : " (enables \(enables))")) + " \(on ? "on " : "off") \(trait.name)" + + (enables.isEmpty ? "" : " (enables \(enables))") + + " --config=\(package.directory).\(trait.name)\(on ? "-off" : "")") } } lines.append("") lines.append(""" - A trait is on when the package makes it a default, or when something \ - that depends on the package asks for it by name. Change either in the \ - manifest, then generate the workspace again. + A trait is on by default when its package makes it one, or when \ + something that depends on that package asks for it by name. Every \ + trait is a flag, so a build switches one with the `--config` beside \ + it — `--config=.` to turn it on, `-off` to turn it \ + off — and `bazel list config` lists them with everything else. """) return lines.joined(separator: "\n") } diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Clang.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Clang.swift index ace244c..41dffe2 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Clang.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Clang.swift @@ -87,9 +87,7 @@ extension SwiftPM.Generator { }? .nonEmpty .map { Starlark.glob($0) }, - deps: deps(of: target, in: package).nonEmpty.map { labels in - .build { labels } - }, + deps: deps(of: target, in: package), data: resources?.label.map { label in .build { [Starlark.Label.named(label)] } }, @@ -99,13 +97,13 @@ extension SwiftPM.Generator { in: package, module: module, compiled: compiled, - resources: resources).nonEmpty, + resources: resources), enable_modules: true, includes: includes( of: target, prefix: prefix, interface: interface).nonEmpty, - linkopts: linkopts(of: target).nonEmpty, + linkopts: linkopts(of: target, in: package), /// The module a dependent's `@import` names: the package target's /// own name, not the one Bazel derives from the label. module_name: module, @@ -261,24 +259,38 @@ extension SwiftPM.Generator { in package: SwiftPM.Package, module: String, compiled: [String], - resources: ResourceBundle?) -> [String] + resources: ResourceBundle?) -> Starlark.Value? { - var copts = ["-fmodule-name=\(module)"] + clangDefines(of: target) + var always = ["-fmodule-name=\(module)", "-DSWIFT_PACKAGE"] /// SwiftPM force-includes the accessor, so a source reaches its bundle /// without importing anything. if let header = resources?.header { - copts.append("-include$(location \(header))") + always.append("-include$(location \(header))") } - copts += standards(of: target, in: package, compiled: compiled) + always += standards(of: target, in: package, compiled: compiled) - for setting in target.settings where setting.tool == "c" || setting.tool == "cxx" { - guard setting.name == "unsafeFlags" else { continue } - copts.append(contentsOf: setting.values) - } + return grouped(target.settings, in: package, always: always, flags: Self.clangFlags) + } - return copts + /// The flags of one C-family setting. + /// + /// Flags, not the `defines` attribute, for the same reason as a Swift + /// target: the attribute would propagate into everything downstream. + private static func clangFlags(_ setting: SwiftPM.Setting) -> [String] { + guard setting.tool == "c" || setting.tool == "cxx" else { return [] } + + switch setting.name { + case "define": + /// `.define("A", to: "1")` is dumped as two values, and is one + /// flag: `-DA=1`. + return setting.values.nonEmpty.map { ["-D\($0.joined(separator: "="))"] } ?? [] + case "unsafeFlags": + return setting.values + default: + return [] + } } /// `-std=`, for the language the target is actually written in. @@ -322,22 +334,3 @@ extension SwiftPM.Generator { private static let cExtensions: Set = ["c", "m"] private static let cxxExtensions: Set = ["cc", "cpp", "cxx", "c++", "mm"] } - -extension SwiftPM.Generator { - /// `c.define` and `cxx.define`, plus the `SWIFT_PACKAGE` every package target - /// compiles with. - /// - /// Flags, not the `defines` attribute, for the same reason as a Swift target: - /// the attribute would propagate into everything downstream. - func clangDefines(of target: SwiftPM.PackageTarget) -> [String] { - let declared = target.settings.compactMap { setting -> String? in - guard setting.name == "define" else { return nil } - guard setting.tool == "c" || setting.tool == "cxx" else { return nil } - /// `.define("A", to: "1")` is dumped as two values, and is one flag: - /// `-DA=1`. - return setting.values.nonEmpty?.joined(separator: "=") - } - - return (["SWIFT_PACKAGE"] + declared).map { "-D\($0)" } - } -} diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Executable.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Executable.swift index 690ab80..0ac28e8 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Executable.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Executable.swift @@ -27,11 +27,9 @@ extension SwiftPM.Generator { builder.call( Rules.Swift.Call.swift_binary( name: ruleName(of: target.name, in: package), - copts: copts(of: target).nonEmpty, - deps: deps(of: target, in: package).nonEmpty.map { labels in - .build { labels } - }, - linkopts: linkopts(of: target).nonEmpty, + copts: copts(of: target, in: package), + deps: deps(of: target, in: package), + linkopts: linkopts(of: target, in: package), module_name: Self.moduleName(target.name), srcs: Starlark.glob( matching( diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift index 2383fdf..57a3e49 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift @@ -30,6 +30,10 @@ extension SwiftPM { private var kinds: [String: [String: TargetKind]] = [:] + /// The `config_setting_group` a condition on several traits asked for, + /// by name, written with the flags once every rule is generated. + var traitGroups: [String: [String]] = [:] + /// What a caller tells the user about: where the build differs from what /// the package asked for, and why. private(set) var notes: [String] = [] @@ -84,6 +88,10 @@ extension SwiftPM { try writePluginRunner(locals: locals) try writeListCommand(locals: locals) + /// Written whether or not there is a trait to switch: the root + /// `.bazelrc` imports it, and an import of a file that is not + /// there is a workspace that does not load. + try writeTraitConfigs() } /// A package that declares a platform version the project does not reach is @@ -738,7 +746,7 @@ extension SwiftPM { /// search paths, which is how a test-support library finds /// XCTest. always_include_developer_search_paths: true, - copts: copts(of: target).nonEmpty, + copts: copts(of: target, in: package), module_name: Self.moduleName(target.name), /// Which targets `package` visibility reaches: every target of /// the same package, which is what the name identifies. @@ -757,13 +765,11 @@ extension SwiftPM { /// written into it: `bazel run //:plugins` does that, and /// a package that cannot load cannot run it. allowEmpty: true), - deps: deps(of: target, in: package).nonEmpty.map { labels in - .build { labels } - }, + deps: deps(of: target, in: package), data: resources?.label.map { label in .build { [Starlark.Label.named(label)] } }, - linkopts: linkopts(of: target).nonEmpty, + linkopts: linkopts(of: target, in: package), tags: Self.manual, visibility: .public)) } @@ -822,13 +828,15 @@ extension SwiftPM { } } - func deps(of target: PackageTarget, in package: Package) -> [Starlark.Label] { + /// What the target links, with whatever a trait decides in a `select` + /// on that trait's flag. + func deps(of target: PackageTarget, in package: Package) -> Starlark.Value? { let localTargets = Set(package.manifest.targets.map(\.name)) let localProducts = Dictionary( package.manifest.products.map { ($0.name, $0) }, uniquingKeysWith: { first, _ in first }) - let labels: [String] = target.dependencies.compactMap { dependency in + func dependencyLabel(_ dependency: SwiftPM.TargetDependency) -> String? { switch dependency.kind { case .target(let name): guard localTargets.contains(name), !isMacro(name, in: package) else { return nil } @@ -845,7 +853,25 @@ extension SwiftPM { } } - return Array(Set(labels)).sorted().map(Starlark.Label.named) + var always: Set = [] + var conditions: [String] = [] + var byCondition: [String: Set] = [:] + + for dependency in target.dependencies { + guard let label = dependencyLabel(dependency) else { continue } + + guard let condition = traitCondition(dependency.traits, in: package) else { + always.insert(label) + continue + } + + if byCondition[condition] == nil { conditions.append(condition) } + byCondition[condition, default: []].insert(label) + } + + return traitValue( + always.sorted(), + conditional: conditions.map { ($0, (byCondition[$0] ?? []).sorted()) }) } private func isMacro(_ target: String, in package: Package) -> Bool { diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Macro.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Macro.swift index 992a890..1d3478e 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Macro.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Macro.swift @@ -36,10 +36,8 @@ extension SwiftPM.Generator { + generated, exclude: excluded(target, prefix: prefix), allowEmpty: true), - copts: copts(of: target).nonEmpty, - deps: deps(of: target, in: package).nonEmpty.map { labels in - .build { labels } - }, + copts: copts(of: target, in: package), + deps: deps(of: target, in: package), module_name: Self.moduleName(target.name), tags: Self.manual, visibility: .public)) diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Manifest.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Manifest.swift index 059b612..ac18f4a 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Manifest.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Manifest.swift @@ -45,20 +45,25 @@ extension SwiftPM { toolsVersion = container.value([String: String].self, "toolsVersion")?["_version"] ?? "5.9.0" } - /// The manifest with every setting that does not apply removed, so - /// nothing downstream has to know a condition exists. + /// The manifest with everything the platform rules out removed. + /// + /// A trait's condition survives: which traits are on is a question the + /// build answers, through a flag per trait, so what is conditional on + /// one becomes a `select` rather than a decision taken here. A platform + /// cannot be that — a package rule is built through the transition of + /// whatever pulls it in — so it is decided now. /// /// `platforms` empty means the caller does not know which platforms are - /// built, and a platform condition is then left alone. - func resolving(traits: Set, platforms: Set) -> Manifest { + /// built, which still rules out the ones no Apple toolchain builds. + func resolving(platforms: Set) -> Manifest { var resolved = self resolved.targets = targets.map { target in var target = target target.settings = target.settings.filter { setting in - setting.applies(traits: traits, platforms: platforms) + setting.condition?.applies(platforms: platforms) ?? true } target.dependencies = target.dependencies.filter { dependency in - dependency.applies(traits: traits, platforms: platforms) + dependency.condition?.applies(platforms: platforms) ?? true } return target } @@ -159,9 +164,10 @@ extension SwiftPM { kind.values.first?.values ?? [] } - /// Whether the setting is one this build uses. - func applies(traits: Set, platforms: Set) -> Bool { - condition?.applies(traits: traits, platforms: platforms) ?? true + /// The traits the setting is conditional on, of which one being on is + /// what puts it in the build. Empty means it is always in. + var traits: [String] { + condition?.traits ?? [] } } @@ -261,9 +267,10 @@ extension SwiftPM { .init(codingPath: decoder.codingPath, debugDescription: "Unknown target dependency")) } - /// Whether the dependency is one this build links. - func applies(traits: Set, platforms: Set) -> Bool { - condition?.applies(traits: traits, platforms: platforms) ?? true + /// The traits the dependency is conditional on, of which one being on + /// is what links it. Empty means it is always linked. + var traits: [String] { + condition?.traits ?? [] } } @@ -388,28 +395,20 @@ extension SwiftPM { } extension SwiftPM.SettingCondition { - /// Whether what carries this condition is part of this build: a condition - /// naming traits needs one of them on, and a condition naming platforms - /// needs one of them built. + /// Whether the platform this condition names is one the project builds. /// /// `platforms` empty means the caller does not know which platforms the /// project builds, which still rules out the platforms Bazelize never /// builds for — Linux, Android, Windows and the rest are not what an Xcode /// project or an Apple toolchain produces. /// - /// A configuration is not one of these: which configuration a rule is - /// built in is decided when Bazel builds it, not when it is generated. - func applies(traits: Set, platforms: Set) -> Bool { - if !self.traits.isEmpty, self.traits.allSatisfy({ !traits.contains($0) }) { - return false - } + /// Traits are not decided here, and neither is a configuration: both are + /// answered when Bazel builds, not when the rules are written. + func applies(platforms: Set) -> Bool { + guard !platformNames.isEmpty else { return true } let built = platforms.isEmpty ? Self.apple : platforms - if !platformNames.isEmpty, platformNames.allSatisfy({ !built.contains($0) }) { - return false - } - - return true + return !platformNames.allSatisfy { !built.contains($0) } } /// The platforms an Apple toolchain builds, which is every platform that diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginRule.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginRule.swift index aba2951..f40ea93 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginRule.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginRule.swift @@ -36,12 +36,12 @@ extension SwiftPM.Generator { /// Which `PackagePlugin` the plugin was written against; its /// availability is stated in terms of the tools version. "-package-description-version", package.manifest.toolsVersion, - ], + ].starlark, linkopts: [ "-L", api, "-lPackagePlugin", "-Xlinker", "-rpath", "-Xlinker", api, - ], + ].starlark, module_name: Self.moduleName(target.name), srcs: Starlark.glob(["\(prefix)/**/*.swift"]), tags: Self.manual, @@ -71,6 +71,9 @@ extension SwiftPM.Generator { name: "plugins", srcs: .build { binaries.map(\.label).sorted().map { Starlark.Label.named($0) } }, visibility: .public)) + /// The flags every trait is switched with, in the package the + /// generator owns. + buildTraitRules(group) try (packagesRoot + "BUILD").write(group.build()) let arguments = ["--output", "."] diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Settings.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Settings.swift index dd0288f..9fb72f5 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Settings.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Settings.swift @@ -6,6 +6,7 @@ // import Foundation +import Starlark extension SwiftPM.Generator { /// What the target compiles with, beyond the defaults. @@ -13,79 +14,120 @@ extension SwiftPM.Generator { /// SwiftPM hands these to `swiftc` directly, so they are copts rather than /// anything the rules model: a `defines` attribute would re-tokenize a value /// and a feature is not a flag the rules know. - func copts(of target: SwiftPM.PackageTarget) -> [String] { - swiftDefines(of: target) + target.settings.flatMap { setting -> [String] in - guard setting.tool == "swift", let name = setting.name else { return [] } + /// + /// A setting conditional on a trait is a `select` on that trait's flag, so + /// the build decides it rather than this run. + func copts(of target: SwiftPM.PackageTarget, in package: SwiftPM.Package) -> Starlark.Value? { + grouped( + target.settings, + in: package, + /// `SWIFT_PACKAGE` is what a package's own sources test for; SwiftPM + /// defines it for every target it builds. + /// + /// A flag, not the `defines` attribute: that attribute propagates to + /// everything that depends on the library, and a project's own target + /// must not compile as if it were a package — Xcode's generated asset + /// symbols, for one, switch on `SWIFT_PACKAGE`. + always: Self.define("SWIFT_PACKAGE"), + flags: Self.swiftFlags) + } + + /// A package can name a system library or framework it needs; nothing else in + /// the graph knows about it. + func linkopts(of target: SwiftPM.PackageTarget, in package: SwiftPM.Package) -> Starlark.Value? { + grouped(target.settings, in: package, always: [], flags: Self.linkerFlags) + } - switch name { - case "swiftLanguageMode", "swiftLanguageVersion": - guard let version = setting.values.first else { return [] } - return ["-swift-version", version] - case "defaultIsolation": - guard let isolation = setting.values.first else { return [] } - return ["-default-isolation", isolation] - case "enableUpcomingFeature": - return setting.values.flatMap { feature in - ["-enable-upcoming-feature", feature] - } - case "enableExperimentalFeature": - return setting.values.flatMap { feature in - ["-enable-experimental-feature", feature] - } - case "strictMemorySafety": - return ["-strict-memory-safety"] - case "interoperabilityMode": - /// The manifest names the language; the compiler takes a mode. - /// `.C` is what it does anyway, and has no flag. - guard setting.values.first == "Cxx" else { return [] } - return ["-cxx-interoperability-mode=default"] - case "unsafeFlags": - return setting.values - default: - return [] + /// The flags of one Swift setting. + private static func swiftFlags(_ setting: SwiftPM.Setting) -> [String] { + guard setting.tool == "swift", let name = setting.name else { return [] } + + switch name { + case "define": + return setting.values.flatMap { define(String($0)) } + case "swiftLanguageMode", "swiftLanguageVersion": + guard let version = setting.values.first else { return [] } + return ["-swift-version", version] + case "defaultIsolation": + guard let isolation = setting.values.first else { return [] } + return ["-default-isolation", isolation] + case "enableUpcomingFeature": + return setting.values.flatMap { feature in + ["-enable-upcoming-feature", feature] + } + case "enableExperimentalFeature": + return setting.values.flatMap { feature in + ["-enable-experimental-feature", feature] } + case "strictMemorySafety": + return ["-strict-memory-safety"] + case "interoperabilityMode": + /// The manifest names the language; the compiler takes a mode. + /// `.C` is what it does anyway, and has no flag. + guard setting.values.first == "Cxx" else { return [] } + return ["-cxx-interoperability-mode=default"] + case "unsafeFlags": + return setting.values + default: + return [] } } - /// `SWIFT_PACKAGE` is what a package's own sources test for; SwiftPM defines it - /// for every target it builds. - /// - /// These are flags, not the `defines` attribute: that attribute propagates to - /// everything that depends on the library, and a project's own target must not - /// compile as if it were a package — Xcode's generated asset symbols, for one, - /// switch on `SWIFT_PACKAGE`. - func swiftDefines(of target: SwiftPM.PackageTarget) -> [String] { - let declared = target.settings.compactMap { setting -> [String]? in - guard setting.tool == "swift", setting.name == "define" else { return nil } - return setting.values - }.flatMap { $0 } + private static func linkerFlags(_ setting: SwiftPM.Setting) -> [String] { + guard setting.tool == "linker", let name = setting.name else { return [] } - return (["SWIFT_PACKAGE"] + declared).flatMap { define in - ["-D\(define)", "-Xcc", "-D\(define)"] + switch name { + case "linkedLibrary": + return setting.values.map { library in + "-l\(library)" + } + case "linkedFramework": + return setting.values.flatMap { framework in + ["-framework", framework] + } + case "unsafeFlags": + return setting.values + default: + return [] } } - /// A package can name a system library or framework it needs; nothing else in - /// the graph knows about it. - func linkopts(of target: SwiftPM.PackageTarget) -> [String] { - target.settings.flatMap { setting -> [String] in - guard setting.tool == "linker", let name = setting.name else { return [] } + /// A Swift define reaches clang too, the way SwiftPM passes it. + private static func define(_ name: String) -> [String] { + ["-D\(name)", "-Xcc", "-D\(name)"] + } - switch name { - case "linkedLibrary": - return setting.values.map { library in - "-l\(library)" - } - case "linkedFramework": - return setting.values.flatMap { framework in - ["-framework", framework] - } - case "unsafeFlags": - return setting.values - default: - return [] + /// Settings as one list plus one `select` per trait condition. + /// + /// One `select` per condition rather than one with every key: two traits can + /// be on at once, and a `select` whose keys both match is an error rather + /// than both lists. + func grouped( + _ settings: [SwiftPM.Setting], + in package: SwiftPM.Package, + always base: [String], + flags: (SwiftPM.Setting) -> [String]) -> Starlark.Value? + { + var always = base + var conditions: [String] = [] + var byCondition: [String: [String]] = [:] + + for setting in settings { + let values = flags(setting) + guard !values.isEmpty else { continue } + + guard let condition = traitCondition(setting.traits, in: package) else { + always += values + continue } + + if byCondition[condition] == nil { conditions.append(condition) } + byCondition[condition, default: []] += values } + + return traitValue( + always, + conditional: conditions.map { ($0, byCondition[$0] ?? []) }) } } diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+SystemLibrary.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+SystemLibrary.swift index 62e148f..76c5650 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+SystemLibrary.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+SystemLibrary.swift @@ -53,7 +53,7 @@ extension SwiftPM.Generator { .nonEmpty .map { Starlark.glob($0) }, includes: [prefix], - linkopts: Self.linkopts(moduleMap: root + moduleMap).nonEmpty, + linkopts: Self.linkopts(moduleMap: root + moduleMap).starlark, tags: Self.manual, visibility: .public)) diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Test.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Test.swift index f08ee20..fe44a43 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Test.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Test.swift @@ -39,7 +39,7 @@ extension SwiftPM.Generator { Rules.Swift.Call.swift_library( name: library, always_include_developer_search_paths: true, - copts: copts(of: target).nonEmpty, + copts: copts(of: target, in: package), module_name: Self.moduleName(target.name), package_name: package.manifest.name, srcs: Starlark.glob( @@ -50,13 +50,11 @@ extension SwiftPM.Generator { + (resources?.accessors ?? []), exclude: excluded(target, prefix: prefix), allowEmpty: true), - deps: deps(of: target, in: package).nonEmpty.map { labels in - .build { labels } - }, + deps: deps(of: target, in: package), data: resources?.label.map { label in .build { [Starlark.Label.named(label)] } }, - linkopts: linkopts(of: target).nonEmpty, + linkopts: linkopts(of: target, in: package), tags: Self.manual, testonly: true, visibility: .private)) diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Trait.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Trait.swift new file mode 100644 index 0000000..4ad3e7d --- /dev/null +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Trait.swift @@ -0,0 +1,169 @@ +// +// SwiftPM+Trait.swift +// +// +// A package's traits, as something the build decides rather than the +// generator. +// + +import BazelRules +import Foundation +@preconcurrency import PathKit +import Starlark +import Util + +extension SwiftPM.Generator { + /// One trait of one package, as a flag a build can flip. + struct TraitFlag { + /// `trait__`, the flag's name under `//Packages`. + let name: String + /// `.`, the name a `--config` goes by. + let config: String + let package: String + let trait: String + /// What the graph resolves the trait to: a package's default traits, or + /// what a dependent asked for by name. + let isDefault: Bool + } + + /// Every trait every package in the workspace declares. + /// + /// The flag defaults to what the manifest graph says, so a build that asks + /// for nothing is the build SwiftPM would have made; `--config=.` + /// is how a build asks for something else. + var traitFlags: [TraitFlag] { + workspace.packages.flatMap { package -> [TraitFlag] in + let enabled = workspace.traits[package.identity] ?? [] + + return package.manifest.traits + /// Not a trait: the list of traits a build that says nothing gets. + .filter { $0.name != "default" } + .sorted { $0.name < $1.name } + .map { trait in + TraitFlag( + name: Self.flagName(package: package.directory, trait: trait.name), + config: "\(package.directory).\(trait.name)", + package: package.directory, + trait: trait.name, + isDefault: enabled.contains(trait.name)) + } + } + } + + static func flagName(package: String, trait: String) -> String { + "trait_\(identifier(package))_\(identifier(trait))" + } + + /// A label is not a place for whatever a package is called on disk. + private static func identifier(_ name: String) -> String { + String(name.map { character in + character.isLetter || character.isNumber || character == "_" ? character : "_" + }) + } + + /// The label a `select` keys a trait condition on, or `nil` when what + /// carries the condition is in every build. + /// + /// A condition naming several traits is satisfied by any of them, which is + /// a `config_setting_group`; the group is recorded here and written with + /// the flags. + func traitCondition(_ traits: [String], in package: SwiftPM.Package) -> String? { + let names = traits.sorted() + guard let first = names.first else { return nil } + + let settings = names.map { "\(Self.flagName(package: package.directory, trait: $0))_on" } + guard names.count > 1 else { + return "//\(PluginSwiftPM.packagesDirectory):\(settings[0])" + } + + let group = "\(Self.flagName(package: package.directory, trait: first))_or_\(names.count - 1)_more" + traitGroups[group] = settings + return "//\(PluginSwiftPM.packagesDirectory):\(group)" + } + + /// A list of flags or labels, plus one `select` per trait condition. + /// + /// One `select` each rather than one with several keys: two traits can be + /// on at once, and a `select` whose keys both match is an error rather than + /// both lists. + func traitValue( + _ always: [String], + conditional: [(condition: String, values: [String])]) -> Starlark.Value? + { + guard !conditional.isEmpty else { return always.starlark } + + let parts = [Starlark.Value.array(always.map { .label(.init($0)) }).text] + + conditional.map { entry in + """ + select({ + "\(entry.condition)": \(Starlark.Value.array(entry.values.map { .label(.init($0)) }).text), + "//conditions:default": [], + }) + """ + } + + return .custom(parts.joined(separator: " + ")) + } + + /// The flags, their settings, and the groups a condition asked for. + /// + /// They live under `Packages/` because that is the package the generator + /// owns: the root `BUILD` belongs to the project. + func buildTraitRules(_ builder: CodeBuilder) { + let flags = traitFlags + guard !flags.isEmpty else { return } + + builder.load(.bool_flag) + for flag in flags { + builder.call( + Rules.Config.Call.bool_flag( + name: flag.name, + build_setting_default: flag.isDefault, + visibility: .public)) + builder.call( + Rules.Builtin.Call.config_setting( + name: "\(flag.name)_on", + flag_values: [":\(flag.name)": "true"])) + } + + guard !traitGroups.isEmpty else { return } + + builder.load(loadableRule: Rules.Selects.selects) + for group in traitGroups.keys.sorted() { + builder.call( + Rules.Selects.Call.config_setting_group( + name: group, + match_any: (traitGroups[group] ?? []).map { ":\($0)" })) + } + } + + /// `--config=.` for every trait, and `-off` for turning one + /// off that the graph turns on. + /// + /// A configuration is how a Bazel workspace is told what to build, so it is + /// how a trait is asked for too. The file is always written, because a + /// `.bazelrc` that imports a file that is not there does not load. + func writeTraitConfigs() throws { + let flags = traitFlags + var lines = [ + "# Generated by Bazelize: one --config per trait of the packages this", + "# workspace builds. A trait's flag defaults to what the manifests say,", + "# so a build that asks for nothing is the build SwiftPM would make.", + ] + + if flags.isEmpty { + lines.append("#") + lines.append("# No package in this workspace declares a trait.") + } + + for flag in flags { + let label = "--//\(PluginSwiftPM.packagesDirectory):\(flag.name)" + lines.append("") + lines.append("# \(flag.package): \(flag.trait)\(flag.isDefault ? ", on by default" : "")") + lines.append("build:\(flag.config) \(label)=true") + lines.append("build:\(flag.config)-off \(label)=false") + } + + try (output + "traits.bazelrc").write(lines.joined(separator: "\n") + "\n") + } +} diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Workspace.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Workspace.swift index d4706ec..1262c06 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Workspace.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Workspace.swift @@ -43,6 +43,10 @@ extension SwiftPM { /// Which directory a package identity or manifest name resolves to, so a /// product dependency can be turned into a label. let directoryByIdentity: [String: String] + + /// Which traits each package is built with unless the build says + /// otherwise, by identity. + let traits: [String: Set] } } @@ -70,7 +74,8 @@ extension SwiftPM { return .init( packages: [], artifacts: output + ".build/artifacts", - directoryByIdentity: [:]) + directoryByIdentity: [:], + traits: [:]) } try await resolve(output: output) @@ -106,8 +111,8 @@ extension SwiftPM { manifests.sort { $0.root.directory < $1.root.directory } /// Which traits are on is a property of the graph, not of one manifest, - /// so it is answered once every manifest is read — and then the - /// conditions are resolved away. + /// so it is answered once every manifest is read. It is what the flag + /// of each trait defaults to, not something resolved away here. let traits = enabledTraits( of: manifests.map { (identity: $0.root.directory.lowercased(), manifest: $0.manifest) }, directoryByIdentity: directoryByIdentity) @@ -115,9 +120,7 @@ extension SwiftPM { Package( directory: entry.root.directory, root: entry.root.path, - manifest: entry.manifest.resolving( - traits: traits[entry.root.directory.lowercased()] ?? [], - platforms: platforms), + manifest: entry.manifest.resolving(platforms: platforms), isLocal: entry.root.isLocal, /// Both sides are made absolute: the output can be a relative path, /// and the package handed in is named however the caller named it. @@ -127,7 +130,8 @@ extension SwiftPM { return .init( packages: packages, artifacts: output + ".build/artifacts", - directoryByIdentity: directoryByIdentity) + directoryByIdentity: directoryByIdentity, + traits: traits) } /// The traits each package is built with, by identity. diff --git a/Sources/Starlark/Starlark/Value/Starlark+Value.swift b/Sources/Starlark/Starlark/Value/Starlark+Value.swift index 1ae31b4..3421dbf 100644 --- a/Sources/Starlark/Starlark/Value/Starlark+Value.swift +++ b/Sources/Starlark/Starlark/Value/Starlark+Value.swift @@ -218,3 +218,11 @@ extension Array where Element == Starlark.Value { .array(self) } } + +extension Array where Element == String { + /// A list of flags as a value, so an attribute that takes one can also take + /// a `select`. `nil` rather than an empty attribute. + public var starlark: Starlark.Value? { + isEmpty ? nil : .array(map { .label(.init($0)) }) + } +} diff --git a/docs/SPM.md b/docs/SPM.md index 484d58d..1858865 100644 --- a/docs/SPM.md +++ b/docs/SPM.md @@ -43,6 +43,7 @@ App/ ├── Package.swift # synthesized manifest, read by rspm ├── Package.resolved # seeded from Xcode's Package.resolved ├── config.bazelrc +├── traits.bazelrc # one `--config` per trait of the packages ├── BUILD ├── Prebuilt/ # project-owned .framework/.a/.dylib (symlinks) └── Targets// @@ -91,7 +92,7 @@ App/ |---|---| | `bazel run //:plugins` | builds this workspace's build tool plugins and their tools, runs them, and writes what they generate back into `Packages/*/Generated/` | | `bazel list config` | the `--config=` this workspace defines, and the flags every build gets anyway | -| `bazel list trait` | the traits its packages declare, which are on, and why | +| `bazel list trait` | the traits its packages declare, which are on, and the `--config` that switches each | `list` is not a Bazel command: `tools/bazel` is, which is the wrapper Bazelisk runs instead of Bazel and hands the real binary in `BAZEL_REAL`. `list` is @@ -214,9 +215,9 @@ No test pins how a package's rules are produced either. | build tool plugin, dependency | not run; the plugin is named at the end of the run | | command plugin | nothing: it runs when someone asks for it by name, never during a build | | macro target | `swift_compiler_plugin`, and `plugins` on whatever declares the macro | -| traits (SE-0450) | resolved: a package gets its defaults unless a dependent names traits instead, and a setting conditional on a trait that is off is dropped | +| traits (SE-0450) | a `bool_flag` each, defaulting to what the manifests resolve to, with a `--config=.` beside it; what is conditional on one is a `select` | | `.when(platforms:)` on a setting or a dependency | dropped unless the project builds one of those platforms; a platform no Apple toolchain builds is always dropped | -| `.when(traits:)` on a dependency | dropped unless one of those traits is on | +| `.when(traits:)` on a setting or a dependency | a `select` on that trait's flag, so the build decides it — a condition naming several traits is a `config_setting_group` | | `.when(configuration:)` on a setting | kept: which configuration a rule is built in is Bazel's answer, not the generator's | Two SwiftPM behaviours are matched on every generated `swift_library`: diff --git a/docs/SPM_ZH.md b/docs/SPM_ZH.md index 617b991..c993368 100644 --- a/docs/SPM_ZH.md +++ b/docs/SPM_ZH.md @@ -37,6 +37,7 @@ App/ ├── Package.swift # 給 rspm 讀的合成 manifest ├── Package.resolved # 由 Xcode 的 Package.resolved 播種 ├── config.bazelrc +├── traits.bazelrc # 每個 package trait 一個 `--config` ├── BUILD ├── Prebuilt/ # 專案自帶的 .framework/.a/.dylib(symlink) └── Targets// @@ -84,7 +85,7 @@ App/ |---|---| | `bazel run //:plugins` | 讓 Bazel 建這個 workspace 的 build tool plugin 與它們的工具、執行它們,把產生的檔案寫回 `Packages/*/Generated/` | | `bazel list config` | 這個 workspace 定義了哪些 `--config=`,以及每次 build 一定會拿到的 flag | -| `bazel list trait` | 它的 package 宣告了哪些 trait、哪些是開的、為什麼 | +| `bazel list trait` | 它的 package 宣告了哪些 trait、哪些是開的,以及切換各自要用哪個 `--config` | `list` 不是 Bazel 的指令,`tools/bazel` 才是:Bazelisk 會執行這個 wrapper 而不是 Bazel 本身,並把真正的執行檔放在 `BAZEL_REAL`。`list` 在那裡就回答完了——印一份清單 @@ -194,9 +195,9 @@ target 的 `deps` 需要改。測試也不釘 package 的規則是怎麼產生 | build tool plugin(依賴的 package) | 不執行;結束時把該 plugin 的名字講出來 | | command plugin | 不處理:它是有人指名才跑,build 永遠用不到 | | macro target | `swift_compiler_plugin`,並在宣告該 macro 的 target 上加 `plugins` | -| traits(SE-0450) | 會解析:沒人指名就用 package 自己的預設 traits,有人指名就用指名的;條件在「沒開的 trait」上的 setting 直接丟掉 | +| traits(SE-0450) | 一個 trait 一個 `bool_flag`,預設值就是 manifest 解析出來的結果,旁邊配一個 `--config=.`;條件掛在 trait 上的東西變成 `select` | | setting 或依賴上的 `.when(platforms:)` | 專案沒有建那些平台就丟掉;Apple toolchain 根本不建的平台一律丟掉 | -| 依賴上的 `.when(traits:)` | 那些 trait 沒開就丟掉 | +| setting 或依賴上的 `.when(traits:)` | 變成掛在該 trait flag 上的 `select`,由 build 當下決定;條件寫了多個 trait 就產生 `config_setting_group` | | setting 上的 `.when(configuration:)` | 保留:規則是在哪個 configuration 建,是 Bazel 當下決定的,不是產生時 | 每個產生的 `swift_library` 都對齊兩個 SwiftPM 行為:`alwayslink`,因為 SwiftPM diff --git a/spm/DependencyCondition/Extras/Sources/Extras/Extras.swift b/spm/DependencyCondition/Extras/Sources/Extras/Extras.swift index f2e8bc2..d1fb200 100644 --- a/spm/DependencyCondition/Extras/Sources/Extras/Extras.swift +++ b/spm/DependencyCondition/Extras/Sources/Extras/Extras.swift @@ -1,5 +1,5 @@ -#error("This package is behind a trait nobody enabled, so building it is the bug this package is here to catch.") - public enum Extras { + /// Only ever compiled when the trait that asks for this package is on: the + /// package that depends on it does so behind that trait. public static let value = 3 } diff --git a/spm/DependencyCondition/Package.swift b/spm/DependencyCondition/Package.swift index 6c5a7cb..113dbe9 100644 --- a/spm/DependencyCondition/Package.swift +++ b/spm/DependencyCondition/Package.swift @@ -3,7 +3,7 @@ import PackageDescription /// A dependency can be conditional: on the platform being built, or on a trait -/// being on. What the condition excludes must not be built at all. +/// being on. What the condition excludes is not part of the build. /// /// The conditional ones are packages of their own because a target of the /// package being built is compiled whether anything depends on it or not — only @@ -31,5 +31,10 @@ let package = Package( .target(name: "Always"), .testTarget( name: "ConditionalTests", - dependencies: ["Conditional"]), + dependencies: ["Conditional"], + swiftSettings: [ + /// So the test can say which build it is in: the trait decides + /// this define the same way it decides the dependency. + .define("EXTRAS", .when(traits: ["Extras"])), + ]), ]) diff --git a/spm/DependencyCondition/Sources/Conditional/Conditional.swift b/spm/DependencyCondition/Sources/Conditional/Conditional.swift index 7aa1524..16517a0 100644 --- a/spm/DependencyCondition/Sources/Conditional/Conditional.swift +++ b/spm/DependencyCondition/Sources/Conditional/Conditional.swift @@ -1,5 +1,18 @@ import Always +#if canImport(Extras) +import Extras +#endif public enum Conditional { public static let always = Always.value + + /// What the package behind a trait provides, and `nil` when the trait is + /// off — the dependency is then not part of the build at all. + public static var extras: Int? { + #if canImport(Extras) + return Extras.value + #else + return nil + #endif + } } diff --git a/spm/DependencyCondition/Tests/ConditionalTests/ConditionalTests.swift b/spm/DependencyCondition/Tests/ConditionalTests/ConditionalTests.swift index c20e703..bb06a69 100644 --- a/spm/DependencyCondition/Tests/ConditionalTests/ConditionalTests.swift +++ b/spm/DependencyCondition/Tests/ConditionalTests/ConditionalTests.swift @@ -2,6 +2,17 @@ import Conditional import Testing @Test -func onlyTheUnconditionalDependencyIsLinked() { +func theUnconditionalDependencyIsAlwaysLinked() { #expect(Conditional.always == 1) } + +/// Both ways round: `bazel test //...` has the trait off, and +/// `bazel test //... --config=DependencyCondition.Extras` has it on. +@Test +func aTraitDecidesWhetherItsDependencyIsLinked() { + #if EXTRAS + #expect(Conditional.extras == 3) + #else + #expect(Conditional.extras == nil) + #endif +} diff --git a/spm/README.md b/spm/README.md index d114687..d745d9b 100644 --- a/spm/README.md +++ b/spm/README.md @@ -32,6 +32,7 @@ bazel run //:plugins # only the packages with a build tool plugin need this bazel test //... bazel list config # what `--config=` the workspace defines bazel list trait # which traits its packages declare, and which are on +bazel test //... --config=. # …with one of them turned on ``` `bazel run //:plugins` runs this workspace's build tool plugins and writes what From 80f9db272c6c0a95c7244eacf570acc00c881bae Mon Sep 17 00:00:00 2001 From: yume190 Date: Mon, 21 Sep 2026 15:09:00 +0800 Subject: [PATCH 03/47] A trait defines its own name, so a trait only ever turns on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SwiftPM compiles a package with `-D` for every trait that package has on — `#if Fast` is what a source asks, and nothing has to declare a define for it. That was missing: only a setting that named a trait was generated, so a source asking for the trait itself compiled as if it were off. Every trait is now a `-D` of its own name on the package's Swift targets, behind that trait's flag. Swift only, because that is where SwiftPM puts it: a C target of the same package sees nothing. And with that, a trait adds and never removes, so there is nothing to name for turning one off: `traits.bazelrc` is one `--config=.` per trait, the `-off` half is gone, and `bazel list trait` names the config beside the traits that are not on already. The flag underneath still takes `=false` for the rare build that wants a default trait without it. The two fixtures now say what SwiftPM says: `spm/Trait` reads `#if Fast` and `#if Slow` rather than defines it declared itself, keeping one conditional setting to cover that case as well, and `spm/DependencyCondition` reads `#if Extras`. Both are built both ways in CI, and the lane checks the flag reaches swiftc — a test that passes either way round proves less than `-DSlow` being on the command line only when it was asked for. --- .github/workflows/swift.yml | 10 ++++++++- Sources/BazelizeKit/List/Listing.swift | 10 ++++----- .../SwiftPM/SwiftPM+Settings.swift | 22 +++++++++++++++++++ .../BazelizeKit/SwiftPM/SwiftPM+Trait.swift | 17 ++++++++------ docs/SPM.md | 2 +- docs/SPM_ZH.md | 2 +- spm/DependencyCondition/Package.swift | 7 +----- .../ConditionalTests/ConditionalTests.swift | 5 +++-- spm/Trait/Dependency/Package.swift | 6 +---- .../Sources/Dependency/Dependency.swift | 7 +++--- spm/Trait/Package.swift | 11 +++++----- spm/Trait/Sources/Trait/Trait.swift | 19 ++++++++++++---- spm/Trait/Tests/TraitTests/TraitTests.swift | 11 +++++++++- 13 files changed, 88 insertions(+), 41 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 1ee94df..3c24df7 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -326,6 +326,7 @@ jobs: config: DependencyCondition.Extras - name: Macro - name: Trait + config: Trait.Slow - name: TargetSources - name: TargetPath - name: TargetExclude @@ -378,7 +379,14 @@ jobs: working-directory: spm/${{ matrix.name }}/App run: bazel test //... + # Both ways round, and not only that both pass: a trait is a compilation + # condition named after itself, so the flag reaching swiftc is the thing + # to see. - name: Test Package With Trait if: matrix.config working-directory: spm/${{ matrix.name }}/App - run: bazel test //... --config=${{ matrix.config }} + run: | + bazel test //... --config=${{ matrix.config }} + trait="${{ matrix.config }}" + ! bazel aquery 'mnemonic("SwiftCompile", //...)' | grep -q -- "-D${trait#*.}" + bazel aquery --config=${{ matrix.config }} 'mnemonic("SwiftCompile", //...)' | grep -q -- "-D${trait#*.}" diff --git a/Sources/BazelizeKit/List/Listing.swift b/Sources/BazelizeKit/List/Listing.swift index 12a68d5..461c219 100644 --- a/Sources/BazelizeKit/List/Listing.swift +++ b/Sources/BazelizeKit/List/Listing.swift @@ -79,17 +79,17 @@ public enum Listing { lines.append( " \(on ? "on " : "off") \(trait.name)" + (enables.isEmpty ? "" : " (enables \(enables))") - + " --config=\(package.directory).\(trait.name)\(on ? "-off" : "")") + + (on ? "" : " --config=\(package.directory).\(trait.name)")) } } lines.append("") lines.append(""" A trait is on by default when its package makes it one, or when \ - something that depends on that package asks for it by name. Every \ - trait is a flag, so a build switches one with the `--config` beside \ - it — `--config=.` to turn it on, `-off` to turn it \ - off — and `bazel list config` lists them with everything else. + something that depends on that package asks for it by name. Anything \ + else is asked for by the `--config` beside it, which defines the \ + trait's own name for that package's sources and pulls in whatever is \ + behind it. `bazel list config` lists them with everything else. """) return lines.joined(separator: "\n") } diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Settings.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Settings.swift index 9fb72f5..e87887a 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Settings.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Settings.swift @@ -29,9 +29,25 @@ extension SwiftPM.Generator { /// must not compile as if it were a package — Xcode's generated asset /// symbols, for one, switch on `SWIFT_PACKAGE`. always: Self.define("SWIFT_PACKAGE"), + /// A trait is a compilation condition of the package that declares + /// it: `#if Fast` is how a source asks. Swift only — SwiftPM does + /// not hand it to clang, so neither is it handed to `-Xcc`. + conditional: traitDefines(of: package), flags: Self.swiftFlags) } + /// `-D` for every trait the package declares, each behind its own + /// flag: the traits the build turned on are the ones defined. + private func traitDefines(of package: SwiftPM.Package) -> [(condition: String, values: [String])] { + package.manifest.traits + .filter { $0.name != "default" } + .sorted { $0.name < $1.name } + .compactMap { trait in + guard let condition = traitCondition([trait.name], in: package) else { return nil } + return (condition, ["-D\(trait.name)"]) + } + } + /// A package can name a system library or framework it needs; nothing else in /// the graph knows about it. func linkopts(of target: SwiftPM.PackageTarget, in package: SwiftPM.Package) -> Starlark.Value? { @@ -106,12 +122,18 @@ extension SwiftPM.Generator { _ settings: [SwiftPM.Setting], in package: SwiftPM.Package, always base: [String], + conditional seeds: [(condition: String, values: [String])] = [], flags: (SwiftPM.Setting) -> [String]) -> Starlark.Value? { var always = base var conditions: [String] = [] var byCondition: [String: [String]] = [:] + for seed in seeds { + if byCondition[seed.condition] == nil { conditions.append(seed.condition) } + byCondition[seed.condition, default: []] += seed.values + } + for setting in settings { let values = flags(setting) guard !values.isEmpty else { continue } diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Trait.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Trait.swift index 4ad3e7d..80968df 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Trait.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Trait.swift @@ -137,12 +137,17 @@ extension SwiftPM.Generator { } } - /// `--config=.` for every trait, and `-off` for turning one - /// off that the graph turns on. + /// `--config=.` for every trait a package declares. /// /// A configuration is how a Bazel workspace is told what to build, so it is - /// how a trait is asked for too. The file is always written, because a - /// `.bazelrc` that imports a file that is not there does not load. + /// how a trait is asked for too. Only asked for: a trait adds — it defines + /// its own name and pulls in what is behind it — so there is nothing to + /// name for turning one off. A trait the manifests turn on is on already, + /// and the flag underneath takes `=false` for the rare build that wants it + /// without. + /// + /// The file is always written, because a `.bazelrc` that imports a file + /// that is not there does not load. func writeTraitConfigs() throws { let flags = traitFlags var lines = [ @@ -157,11 +162,9 @@ extension SwiftPM.Generator { } for flag in flags { - let label = "--//\(PluginSwiftPM.packagesDirectory):\(flag.name)" lines.append("") lines.append("# \(flag.package): \(flag.trait)\(flag.isDefault ? ", on by default" : "")") - lines.append("build:\(flag.config) \(label)=true") - lines.append("build:\(flag.config)-off \(label)=false") + lines.append("build:\(flag.config) --//\(PluginSwiftPM.packagesDirectory):\(flag.name)=true") } try (output + "traits.bazelrc").write(lines.joined(separator: "\n") + "\n") diff --git a/docs/SPM.md b/docs/SPM.md index 1858865..ad40ff0 100644 --- a/docs/SPM.md +++ b/docs/SPM.md @@ -215,7 +215,7 @@ No test pins how a package's rules are produced either. | build tool plugin, dependency | not run; the plugin is named at the end of the run | | command plugin | nothing: it runs when someone asks for it by name, never during a build | | macro target | `swift_compiler_plugin`, and `plugins` on whatever declares the macro | -| traits (SE-0450) | a `bool_flag` each, defaulting to what the manifests resolve to, with a `--config=.` beside it; what is conditional on one is a `select` | +| traits (SE-0450) | a `bool_flag` each, defaulting to what the manifests resolve to, with a `--config=.` that turns one on; a trait that is on defines its own name for that package's Swift sources, the way SwiftPM compiles it | | `.when(platforms:)` on a setting or a dependency | dropped unless the project builds one of those platforms; a platform no Apple toolchain builds is always dropped | | `.when(traits:)` on a setting or a dependency | a `select` on that trait's flag, so the build decides it — a condition naming several traits is a `config_setting_group` | | `.when(configuration:)` on a setting | kept: which configuration a rule is built in is Bazel's answer, not the generator's | diff --git a/docs/SPM_ZH.md b/docs/SPM_ZH.md index c993368..133ab59 100644 --- a/docs/SPM_ZH.md +++ b/docs/SPM_ZH.md @@ -195,7 +195,7 @@ target 的 `deps` 需要改。測試也不釘 package 的規則是怎麼產生 | build tool plugin(依賴的 package) | 不執行;結束時把該 plugin 的名字講出來 | | command plugin | 不處理:它是有人指名才跑,build 永遠用不到 | | macro target | `swift_compiler_plugin`,並在宣告該 macro 的 target 上加 `plugins` | -| traits(SE-0450) | 一個 trait 一個 `bool_flag`,預設值就是 manifest 解析出來的結果,旁邊配一個 `--config=.`;條件掛在 trait 上的東西變成 `select` | +| traits(SE-0450) | 一個 trait 一個 `bool_flag`,預設值就是 manifest 解析出來的結果,旁邊配一個會把它打開的 `--config=.`;開著的 trait 會為該 package 的 Swift 原始碼定義同名條件,跟 SwiftPM 一樣 | | setting 或依賴上的 `.when(platforms:)` | 專案沒有建那些平台就丟掉;Apple toolchain 根本不建的平台一律丟掉 | | setting 或依賴上的 `.when(traits:)` | 變成掛在該 trait flag 上的 `select`,由 build 當下決定;條件寫了多個 trait 就產生 `config_setting_group` | | setting 上的 `.when(configuration:)` | 保留:規則是在哪個 configuration 建,是 Bazel 當下決定的,不是產生時 | diff --git a/spm/DependencyCondition/Package.swift b/spm/DependencyCondition/Package.swift index 113dbe9..37a4ea9 100644 --- a/spm/DependencyCondition/Package.swift +++ b/spm/DependencyCondition/Package.swift @@ -31,10 +31,5 @@ let package = Package( .target(name: "Always"), .testTarget( name: "ConditionalTests", - dependencies: ["Conditional"], - swiftSettings: [ - /// So the test can say which build it is in: the trait decides - /// this define the same way it decides the dependency. - .define("EXTRAS", .when(traits: ["Extras"])), - ]), + dependencies: ["Conditional"]), ]) diff --git a/spm/DependencyCondition/Tests/ConditionalTests/ConditionalTests.swift b/spm/DependencyCondition/Tests/ConditionalTests/ConditionalTests.swift index bb06a69..cf66c8a 100644 --- a/spm/DependencyCondition/Tests/ConditionalTests/ConditionalTests.swift +++ b/spm/DependencyCondition/Tests/ConditionalTests/ConditionalTests.swift @@ -7,10 +7,11 @@ func theUnconditionalDependencyIsAlwaysLinked() { } /// Both ways round: `bazel test //...` has the trait off, and -/// `bazel test //... --config=DependencyCondition.Extras` has it on. +/// `bazel test //... --config=DependencyCondition.Extras` has it on. The +/// trait is a condition of every target of the package, tests included. @Test func aTraitDecidesWhetherItsDependencyIsLinked() { - #if EXTRAS + #if Extras #expect(Conditional.extras == 3) #else #expect(Conditional.extras == nil) diff --git a/spm/Trait/Dependency/Package.swift b/spm/Trait/Dependency/Package.swift index 222265d..60f1394 100644 --- a/spm/Trait/Dependency/Package.swift +++ b/spm/Trait/Dependency/Package.swift @@ -12,9 +12,5 @@ let package = Package( .trait(name: "Extra"), ], targets: [ - .target( - name: "Dependency", - swiftSettings: [ - .define("EXTRA", .when(traits: ["Extra"])), - ]), + .target(name: "Dependency"), ]) diff --git a/spm/Trait/Dependency/Sources/Dependency/Dependency.swift b/spm/Trait/Dependency/Sources/Dependency/Dependency.swift index 02784b8..d852972 100644 --- a/spm/Trait/Dependency/Sources/Dependency/Dependency.swift +++ b/spm/Trait/Dependency/Sources/Dependency/Dependency.swift @@ -1,8 +1,9 @@ public enum Dependency { - /// On only because the package that depends on this one asked for the trait - /// by name: it is not one of this package's defaults. + /// On only because the package that depends on this one asked for the + /// trait by name: it is not one of this package's defaults. The trait is + /// the condition, nothing had to declare a define for it. public static var extra: Bool { - #if EXTRA + #if Extra return true #else return false diff --git a/spm/Trait/Package.swift b/spm/Trait/Package.swift index d7f3d97..48e0890 100644 --- a/spm/Trait/Package.swift +++ b/spm/Trait/Package.swift @@ -2,9 +2,9 @@ import PackageDescription -/// Traits: a package's own build-time options. Three things decide which ones -/// are on — the package's defaults, what a dependent asks for by name, and -/// nothing else — and a build setting can be conditional on one. +/// Traits: a package's own build-time options. A trait that is on is a +/// compilation condition of that package's own sources — `#if Fast` — and can +/// carry settings and dependencies of its own. let package = Package( name: "Trait", products: [ @@ -25,8 +25,9 @@ let package = Package( .product(name: "Dependency", package: "Dependency"), ], swiftSettings: [ - .define("FAST", .when(traits: ["Fast"])), - .define("SLOW", .when(traits: ["Slow"])), + /// A setting of its own, on top of the condition the trait is: + /// `Slow` defines `Slow`, and this as well. + .define("SLOW_EXTRA", .when(traits: ["Slow"])), ]), .testTarget( name: "TraitTests", diff --git a/spm/Trait/Sources/Trait/Trait.swift b/spm/Trait/Sources/Trait/Trait.swift index 67af68a..8f1bca9 100644 --- a/spm/Trait/Sources/Trait/Trait.swift +++ b/spm/Trait/Sources/Trait/Trait.swift @@ -1,19 +1,30 @@ import Dependency public enum Trait { - /// Which of this package's traits the build enabled. `Fast` is the default - /// one, so a build that asked for nothing still gets it; `Slow` is not. + /// Which of this package's traits the build has on. A trait is a + /// compilation condition named after itself, which is what SwiftPM + /// compiles the package with. public static var enabled: [String] { var traits: [String] = [] - #if FAST + #if Fast traits.append("Fast") #endif - #if SLOW + #if Slow traits.append("Slow") #endif return traits } + /// The setting that trait carries, which is not the same thing as the + /// trait being on. + public static var slowExtra: Bool { + #if SLOW_EXTRA + return true + #else + return false + #endif + } + /// Whether the trait this package asked the package next door for is on. public static var dependencyExtra: Bool { Dependency.extra diff --git a/spm/Trait/Tests/TraitTests/TraitTests.swift b/spm/Trait/Tests/TraitTests/TraitTests.swift index 630413c..109d132 100644 --- a/spm/Trait/Tests/TraitTests/TraitTests.swift +++ b/spm/Trait/Tests/TraitTests/TraitTests.swift @@ -1,9 +1,18 @@ import Testing import Trait +/// Both ways round: `bazel test //...` has only the default trait, and +/// `bazel test //... --config=Trait.Slow` has `Slow` as well. The test target +/// is part of the package, so the trait is its condition too. @Test -func onlyTheDefaultTraitIsEnabled() { +func theTraitsOnAreTheOnesAskedFor() { + #if Slow + #expect(Trait.enabled == ["Fast", "Slow"]) + #expect(Trait.slowExtra) + #else #expect(Trait.enabled == ["Fast"]) + #expect(!Trait.slowExtra) + #endif } @Test From 85af7b97fbc2377efe73fbab89c859d4ee2574a7 Mon Sep 17 00:00:00 2001 From: yume190 Date: Tue, 22 Sep 2026 10:07:16 +0800 Subject: [PATCH 04/47] test(spm): cover each trait selection --- .github/workflows/swift.yml | 22 ++++++++++++++++++++- spm/Trait/Package.swift | 5 ----- spm/Trait/Sources/Trait/Trait.swift | 10 ---------- spm/Trait/Tests/TraitTests/TraitTests.swift | 15 +++++++------- 4 files changed, 29 insertions(+), 23 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 3c24df7..667a681 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -326,7 +326,7 @@ jobs: config: DependencyCondition.Extras - name: Macro - name: Trait - config: Trait.Slow + traits: Fast,Slow - name: TargetSources - name: TargetPath - name: TargetExclude @@ -390,3 +390,23 @@ jobs: trait="${{ matrix.config }}" ! bazel aquery 'mnemonic("SwiftCompile", //...)' | grep -q -- "-D${trait#*.}" bazel aquery --config=${{ matrix.config }} 'mnemonic("SwiftCompile", //...)' | grep -q -- "-D${trait#*.}" + + # Select each declared trait on its own. This mirrors + # `swift test --traits ` rather than carrying default traits into + # every explicit selection. + - name: Test Each Package Trait + if: matrix.traits + working-directory: spm/${{ matrix.name }}/App + env: + PACKAGE: ${{ matrix.name }} + TRAITS: ${{ matrix.traits }} + run: | + IFS=',' read -ra traits <<< "$TRAITS" + disabled=() + for trait in "${traits[@]}"; do + disabled+=("--//Packages:trait_${PACKAGE}_${trait}=false") + done + for trait in "${traits[@]}"; do + bazel test //... "${disabled[@]}" \ + "--//Packages:trait_${PACKAGE}_${trait}=true" + done diff --git a/spm/Trait/Package.swift b/spm/Trait/Package.swift index 48e0890..ca046ca 100644 --- a/spm/Trait/Package.swift +++ b/spm/Trait/Package.swift @@ -23,11 +23,6 @@ let package = Package( name: "Trait", dependencies: [ .product(name: "Dependency", package: "Dependency"), - ], - swiftSettings: [ - /// A setting of its own, on top of the condition the trait is: - /// `Slow` defines `Slow`, and this as well. - .define("SLOW_EXTRA", .when(traits: ["Slow"])), ]), .testTarget( name: "TraitTests", diff --git a/spm/Trait/Sources/Trait/Trait.swift b/spm/Trait/Sources/Trait/Trait.swift index 8f1bca9..8fde827 100644 --- a/spm/Trait/Sources/Trait/Trait.swift +++ b/spm/Trait/Sources/Trait/Trait.swift @@ -15,16 +15,6 @@ public enum Trait { return traits } - /// The setting that trait carries, which is not the same thing as the - /// trait being on. - public static var slowExtra: Bool { - #if SLOW_EXTRA - return true - #else - return false - #endif - } - /// Whether the trait this package asked the package next door for is on. public static var dependencyExtra: Bool { Dependency.extra diff --git a/spm/Trait/Tests/TraitTests/TraitTests.swift b/spm/Trait/Tests/TraitTests/TraitTests.swift index 109d132..36556ae 100644 --- a/spm/Trait/Tests/TraitTests/TraitTests.swift +++ b/spm/Trait/Tests/TraitTests/TraitTests.swift @@ -1,17 +1,18 @@ import Testing import Trait -/// Both ways round: `bazel test //...` has only the default trait, and -/// `bazel test //... --config=Trait.Slow` has `Slow` as well. The test target -/// is part of the package, so the trait is its condition too. +/// The test target and library target must receive the same trait conditions, +/// for every combination SwiftPM accepts. @Test func theTraitsOnAreTheOnesAskedFor() { - #if Slow + #if Fast && Slow #expect(Trait.enabled == ["Fast", "Slow"]) - #expect(Trait.slowExtra) - #else + #elseif Fast #expect(Trait.enabled == ["Fast"]) - #expect(!Trait.slowExtra) + #elseif Slow + #expect(Trait.enabled == ["Slow"]) + #else + #expect(Trait.enabled == []) #endif } From c0de6bebdbcae705da70353d5a8430cfa07dd0b1 Mon Sep 17 00:00:00 2001 From: yume190 Date: Tue, 22 Sep 2026 10:07:35 +0800 Subject: [PATCH 05/47] feat(spm): move listings into Bazel --- Sources/Bazelize/Command.swift | 44 ---------- Sources/BazelizeKit/List/Listing.swift | 26 +++--- .../SwiftPM/SwiftPM+Generator.swift | 2 +- .../SwiftPM/SwiftPM+PluginRule.swift | 86 ++++++++++++------- docs/SPM.md | 20 ++--- docs/SPM_ZH.md | 17 ++-- spm/README.md | 11 +-- 7 files changed, 89 insertions(+), 117 deletions(-) diff --git a/Sources/Bazelize/Command.swift b/Sources/Bazelize/Command.swift index 4adb7e4..d6d8d19 100644 --- a/Sources/Bazelize/Command.swift +++ b/Sources/Bazelize/Command.swift @@ -22,56 +22,12 @@ struct Command: AsyncParsableCommand { subcommands: [ GenerateCommand.self, PluginsCommand.self, - ListCommand.self, XcodeCommand.self, // RoadmapCommand.self, ], defaultSubcommand: GenerateCommand.self) } -// MARK: - ListCommand - -/// Answers a question about a generated workspace: `bazel run //list:config` -/// and `bazel run //list:trait` are the workspace's own way to ask them. -/// -/// Both answers are read from the workspace as it is now, not from something -/// written into it when it was generated: a `.bazelrc` gets edited, and a -/// manifest's traits change with the manifest. -struct ListCommand: AsyncParsableCommand { - enum Topic: String, ExpressibleByArgument, CaseIterable { - /// The `--config=` this workspace defines. - case config - /// The traits its packages declare, and which of them are on. - case trait - } - - static let configuration = CommandConfiguration( - commandName: "list", - abstract: "List what a generated workspace is built with.") - - @Argument(help: "config|trait") - var topic: Topic - - @Option(name: [.customLong("output", withSingleDash: false)], help: "PATH/TO/OUTPUT") - var output = "." - - @Option(name: [.customLong("local", withSingleDash: false)], help: "PATH/TO/LOCAL/PACKAGE") - var locals: [String] = [] - - func run() async throws { - let outputPath = Path.current + output - - switch topic { - case .config: - print(try Listing.config(output: outputPath)) - case .trait: - print(try await Listing.traits( - output: outputPath, - locals: locals.map { Path.current + $0 })) - } - } -} - // MARK: - PluginsCommand /// Runs the build tool plugins of a generated workspace, and nothing else. diff --git a/Sources/BazelizeKit/List/Listing.swift b/Sources/BazelizeKit/List/Listing.swift index 461c219..442b774 100644 --- a/Sources/BazelizeKit/List/Listing.swift +++ b/Sources/BazelizeKit/List/Listing.swift @@ -10,16 +10,14 @@ import Foundation // MARK: - Listing -/// The questions a generated workspace answers about itself. +/// The answers embedded in a generated workspace's listing executables. /// -/// Both are read from the workspace as it is now rather than written into it -/// when it was generated: a `.bazelrc` is edited by hand, and a manifest's -/// traits change with the manifest. An answer that was true at generation time -/// and is false now is worse than no answer. -public enum Listing { - /// `bazel run //list:config`: the configurations this workspace defines, - /// and what every build gets whether it names one or not. - public static func config(output: Path) throws -> String { +/// Configuration files are read after generation, and trait state comes from +/// the same resolved workspace that generated the package rules. +enum Listing { + /// The configurations this workspace defines, and what every build gets + /// whether it names one or not. + static func config(output: Path) throws -> String { let rc = try configurations(in: output) var lines: [String] = [] @@ -47,11 +45,9 @@ public enum Listing { return lines.joined(separator: "\n") } - /// `bazel list trait`: the traits of every package in the workspace, which - /// of them a build gets by default, and what to say to change that. - public static func traits(output: Path, locals: [Path]) async throws -> String { - let workspace = try await SwiftPM.loadWorkspace(output: output, root: nil, locals: locals) - + /// The traits of every package in the workspace, which of them a build gets + /// by default, and what to say to change that. + static func traits(workspace: SwiftPM.Workspace) -> String { let declaring = workspace.packages .filter { !$0.manifest.traits.isEmpty } .sorted { $0.directory < $1.directory } @@ -89,7 +85,7 @@ public enum Listing { something that depends on that package asks for it by name. Anything \ else is asked for by the `--config` beside it, which defines the \ trait's own name for that package's sources and pulls in whatever is \ - behind it. `bazel list config` lists them with everything else. + behind it. `bazel run //tools:list-config` lists them with everything else. """) return lines.joined(separator: "\n") } diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift index 57a3e49..6ea791d 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift @@ -87,11 +87,11 @@ extension SwiftPM { } try writePluginRunner(locals: locals) - try writeListCommand(locals: locals) /// Written whether or not there is a trait to switch: the root /// `.bazelrc` imports it, and an import of a file that is not /// there is a workspace that does not load. try writeTraitConfigs() + try writeListingCommands() } /// A package that declares a platform version the project does not reach is diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginRule.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginRule.swift index f40ea93..dc065c6 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginRule.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginRule.swift @@ -101,54 +101,77 @@ extension SwiftPM.Generator { try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: script.string) } - /// `bazel list config` and `bazel list trait`: what a generated workspace - /// can be asked about itself. + /// Bazel-native commands that describe the generated workspace. /// - /// Bazel has no way to add a command, but its launcher does: Bazelisk runs - /// `tools/bazel` instead of Bazel itself and hands it the real binary in - /// `BAZEL_REAL`. So `list` is answered by the wrapper and everything else - /// goes straight through — no server starts to print a list. - /// - /// It is written for every workspace: a project with no packages still has - /// configurations, and packages that declare no trait is an answer too. - func writeListCommand(locals: [Path]) throws { + /// The answers are embedded in executable targets, so using them never + /// depends on whichever `bazelize` executable happens to be on `PATH`. + /// Bazel itself has no extension point for custom commands; `tools/bazel` + /// keeps `bazel list config|trait` as aliases for the two `bazel run` + /// targets and forwards every other command unchanged. + func writeListingCommands() throws { let directory = output + "tools" try directory.mkpath() - /// `tools` is a package of its own, so nothing globs the wrapper into - /// a rule of the workspace's root package. - try (directory + "BUILD").write("# The `bazel` launcher's wrapper lives here, and is not a build input.\n") + let listings = [ + ("config", try Listing.config(output: output)), + ("trait", Listing.traits(workspace: workspace)) + ] + let builder = CodeBuilder() + builder.load(loadableRule: Rules.Shell.sh_binary) + + for (topic, contents) in listings { + let name = "list-\(topic)" + let script = directory + "\(name).sh" + try script.write(""" + #!/bin/bash + cat <<'BAZELIZE_LIST' + \(contents) + BAZELIZE_LIST + + """) + try FileManager.default.setAttributes( + [.posixPermissions: 0o755], + ofItemAtPath: script.string) + builder.call( + Rules.Shell.Call.sh_binary( + name: name, + srcs: ["\(name).sh"])) + } - let arguments = locals - .flatMap { local in ["--local", local.absolute().string.quoted] } - .joined(separator: " ") + try (directory + "BUILD").write(builder.build()) - let script = directory + "bazel" - try script.write(""" + try writeBazelWrapper(to: directory + "bazel") + } + + private func writeBazelWrapper(to wrapper: Path) throws { + try wrapper.write(""" #!/bin/bash - # The `bazel` this workspace runs: `bazel list config` says what - # `--config=` it defines, `bazel list trait` says which traits its - # packages declare and which of them are on. Every other command is the - # one Bazel would have run. + # Bazelisk runs this workspace wrapper and exposes Bazel as BAZEL_REAL. set -euo pipefail - workspace="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" - - if [[ "${1:-}" == "list" ]]; then - shift - exec bazelize list "$@" --output "$workspace" \(arguments) - fi if [[ -z "${BAZEL_REAL:-}" ]]; then echo "tools/bazel ran without BAZEL_REAL: run Bazel through Bazelisk." >&2 exit 1 fi + if [[ "${1:-}" == "list" ]]; then + case "${2:-}" in + config|trait) + exec "$BAZEL_REAL" run "//tools:list-${2}" + ;; + *) + echo "Usage: bazel list config|trait" >&2 + exit 2 + ;; + esac + fi + exec "$BAZEL_REAL" "$@" """) - - /// The launcher only runs a wrapper it can execute. - try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: script.string) + try FileManager.default.setAttributes( + [.posixPermissions: 0o755], + ofItemAtPath: wrapper.string) } /// What a plugin needs built: the plugin itself, and the tools it runs. @@ -215,4 +238,3 @@ extension SwiftPM.Generator { let isPlugin: Bool } } - diff --git a/docs/SPM.md b/docs/SPM.md index ad40ff0..0d81d3c 100644 --- a/docs/SPM.md +++ b/docs/SPM.md @@ -73,7 +73,7 @@ App/ ├── config.bazelrc ├── BUILD ├── plugins.sh # what `bazel run //:plugins` runs -├── tools/bazel # what makes `bazel list` a command +├── tools/ # Bazel-native workspace inspection commands ├── Prebuilt/ ├── Targets// # unchanged └── Packages/ # ★ new @@ -91,16 +91,14 @@ App/ | command | what it does | |---|---| | `bazel run //:plugins` | builds this workspace's build tool plugins and their tools, runs them, and writes what they generate back into `Packages/*/Generated/` | -| `bazel list config` | the `--config=` this workspace defines, and the flags every build gets anyway | -| `bazel list trait` | the traits its packages declare, which are on, and the `--config` that switches each | - -`list` is not a Bazel command: `tools/bazel` is, which is the wrapper Bazelisk -runs instead of Bazel and hands the real binary in `BAZEL_REAL`. `list` is -answered there — no server starts to print a list — and every other command -goes straight through. Both answers are read from the workspace as it is now -rather than from something written into it at generation time, because a -`.bazelrc` gets edited and a manifest's traits change with the manifest. Both -answer for any workspace: no configuration and no trait are answers too. +| `bazel run //tools:list-config` | the `--config=` this workspace defines, and the flags every build gets anyway | +| `bazel run //tools:list-trait` | the traits its packages declare, which are on, and the `--config` that switches each | + +Both listing commands are generated `sh_binary` targets. Their answers are +embedded from the same resolved workspace and configuration files that generate +the package rules; running them requires Bazel, but no `bazelize` executable. +The generated `tools/bazel` wrapper keeps `bazel list config|trait` as shorter +aliases and forwards every other command unchanged. ### How a package's sources get in diff --git a/docs/SPM_ZH.md b/docs/SPM_ZH.md index 133ab59..346c9b3 100644 --- a/docs/SPM_ZH.md +++ b/docs/SPM_ZH.md @@ -66,7 +66,7 @@ App/ ├── config.bazelrc ├── BUILD ├── plugins.sh # `bazel run //:plugins` 跑的就是它 -├── tools/bazel # 讓 `bazel list` 變成一個指令的東西 +├── tools/ # Bazel 原生的 workspace 查詢指令 ├── Prebuilt/ ├── Targets// # 完全不變 └── Packages/ # ★ 新增 @@ -84,14 +84,13 @@ App/ | 指令 | 做什麼 | |---|---| | `bazel run //:plugins` | 讓 Bazel 建這個 workspace 的 build tool plugin 與它們的工具、執行它們,把產生的檔案寫回 `Packages/*/Generated/` | -| `bazel list config` | 這個 workspace 定義了哪些 `--config=`,以及每次 build 一定會拿到的 flag | -| `bazel list trait` | 它的 package 宣告了哪些 trait、哪些是開的,以及切換各自要用哪個 `--config` | - -`list` 不是 Bazel 的指令,`tools/bazel` 才是:Bazelisk 會執行這個 wrapper 而不是 -Bazel 本身,並把真正的執行檔放在 `BAZEL_REAL`。`list` 在那裡就回答完了——印一份清單 -不需要起一個 Bazel server——其他指令原封不動往下傳。兩個答案都是「現在」讀出來的, -不是產生當下寫死的:`.bazelrc` 會被人改,manifest 的 trait 也會跟著 manifest 變。 -兩個指令對任何 workspace 都答得出來:沒有 config、沒有 trait 也是答案。 +| `bazel run //tools:list-config` | 這個 workspace 定義了哪些 `--config=`,以及每次 build 一定會拿到的 flag | +| `bazel run //tools:list-trait` | 它的 package 宣告了哪些 trait、哪些是開的,以及切換各自要用哪個 `--config` | + +兩個清單指令都是產生出來的 `sh_binary` target。答案來自產生 package rules +時使用的同一份 resolved workspace 與設定檔;執行時只需要 Bazel,不需要 +`bazelize`。產生的 `tools/bazel` wrapper 仍保留較短的 +`bazel list config|trait` alias,其他指令則原封不動往下傳。 ### package 的原始碼怎麼進來 diff --git a/spm/README.md b/spm/README.md index d745d9b..6bbd9b7 100644 --- a/spm/README.md +++ b/spm/README.md @@ -12,7 +12,7 @@ would not tell us anything. | `CommandPlugin` | a plugin that is run on demand rather than while building, which nothing in a build may try to run | | `DependencyCondition` | dependencies conditional on a platform and on a trait: what the condition excludes must not be built | | `Macro` | a macro target, loaded by the compiler while the target beside it is compiled | -| `Trait` | the package's own traits, a default one, a dependency whose trait is turned on by name, and build settings conditional on each | +| `Trait` | the package's own traits, a default one, and a dependency whose trait is turned on by name | | `TargetSources` | `sources:`, where a file beside the listed ones must not be compiled | | `TargetPath` | `path:`, where neither the target nor its tests are under `Sources/` | | `TargetExclude` | `exclude:`, where a named file and a named directory must not be compiled | @@ -30,8 +30,8 @@ bazelize --project . --output App cd App bazel run //:plugins # only the packages with a build tool plugin need this bazel test //... -bazel list config # what `--config=` the workspace defines -bazel list trait # which traits its packages declare, and which are on +bazel run //tools:list-config # what `--config=` the workspace defines +bazel run //tools:list-trait # which traits its packages declare, and which are on bazel test //... --config=. # …with one of them turned on ``` @@ -40,7 +40,8 @@ they generate into `Packages//Generated/`. It is a separate step because a plugin is a program: Bazel builds it, and bazelize runs it as SwiftPM would. -`bazel list` is a command the generated `tools/bazel` adds, which Bazelisk runs -in Bazel's place. `bazelize` has to be on `PATH` for either. +The listing commands are generated Bazel targets and do not need `bazelize` at +runtime. The generated `tools/bazel` wrapper also exposes them as +`bazel list config|trait`. `App/` is generated, and is not checked in. From 26516be8e5e744d0013995640eb5b7b308382162 Mon Sep 17 00:00:00 2001 From: yume190 Date: Tue, 22 Sep 2026 11:18:57 +0800 Subject: [PATCH 06/47] feat(spm): decide a configuration and a trait selection in the build A setting conditional on debug or release becomes a select on compilation mode, and one naming both a configuration and traits a config_setting_group of the two. A trait config is now a selection rather than a single flag: it turns on the trait and everything that trait enables, and turns the package's other traits off, which is what an explicit selection means to SwiftPM. A dependency asking for no trait at all gets none, defaults included. --- Sources/BazelRules/Rules+Builtin.swift | 4 + Sources/BazelRules/Rules+Selects.swift | 12 +- .../BazelizeKit/SwiftPM/SwiftPM+Trait.swift | 128 +++++++++++++++--- .../SwiftPM/SwiftPM+Workspace.swift | 11 +- Tests/BazelRulesTests/RulesConfigTests.swift | 4 + spm/ConfigurationCondition/Package.swift | 24 ++++ .../ConfigurationCondition.swift | 13 ++ .../ConfigurationConditionTests.swift | 13 ++ .../DefaultDependency/Package.swift | 16 +++ .../DefaultDependency/DefaultDependency.swift | 5 + spm/TraitGraph/EmptyDependency/Package.swift | 16 +++ .../EmptyDependency/EmptyDependency.swift | 5 + .../ExplicitDependency/Package.swift | 18 +++ .../ExplicitDependency.swift | 13 ++ spm/TraitGraph/Package.swift | 39 ++++++ .../Sources/TraitGraph/TraitGraph.swift | 40 ++++++ spm/TraitGraph/Sources/TraitProbe/main.swift | 19 +++ .../TraitGraphTests/TraitGraphTests.swift | 38 ++++++ 18 files changed, 390 insertions(+), 28 deletions(-) create mode 100644 spm/ConfigurationCondition/Package.swift create mode 100644 spm/ConfigurationCondition/Sources/ConfigurationCondition/ConfigurationCondition.swift create mode 100644 spm/ConfigurationCondition/Tests/ConfigurationConditionTests/ConfigurationConditionTests.swift create mode 100644 spm/TraitGraph/DefaultDependency/Package.swift create mode 100644 spm/TraitGraph/DefaultDependency/Sources/DefaultDependency/DefaultDependency.swift create mode 100644 spm/TraitGraph/EmptyDependency/Package.swift create mode 100644 spm/TraitGraph/EmptyDependency/Sources/EmptyDependency/EmptyDependency.swift create mode 100644 spm/TraitGraph/ExplicitDependency/Package.swift create mode 100644 spm/TraitGraph/ExplicitDependency/Sources/ExplicitDependency/ExplicitDependency.swift create mode 100644 spm/TraitGraph/Package.swift create mode 100644 spm/TraitGraph/Sources/TraitGraph/TraitGraph.swift create mode 100644 spm/TraitGraph/Sources/TraitProbe/main.swift create mode 100644 spm/TraitGraph/Tests/TraitGraphTests/TraitGraphTests.swift diff --git a/Sources/BazelRules/Rules+Builtin.swift b/Sources/BazelRules/Rules+Builtin.swift index 86d401a..ee92327 100644 --- a/Sources/BazelRules/Rules+Builtin.swift +++ b/Sources/BazelRules/Rules+Builtin.swift @@ -12,12 +12,16 @@ extension Rules { extension Rules.Builtin.Call { public static func config_setting( name: String, + values: [String: String]? = nil, flag_values: [String: String]? = nil, visibility: Starlark.Statement.Argument.Visibility? = nil) -> Starlark.Statement.Call { .init("config_setting") { "name" => name + if let values { + "values" => values + } if let flag_values { "flag_values" => flag_values } diff --git a/Sources/BazelRules/Rules+Selects.swift b/Sources/BazelRules/Rules+Selects.swift index 850b998..6414498 100644 --- a/Sources/BazelRules/Rules+Selects.swift +++ b/Sources/BazelRules/Rules+Selects.swift @@ -26,14 +26,20 @@ extension Rules { extension Rules.Selects { public enum Call { - /// A `config_setting` that holds when any of the given ones does. + /// A `config_setting` that holds when any or all of the given settings do. public static func config_setting_group( name: String, - match_any: [String]) -> Starlark.Statement.Call + match_any: [String]? = nil, + match_all: [String]? = nil) -> Starlark.Statement.Call { .init("selects.config_setting_group") { "name" => name - "match_any" => match_any.map { Starlark.Label.named($0) } + if let matchAny = match_any { + "match_any" => matchAny.map { Starlark.Label.named($0) } + } + if let matchAll = match_all { + "match_all" => matchAll.map { Starlark.Label.named($0) } + } } } } diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Trait.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Trait.swift index 80968df..ef542fb 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Trait.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Trait.swift @@ -21,6 +21,8 @@ extension SwiftPM.Generator { let config: String let package: String let trait: String + /// The traits selecting this one enables, including itself. + let selection: Set /// What the graph resolves the trait to: a package's default traits, or /// what a dependent asked for by name. let isDefault: Bool @@ -34,6 +36,20 @@ extension SwiftPM.Generator { var traitFlags: [TraitFlag] { workspace.packages.flatMap { package -> [TraitFlag] in let enabled = workspace.traits[package.identity] ?? [] + let traits = Dictionary( + package.manifest.traits.map { ($0.name, $0) }, + uniquingKeysWith: { first, _ in first }) + + func selection(of name: String) -> Set { + var selected: Set = [] + var pending = [name] + while let next = pending.popLast() { + guard selected.insert(next).inserted else { continue } + pending += traits[next]?.enabledTraits ?? [] + } + selected.remove("default") + return selected + } return package.manifest.traits /// Not a trait: the list of traits a build that says nothing gets. @@ -45,6 +61,7 @@ extension SwiftPM.Generator { config: "\(package.directory).\(trait.name)", package: package.directory, trait: trait.name, + selection: selection(of: trait.name), isDefault: enabled.contains(trait.name)) } } @@ -81,6 +98,42 @@ extension SwiftPM.Generator { return "//\(PluginSwiftPM.packagesDirectory):\(group)" } + /// The label a setting condition selects on. Trait names are alternatives + /// within the trait condition; a build configuration must also match when + /// the manifest names both. + func settingCondition( + _ condition: SwiftPM.SettingCondition?, + in package: SwiftPM.Package) -> String? + { + guard let condition else { return nil } + + let trait = traitCondition(condition.traits, in: package) + let configuration = configurationCondition(condition.config) + switch (trait, configuration) { + case (nil, nil): + return nil + case (.some(let label), nil), (nil, .some(let label)): + return label + case (.some(let trait), .some(let configuration)): + let names = condition.traits.sorted() + [condition.config?.lowercased() ?? ""] + let group = "condition_\(Self.identifier(package.directory))_" + + names.map(Self.identifier).joined(separator: "_and_") + conditionGroups[group] = [trait, configuration] + return "//\(PluginSwiftPM.packagesDirectory):\(group)" + } + } + + private func configurationCondition(_ configuration: String?) -> String? { + guard let configuration = configuration?.lowercased(), + configuration == "debug" || configuration == "release" + else { + return nil + } + + configurationConditions.insert(configuration) + return "//\(PluginSwiftPM.packagesDirectory):swiftpm_\(configuration)" + } + /// A list of flags or labels, plus one `select` per trait condition. /// /// One `select` each rather than one with several keys: two traits can be @@ -105,46 +158,77 @@ extension SwiftPM.Generator { return .custom(parts.joined(separator: " + ")) } - /// The flags, their settings, and the groups a condition asked for. + /// The flags, configuration settings, and groups a condition asked for. /// /// They live under `Packages/` because that is the package the generator /// owns: the root `BUILD` belongs to the project. func buildTraitRules(_ builder: CodeBuilder) { let flags = traitFlags - guard !flags.isEmpty else { return } + if !flags.isEmpty { + builder.load(.bool_flag) + for flag in flags { + builder.call( + Rules.Config.Call.bool_flag( + name: flag.name, + build_setting_default: flag.isDefault, + visibility: .public)) + builder.call( + Rules.Builtin.Call.config_setting( + name: "\(flag.name)_on", + flag_values: [":\(flag.name)": "true"])) + } + } - builder.load(.bool_flag) - for flag in flags { + if configurationConditions.contains("debug") { builder.call( - Rules.Config.Call.bool_flag( - name: flag.name, - build_setting_default: flag.isDefault, - visibility: .public)) + Rules.Builtin.Call.config_setting( + name: "swiftpm_debug_dbg", + values: ["compilation_mode": "dbg"])) builder.call( Rules.Builtin.Call.config_setting( - name: "\(flag.name)_on", - flag_values: [":\(flag.name)": "true"])) + name: "swiftpm_debug_fastbuild", + values: ["compilation_mode": "fastbuild"])) + } + if configurationConditions.contains("release") { + builder.call( + Rules.Builtin.Call.config_setting( + name: "swiftpm_release", + values: ["compilation_mode": "opt"])) } - guard !traitGroups.isEmpty else { return } + let hasGroups = !traitGroups.isEmpty + || !conditionGroups.isEmpty + || configurationConditions.contains("debug") + guard hasGroups else { return } builder.load(loadableRule: Rules.Selects.selects) + if configurationConditions.contains("debug") { + builder.call( + Rules.Selects.Call.config_setting_group( + name: "swiftpm_debug", + match_any: [":swiftpm_debug_dbg", ":swiftpm_debug_fastbuild"])) + } for group in traitGroups.keys.sorted() { builder.call( Rules.Selects.Call.config_setting_group( name: group, match_any: (traitGroups[group] ?? []).map { ":\($0)" })) } + for group in conditionGroups.keys.sorted() { + builder.call( + Rules.Selects.Call.config_setting_group( + name: group, + match_all: conditionGroups[group] ?? [])) + } } /// `--config=.` for every trait a package declares. /// - /// A configuration is how a Bazel workspace is told what to build, so it is - /// how a trait is asked for too. Only asked for: a trait adds — it defines - /// its own name and pulls in what is behind it — so there is nothing to - /// name for turning one off. A trait the manifests turn on is on already, - /// and the flag underneath takes `=false` for the rare build that wants it - /// without. + /// SwiftPM treats an explicit trait selection as a replacement for that + /// package's defaults. Each configuration therefore writes every flag for + /// the package, turning on only the selected trait and its transitive + /// `enabledTraits`. Multiple selections remain available through the + /// underlying boolean flags. /// /// The file is always written, because a `.bazelrc` that imports a file /// that is not there does not load. @@ -161,10 +245,14 @@ extension SwiftPM.Generator { lines.append("# No package in this workspace declares a trait.") } - for flag in flags { + for selected in flags { lines.append("") - lines.append("# \(flag.package): \(flag.trait)\(flag.isDefault ? ", on by default" : "")") - lines.append("build:\(flag.config) --//\(PluginSwiftPM.packagesDirectory):\(flag.name)=true") + lines.append("# \(selected.package): \(selected.trait)\(selected.isDefault ? ", on by default" : "")") + for flag in flags where flag.package == selected.package { + lines.append( + "build:\(selected.config) --//\(PluginSwiftPM.packagesDirectory):\(flag.name)=" + + (selected.selection.contains(flag.trait) ? "true" : "false")) + } } try (output + "traits.bazelrc").write(lines.joined(separator: "\n") + "\n") diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Workspace.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Workspace.swift index 1262c06..dbb6e46 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Workspace.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Workspace.swift @@ -137,9 +137,9 @@ extension SwiftPM { /// The traits each package is built with, by identity. /// /// A package gets its own default traits unless something that depends on it - /// names traits instead — naming them replaces the defaults, which is why a - /// manifest that wants both says so. A trait can enable further traits, so - /// the set is closed over that. + /// names a selection instead. An explicit empty selection disables defaults; + /// `.defaults` is encoded as the trait named `default`. A trait can enable + /// further traits, so the set is closed over that. static func enabledTraits( of manifests: [(identity: String, manifest: Manifest)], directoryByIdentity: [String: String]) -> [String: Set] @@ -152,8 +152,9 @@ extension SwiftPM { var requested: [String: Set] = [:] for entry in manifests { - for dependency in entry.manifest.dependencies where !dependency.traits.isEmpty { - requested[identity(of: dependency), default: []].formUnion(dependency.traits) + for dependency in entry.manifest.dependencies { + let dependencyIdentity = identity(of: dependency) + requested[dependencyIdentity, default: []].formUnion(dependency.traits) } } diff --git a/Tests/BazelRulesTests/RulesConfigTests.swift b/Tests/BazelRulesTests/RulesConfigTests.swift index 3aaab65..7997fcd 100644 --- a/Tests/BazelRulesTests/RulesConfigTests.swift +++ b/Tests/BazelRulesTests/RulesConfigTests.swift @@ -80,6 +80,7 @@ struct RulesConfigTests { func testBuiltinConfigSettingCall() { let call = Rules.Builtin.Call.config_setting( name: "Debug", + values: ["compilation_mode": "dbg"], flag_values: [":mode": "Debug"]) #expect( @@ -87,6 +88,9 @@ struct RulesConfigTests { == """ config_setting( name = "Debug", + values = { + "compilation_mode": "dbg" + }, flag_values = { ":mode": "Debug" }, diff --git a/spm/ConfigurationCondition/Package.swift b/spm/ConfigurationCondition/Package.swift new file mode 100644 index 0000000..e022069 --- /dev/null +++ b/spm/ConfigurationCondition/Package.swift @@ -0,0 +1,24 @@ +// swift-tools-version: 6.1 + +import PackageDescription + +/// Settings conditional on SwiftPM's debug and release build configurations. +let configurationSettings: [SwiftSetting] = [ + .define("DEBUG_ONLY", .when(configuration: .debug)), + .define("RELEASE_ONLY", .when(configuration: .release)), +] + +let package = Package( + name: "ConfigurationCondition", + products: [ + .library(name: "ConfigurationCondition", targets: ["ConfigurationCondition"]), + ], + targets: [ + .target( + name: "ConfigurationCondition", + swiftSettings: configurationSettings), + .testTarget( + name: "ConfigurationConditionTests", + dependencies: ["ConfigurationCondition"], + swiftSettings: configurationSettings), + ]) diff --git a/spm/ConfigurationCondition/Sources/ConfigurationCondition/ConfigurationCondition.swift b/spm/ConfigurationCondition/Sources/ConfigurationCondition/ConfigurationCondition.swift new file mode 100644 index 0000000..bcd9e01 --- /dev/null +++ b/spm/ConfigurationCondition/Sources/ConfigurationCondition/ConfigurationCondition.swift @@ -0,0 +1,13 @@ +public enum ConfigurationCondition { + public static var name: String { + #if DEBUG_ONLY && RELEASE_ONLY + #error("debug and release settings must be mutually exclusive") + #elseif DEBUG_ONLY + return "debug" + #elseif RELEASE_ONLY + return "release" + #else + #error("one build configuration must be selected") + #endif + } +} diff --git a/spm/ConfigurationCondition/Tests/ConfigurationConditionTests/ConfigurationConditionTests.swift b/spm/ConfigurationCondition/Tests/ConfigurationConditionTests/ConfigurationConditionTests.swift new file mode 100644 index 0000000..566eeba --- /dev/null +++ b/spm/ConfigurationCondition/Tests/ConfigurationConditionTests/ConfigurationConditionTests.swift @@ -0,0 +1,13 @@ +import ConfigurationCondition +import Testing + +@Test +func onlyTheCurrentBuildConfigurationsSettingApplies() { + #if DEBUG_ONLY + #expect(ConfigurationCondition.name == "debug") + #elseif RELEASE_ONLY + #expect(ConfigurationCondition.name == "release") + #else + #error("one build configuration must be selected") + #endif +} diff --git a/spm/TraitGraph/DefaultDependency/Package.swift b/spm/TraitGraph/DefaultDependency/Package.swift new file mode 100644 index 0000000..92253c7 --- /dev/null +++ b/spm/TraitGraph/DefaultDependency/Package.swift @@ -0,0 +1,16 @@ +// swift-tools-version: 6.1 + +import PackageDescription + +let package = Package( + name: "DefaultDependency", + products: [ + .library(name: "DefaultDependency", targets: ["DefaultDependency"]), + ], + traits: [ + .trait(name: "DefaultOn"), + .default(enabledTraits: ["DefaultOn"]), + ], + targets: [ + .target(name: "DefaultDependency"), + ]) diff --git a/spm/TraitGraph/DefaultDependency/Sources/DefaultDependency/DefaultDependency.swift b/spm/TraitGraph/DefaultDependency/Sources/DefaultDependency/DefaultDependency.swift new file mode 100644 index 0000000..2617f56 --- /dev/null +++ b/spm/TraitGraph/DefaultDependency/Sources/DefaultDependency/DefaultDependency.swift @@ -0,0 +1,5 @@ +#if DefaultOn +public let defaultDependencyEnabled = true +#else +public let defaultDependencyEnabled = false +#endif diff --git a/spm/TraitGraph/EmptyDependency/Package.swift b/spm/TraitGraph/EmptyDependency/Package.swift new file mode 100644 index 0000000..f6f38b0 --- /dev/null +++ b/spm/TraitGraph/EmptyDependency/Package.swift @@ -0,0 +1,16 @@ +// swift-tools-version: 6.1 + +import PackageDescription + +let package = Package( + name: "EmptyDependency", + products: [ + .library(name: "EmptyDependency", targets: ["EmptyDependency"]), + ], + traits: [ + .trait(name: "DefaultOn"), + .default(enabledTraits: ["DefaultOn"]), + ], + targets: [ + .target(name: "EmptyDependency"), + ]) diff --git a/spm/TraitGraph/EmptyDependency/Sources/EmptyDependency/EmptyDependency.swift b/spm/TraitGraph/EmptyDependency/Sources/EmptyDependency/EmptyDependency.swift new file mode 100644 index 0000000..e20d913 --- /dev/null +++ b/spm/TraitGraph/EmptyDependency/Sources/EmptyDependency/EmptyDependency.swift @@ -0,0 +1,5 @@ +#if DefaultOn +public let emptyDependencyEnabled = true +#else +public let emptyDependencyEnabled = false +#endif diff --git a/spm/TraitGraph/ExplicitDependency/Package.swift b/spm/TraitGraph/ExplicitDependency/Package.swift new file mode 100644 index 0000000..87052f9 --- /dev/null +++ b/spm/TraitGraph/ExplicitDependency/Package.swift @@ -0,0 +1,18 @@ +// swift-tools-version: 6.1 + +import PackageDescription + +let package = Package( + name: "ExplicitDependency", + products: [ + .library(name: "ExplicitDependency", targets: ["ExplicitDependency"]), + ], + traits: [ + .trait(name: "Leaf"), + .trait(name: "Middle", enabledTraits: ["Leaf"]), + .trait(name: "Top", enabledTraits: ["Middle"]), + .default(enabledTraits: ["Leaf"]), + ], + targets: [ + .target(name: "ExplicitDependency"), + ]) diff --git a/spm/TraitGraph/ExplicitDependency/Sources/ExplicitDependency/ExplicitDependency.swift b/spm/TraitGraph/ExplicitDependency/Sources/ExplicitDependency/ExplicitDependency.swift new file mode 100644 index 0000000..525f01b --- /dev/null +++ b/spm/TraitGraph/ExplicitDependency/Sources/ExplicitDependency/ExplicitDependency.swift @@ -0,0 +1,13 @@ +public let explicitDependencyTraits: Set = { + var traits: Set = [] + #if Leaf + traits.insert("Leaf") + #endif + #if Middle + traits.insert("Middle") + #endif + #if Top + traits.insert("Top") + #endif + return traits +}() diff --git a/spm/TraitGraph/Package.swift b/spm/TraitGraph/Package.swift new file mode 100644 index 0000000..9ffdb62 --- /dev/null +++ b/spm/TraitGraph/Package.swift @@ -0,0 +1,39 @@ +// swift-tools-version: 6.1 + +import PackageDescription + +let package = Package( + name: "TraitGraph", + products: [ + .library(name: "TraitGraph", targets: ["TraitGraph"]), + ], + traits: [ + .trait(name: "Leaf"), + .trait(name: "Middle", enabledTraits: ["Leaf"]), + .trait(name: "Top", enabledTraits: ["Middle"]), + .trait(name: "Alternative"), + .default(enabledTraits: ["Top"]), + ], + dependencies: [ + .package(path: "DefaultDependency", traits: [.defaults]), + .package(path: "EmptyDependency", traits: []), + .package(path: "ExplicitDependency", traits: ["Top"]), + ], + targets: [ + .target( + name: "TraitGraph", + dependencies: [ + .product(name: "DefaultDependency", package: "DefaultDependency"), + .product(name: "EmptyDependency", package: "EmptyDependency"), + .product(name: "ExplicitDependency", package: "ExplicitDependency"), + ], + swiftSettings: [ + .define("ANY_BRANCH", .when(traits: ["Middle", "Alternative"])), + ]), + .executableTarget( + name: "TraitProbe", + dependencies: ["TraitGraph"]), + .testTarget( + name: "TraitGraphTests", + dependencies: ["TraitGraph"]), + ]) diff --git a/spm/TraitGraph/Sources/TraitGraph/TraitGraph.swift b/spm/TraitGraph/Sources/TraitGraph/TraitGraph.swift new file mode 100644 index 0000000..0e82795 --- /dev/null +++ b/spm/TraitGraph/Sources/TraitGraph/TraitGraph.swift @@ -0,0 +1,40 @@ +import DefaultDependency +import EmptyDependency +import ExplicitDependency + +public struct TraitState: Equatable, Sendable { + public let root: Set + public let anyBranch: Bool + public let dependencyDefaults: Bool + public let emptyDependency: Bool + public let explicitDependency: Set +} + +public func traitState() -> TraitState { + var root: Set = [] + #if Leaf + root.insert("Leaf") + #endif + #if Middle + root.insert("Middle") + #endif + #if Top + root.insert("Top") + #endif + #if Alternative + root.insert("Alternative") + #endif + + return TraitState( + root: root, + anyBranch: { + #if ANY_BRANCH + true + #else + false + #endif + }(), + dependencyDefaults: defaultDependencyEnabled, + emptyDependency: emptyDependencyEnabled, + explicitDependency: explicitDependencyTraits) +} diff --git a/spm/TraitGraph/Sources/TraitProbe/main.swift b/spm/TraitGraph/Sources/TraitProbe/main.swift new file mode 100644 index 0000000..3a7a3f6 --- /dev/null +++ b/spm/TraitGraph/Sources/TraitProbe/main.swift @@ -0,0 +1,19 @@ +import Foundation +import TraitGraph + +let expected = Set(CommandLine.arguments.dropFirst()) +let state = traitState() + +guard state.root == expected else { + fatalError("expected \(expected.sorted()), got \(state.root.sorted())") +} +guard state.anyBranch == (expected.contains("Middle") || expected.contains("Alternative")) else { + fatalError("multi-trait condition did not follow the selected traits") +} +guard state.dependencyDefaults, !state.emptyDependency, + state.explicitDependency == ["Leaf", "Middle", "Top"] +else { + fatalError("dependency trait policy was not preserved") +} + +print(state.root.sorted().joined(separator: ",")) diff --git a/spm/TraitGraph/Tests/TraitGraphTests/TraitGraphTests.swift b/spm/TraitGraph/Tests/TraitGraphTests/TraitGraphTests.swift new file mode 100644 index 0000000..38f3862 --- /dev/null +++ b/spm/TraitGraph/Tests/TraitGraphTests/TraitGraphTests.swift @@ -0,0 +1,38 @@ +import Testing +import TraitGraph + +/// Whatever selection the build was made with, these hold: a trait that enables +/// another is on with it, a condition naming several traits is on when any of +/// them is, and what a dependency's traits were is the dependency's manifest +/// and this one's selection for it — never this package's own selection. +@Test +func theTraitGraphIsClosedOverWhatEachTraitEnables() { + let state = traitState() + + if state.root.contains("Top") { + #expect(state.root.contains("Middle")) + } + if state.root.contains("Middle") { + #expect(state.root.contains("Leaf")) + } +} + +@Test +func aConditionOnSeveralTraitsIsOnWhenAnyOfThemIs() { + let state = traitState() + + #expect(state.anyBranch == (state.root.contains("Middle") || state.root.contains("Alternative"))) +} + +@Test +func eachDependencyGetsTheTraitsItWasAskedFor() { + let state = traitState() + + /// `.defaults`: the dependency's own default traits. + #expect(state.dependencyDefaults) + /// `traits: []`: no trait at all, defaults included. + #expect(!state.emptyDependency) + /// `traits: ["Top"]`: that trait and everything it enables, and nothing of + /// the dependency's own defaults beyond it. + #expect(state.explicitDependency == ["Leaf", "Middle", "Top"]) +} From 6b929336e6cc1ec7d0134f4c190aff4cdae68620 Mon Sep 17 00:00:00 2001 From: yume190 Date: Tue, 22 Sep 2026 11:19:02 +0800 Subject: [PATCH 07/47] feat(spm): run the program an artifact bundle ships A binary target is not only an XCFramework: an artifact bundle holds one build of a program per platform. The variant this machine can run becomes a native_binary, and a plugin that names such a target as its tool is handed that program rather than asked to build one. --- Sources/BazelRules/Rules+Native.swift | 64 ++++++++ .../BazelizeKit/SwiftPM/SwiftPM+Binary.swift | 155 +++++++++++++++--- .../SwiftPM/SwiftPM+PluginHost.swift | 32 +++- Tests/BazelRulesTests/RulesNativeTests.swift | 42 +++++ .../GreetTool-1.0.0-macos/bin/GreetTool | 10 ++ .../GreetTool.artifactbundle/info.json | 15 ++ spm/ArtifactBundle/Package.swift | 26 +++ .../Plugins/GenerateGreeting/Plugin.swift | 18 ++ .../ArtifactBundle/ArtifactBundle.swift | 5 + .../ArtifactBundleTests.swift | 7 + 10 files changed, 342 insertions(+), 32 deletions(-) create mode 100644 Sources/BazelRules/Rules+Native.swift create mode 100644 Tests/BazelRulesTests/RulesNativeTests.swift create mode 100755 spm/ArtifactBundle/GreetTool.artifactbundle/GreetTool-1.0.0-macos/bin/GreetTool create mode 100644 spm/ArtifactBundle/GreetTool.artifactbundle/info.json create mode 100644 spm/ArtifactBundle/Package.swift create mode 100644 spm/ArtifactBundle/Plugins/GenerateGreeting/Plugin.swift create mode 100644 spm/ArtifactBundle/Sources/ArtifactBundle/ArtifactBundle.swift create mode 100644 spm/ArtifactBundle/Tests/ArtifactBundleTests/ArtifactBundleTests.swift diff --git a/Sources/BazelRules/Rules+Native.swift b/Sources/BazelRules/Rules+Native.swift new file mode 100644 index 0000000..157901c --- /dev/null +++ b/Sources/BazelRules/Rules+Native.swift @@ -0,0 +1,64 @@ +// +// Rules+Native.swift +// +// +// The rule a prebuilt program is run through. +// + +import Foundation +import Starlark + +// MARK: - Rules.Native + +extension Rules { + /// https://github.com/bazelbuild/bazel-skylib/blob/main/docs/native_binary_doc.md + public enum Native: String, LoadableRule { + public var module: String { + "@bazel_skylib//rules:native_binary.bzl" + } + + case native_binary + } +} + +// MARK: - Rules.Native.Call + +extension Rules.Native { + public enum Call { + /// Wraps an already-built executable as a runnable target. + /// + /// Parameters: + /// - `name: String` + /// The Bazel target name. + /// - `src: String` + /// The prebuilt executable. + /// - `out: String` + /// What the executable is called in the output tree. + /// - `data: Starlark.Value?` + /// What the program needs beside it at run time. + public static func native_binary( + name: String, + src: String, + out: String, + data: Starlark.Value? = nil, + tags: [String] = [], + visibility: Starlark.Statement.Argument.Visibility? = nil) + -> Starlark.Statement.Call + { + Rules.Native.native_binary.call { + "name" => name + "src" => Starlark.Label.named(src) + "out" => out + if let data { + "data" => data + } + if !tags.isEmpty { + "tags" => tags + } + if let visibility { + visibility + } + } + } + } +} diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Binary.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Binary.swift index a6be5ba..c4d71df 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Binary.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Binary.swift @@ -11,19 +11,65 @@ import Foundation import Starlark import Util +extension SwiftPM { + /// What a binary target ships. + struct BinaryArtifact { + enum Kind { + /// A framework to link against. + case xcframework + /// A program to run, one build per platform. + case artifactBundle + + init?(extension: String?) { + switch `extension` { + case "xcframework": + self = .xcframework + case "artifactbundle": + self = .artifactBundle + default: + return nil + } + } + } + + let path: Path + let kind: Kind + } + + /// `info.json`: what an artifact bundle says it holds. + struct ArtifactBundleInfo: Decodable { + struct Artifact: Decodable { + struct Variant: Decodable { + /// Where the program is, inside the bundle. + let path: String + /// The triples it was built for; absent means anywhere. + let supportedTriples: [String]? + } + + /// `executable`, the only kind a bundle can hold today. + let type: String + let variants: [Variant] + } + + let artifacts: [String: Artifact] + } +} + extension SwiftPM.Generator { - /// A binary target is an `.xcframework` SwiftPM already fetched, imported the - /// way a project-owned one is. + /// A binary target is what SwiftPM already fetched: an `.xcframework`, + /// imported the way a project-owned one is, or an `.artifactbundle`, whose + /// executable is run rather than linked. /// - /// Whether it links statically or dynamically is not in the manifest, so the - /// binary itself is read: an archive is static, a Mach-O dylib is not. + /// Whether an XCFramework links statically or dynamically is not in the + /// manifest, so the binary itself is read: an archive is static, a Mach-O + /// dylib is not. func buildBinary( _ target: SwiftPM.PackageTarget, in package: SwiftPM.Package, root: Path, builder: CodeBuilder) throws -> Bool { - guard let xcframework = try artifact(of: target, in: package) else { + guard let artifact = artifact(of: target, in: package) else { Log.codeGenerate.warning(""" Skip \(package.directory, privacy: .public)/\(target.name, privacy: .public): \ no artifact for the binary target @@ -31,34 +77,55 @@ extension SwiftPM.Generator { return false } - /// The link keeps the `.xcframework` name: the import rule reads the - /// bundle name out of the path. + /// The link keeps the artifact's name: the import rule reads the bundle + /// name out of the path, and an artifact bundle names its executable + /// relative to its own root. let directory = root + Self.artifactsRoot + target.name - let link = directory + xcframework.lastComponent + let link = directory + artifact.path.lastComponent try directory.mkpath() if link.isSymlink || link.exists { try? link.delete() } - try link.symlink(xcframework) + try link.symlink(artifact.path) + + let contents = "\(Self.artifactsRoot)/\(target.name)/**" + let rule = ruleName(of: target.name, in: package) + + switch artifact.kind { + case .artifactBundle: + guard let executable = Self.executable(inArtifactBundle: artifact.path) else { + Log.codeGenerate.warning(""" + Skip \(package.directory, privacy: .public)/\(target.name, privacy: .public): \ + the artifact bundle has no executable this machine can run + """) + return false + } - let imports = Starlark.glob([ - "\(Self.artifactsRoot)/\(target.name)/**", - ]) + builder.load(loadableRule: Rules.Native.native_binary) + builder.call( + Rules.Native.Call.native_binary( + name: rule, + src: "\(Self.artifactsRoot)/\(target.name)/\(artifact.path.lastComponent)/\(executable)", + out: rule, + data: Starlark.glob([contents]), + tags: Self.manual, + visibility: .public)) - if isStatic(xcframework) { + case .xcframework where isStatic(artifact.path): builder.load(.apple_static_xcframework_import) builder.call( Rules.Apple.General.Call.apple_static_xcframework_import( - name: ruleName(of: target.name, in: package), - xcframework_imports: imports, + name: rule, + xcframework_imports: Starlark.glob([contents]), tags: Self.manual, visibility: .public)) - } else { + + case .xcframework: builder.load(.apple_dynamic_xcframework_import) builder.call( Rules.Apple.General.Call.apple_dynamic_xcframework_import( - name: ruleName(of: target.name, in: package), - xcframework_imports: imports, + name: rule, + xcframework_imports: Starlark.glob([contents]), tags: Self.manual, visibility: .public)) } @@ -68,9 +135,10 @@ extension SwiftPM.Generator { static let artifactsRoot = "Artifacts" - /// Where the artifact ended up: a remote one was downloaded and unpacked into - /// the workspace's artifact directory, a local one is a path in the package. - private func artifact(of target: SwiftPM.PackageTarget, in package: SwiftPM.Package) throws -> Path? { + /// What a binary target ships, and where it ended up: a remote artifact was + /// downloaded and unpacked into the workspace's artifact directory, a local + /// one is a path in the package. + func artifact(of target: SwiftPM.PackageTarget, in package: SwiftPM.Package) -> SwiftPM.BinaryArtifact? { var roots: [Path] = [workspace.artifacts + package.identity + target.name] if let path = target.path { @@ -78,18 +146,57 @@ extension SwiftPM.Generator { } for root in roots { - if root.extension == "xcframework", root.exists { return root } + if let kind = SwiftPM.BinaryArtifact.Kind(extension: root.extension), root.exists { + return .init(path: root, kind: kind) + } guard root.isDirectory else { continue } let children = (try? root.children()) ?? [] - if let xcframework = children.first(where: { $0.extension == "xcframework" }) { - return xcframework + for child in children.sorted(by: { $0.lastComponent < $1.lastComponent }) { + guard let kind = SwiftPM.BinaryArtifact.Kind(extension: child.extension) else { continue } + return .init(path: child, kind: kind) } } return nil } + /// The executable of an artifact bundle, as a path inside the bundle. + /// + /// A bundle ships one variant per platform and names the triples each was + /// built for, so the one this machine can run is the one whose triples name + /// its architecture; a bundle that names none is taken at its word. + static func executable(inArtifactBundle bundle: Path) -> String? { + guard + let data = try? Data(contentsOf: (bundle + "info.json").url), + let info = try? JSONDecoder().decode(SwiftPM.ArtifactBundleInfo.self, from: data) + else { + return nil + } + + var variants: [SwiftPM.ArtifactBundleInfo.Artifact.Variant] = [] + for name in info.artifacts.keys.sorted() { + guard let artifact = info.artifacts[name], artifact.type == "executable" else { continue } + variants += artifact.variants + } + + let host = Self.hostTriple + return variants.first { variant in + guard let triples = variant.supportedTriples else { return true } + return triples.contains { $0.hasPrefix(host) } + }?.path + } + + /// `-apple-macos`, which is how an artifact bundle spells the machine + /// this runs on — `macosx` and a version both start with it. + private static var hostTriple: String { + #if arch(arm64) + "arm64-apple-macos" + #else + "x86_64-apple-macos" + #endif + } + /// Whether the framework in the slice links statically, read from the binary /// itself: the manifest does not say, and a static archive handed to the /// dynamic import rule fails in the bitcode stripper. diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginHost.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginHost.swift index 8a3b490..c9fd585 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginHost.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginHost.swift @@ -231,19 +231,35 @@ extension SwiftPM.Generator { pluginGeneratedResources: []) } - /// The program a plugin runs, built by SwiftPM because it is an ordinary - /// executable target with ordinary dependencies. + /// The program a plugin runs. /// - /// Only this one product is built, rather than the target the plugin is - /// attached to: which product holds the tool is read from the manifest here - /// instead of guessed from the target's name, which is what some toolchains - /// get wrong. + /// An executable target is built by SwiftPM because it is an ordinary + /// target with ordinary dependencies; only that one product is built, + /// rather than the target the plugin is attached to, and which product + /// holds the tool is read from the manifest here instead of guessed from + /// the target's name, which is what some toolchains get wrong. + /// + /// A binary target is already a program: the artifact bundle holds one + /// build per platform, and the one for this machine is run as it is. private func tool(named name: String, in package: SwiftPM.Package) async throws -> Path? { - guard package.manifest.targets.contains(where: { $0.name == name && $0.type == "executable" }) - else { + guard let target = package.manifest.targets.first(where: { $0.name == name }) else { return nil } + if target.type == "binary" { + guard + let artifact = artifact(of: target, in: package), + case .artifactBundle = artifact.kind, + let executable = Self.executable(inArtifactBundle: artifact.path) + else { + return nil + } + + return artifact.path + executable + } + + guard target.type == "executable" else { return nil } + /// Bazel built it, so SwiftPM never has to load the package the tool /// lives in — which some toolchains cannot do when a plugin names its /// tool by target. diff --git a/Tests/BazelRulesTests/RulesNativeTests.swift b/Tests/BazelRulesTests/RulesNativeTests.swift new file mode 100644 index 0000000..c48be73 --- /dev/null +++ b/Tests/BazelRulesTests/RulesNativeTests.swift @@ -0,0 +1,42 @@ +import Starlark +import Testing +@testable import BazelRules + +struct RulesNativeTests { + @Test + func testNativeModule() { + #expect( + Rules.Native.native_binary.module + == "@bazel_skylib//rules:native_binary.bzl") + } + + @Test + func testNativeBinaryTypedCall() { + let call = Rules.Native.Call.native_binary( + name: "GreetTool", + src: "Artifacts/GreetTool/GreetTool.artifactbundle/bin/GreetTool", + out: "GreetTool", + data: Starlark.glob(["Artifacts/GreetTool/**"]), + tags: ["manual"], + visibility: .public) + + #expect( + call.text + == """ + native_binary( + name = "GreetTool", + src = "Artifacts/GreetTool/GreetTool.artifactbundle/bin/GreetTool", + out = "GreetTool", + data = glob([ + "Artifacts/GreetTool/**", + ]), + tags = [ + "manual", + ], + visibility = [ + "//visibility:public", + ], + ) + """) + } +} diff --git a/spm/ArtifactBundle/GreetTool.artifactbundle/GreetTool-1.0.0-macos/bin/GreetTool b/spm/ArtifactBundle/GreetTool.artifactbundle/GreetTool-1.0.0-macos/bin/GreetTool new file mode 100755 index 0000000..a052060 --- /dev/null +++ b/spm/ArtifactBundle/GreetTool.artifactbundle/GreetTool-1.0.0-macos/bin/GreetTool @@ -0,0 +1,10 @@ +#!/bin/sh +# +# The program the artifact bundle ships: it writes the source its caller names. +set -eu + +output="$1" +mkdir -p "$(dirname "$output")" +cat > "$output" <<'SWIFT' +public let greeting = "hello from an artifact bundle" +SWIFT diff --git a/spm/ArtifactBundle/GreetTool.artifactbundle/info.json b/spm/ArtifactBundle/GreetTool.artifactbundle/info.json new file mode 100644 index 0000000..35098e6 --- /dev/null +++ b/spm/ArtifactBundle/GreetTool.artifactbundle/info.json @@ -0,0 +1,15 @@ +{ + "schemaVersion": "1.0", + "artifacts": { + "GreetTool": { + "version": "1.0.0", + "type": "executable", + "variants": [ + { + "path": "GreetTool-1.0.0-macos/bin/GreetTool", + "supportedTriples": ["arm64-apple-macosx", "x86_64-apple-macosx"] + } + ] + } + } +} diff --git a/spm/ArtifactBundle/Package.swift b/spm/ArtifactBundle/Package.swift new file mode 100644 index 0000000..154ee51 --- /dev/null +++ b/spm/ArtifactBundle/Package.swift @@ -0,0 +1,26 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +/// A binary target that ships a program rather than a framework: an artifact +/// bundle, holding one build per platform, run by a build tool plugin. +let package = Package( + name: "ArtifactBundle", + products: [ + .library(name: "ArtifactBundle", targets: ["ArtifactBundle"]), + ], + targets: [ + .binaryTarget( + name: "GreetTool", + path: "GreetTool.artifactbundle"), + .plugin( + name: "GenerateGreeting", + capability: .buildTool(), + dependencies: ["GreetTool"]), + .target( + name: "ArtifactBundle", + plugins: ["GenerateGreeting"]), + .testTarget( + name: "ArtifactBundleTests", + dependencies: ["ArtifactBundle"]), + ]) diff --git a/spm/ArtifactBundle/Plugins/GenerateGreeting/Plugin.swift b/spm/ArtifactBundle/Plugins/GenerateGreeting/Plugin.swift new file mode 100644 index 0000000..5713b30 --- /dev/null +++ b/spm/ArtifactBundle/Plugins/GenerateGreeting/Plugin.swift @@ -0,0 +1,18 @@ +import Foundation +import PackagePlugin + +@main +struct GenerateGreeting: BuildToolPlugin { + func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { + let output = context.pluginWorkDirectoryURL.appending(component: "Greeting.generated.swift") + let tool = try context.tool(named: "GreetTool") + + return [ + .buildCommand( + displayName: "Write the greeting", + executable: tool.url, + arguments: [output.path()], + outputFiles: [output]), + ] + } +} diff --git a/spm/ArtifactBundle/Sources/ArtifactBundle/ArtifactBundle.swift b/spm/ArtifactBundle/Sources/ArtifactBundle/ArtifactBundle.swift new file mode 100644 index 0000000..8ebf343 --- /dev/null +++ b/spm/ArtifactBundle/Sources/ArtifactBundle/ArtifactBundle.swift @@ -0,0 +1,5 @@ +/// `greeting` is not here: the plugin's tool — the program the artifact bundle +/// ships — writes it, and this only passes it on. +public func greetingFromTool() -> String { + greeting +} diff --git a/spm/ArtifactBundle/Tests/ArtifactBundleTests/ArtifactBundleTests.swift b/spm/ArtifactBundle/Tests/ArtifactBundleTests/ArtifactBundleTests.swift new file mode 100644 index 0000000..d56814e --- /dev/null +++ b/spm/ArtifactBundle/Tests/ArtifactBundleTests/ArtifactBundleTests.swift @@ -0,0 +1,7 @@ +import ArtifactBundle +import Testing + +@Test +func theBundledProgramGeneratedTheSource() { + #expect(greetingFromTool() == "hello from an artifact bundle") +} From e6ae4092caaaf25663649027d123b97965807f61 Mon Sep 17 00:00:00 2001 From: yume190 Date: Tue, 22 Sep 2026 11:19:07 +0800 Subject: [PATCH 08/47] feat(spm): build a package's snippets, and alias a renamed module A file under Snippets/ is an executable target the manifest never mentions: it is found on disk, linked as main.swift because it is top-level code, and given the package's libraries. moduleAliases renames what is compiled, not what a source says: the aliased module gets a rule of its own under the new name, and the consumer compiles with -module-alias so two packages shipping the same module name can both be used. --- .../SwiftPM/SwiftPM+Executable.swift | 50 +++++ .../SwiftPM/SwiftPM+Generator.swift | 193 ++++++++++++++---- .../SwiftPM/SwiftPM+Manifest.swift | 10 + .../SwiftPM/SwiftPM+Settings.swift | 29 ++- spm/DependencyShape/Other/Package.swift | 14 ++ .../Other/Sources/Core/Core.swift | 1 + spm/DependencyShape/Package.swift | 42 ++++ .../Sources/Consumer/Consumer.swift | 11 + .../Sources/Helper/Helper.swift | 1 + spm/DependencyShape/Sources/Local/Local.swift | 1 + .../DependencyShapeTests.swift | 7 + spm/DependencyShape/vendor-kit/Package.swift | 15 ++ .../Sources/VendorCore/VendorCore.swift | 1 + spm/ProductShapes/Package.swift | 24 +++ spm/ProductShapes/Products/Package.swift | 19 ++ .../Products/Snippets/ProductSnippet.swift | 4 + .../Products/Sources/First/First.swift | 1 + .../Products/Sources/Second/Second.swift | 1 + .../Products/Sources/Tool/main.swift | 1 + .../Sources/ProductShapes/ProductShapes.swift | 8 + .../ProductShapesTests.swift | 7 + 21 files changed, 388 insertions(+), 52 deletions(-) create mode 100644 spm/DependencyShape/Other/Package.swift create mode 100644 spm/DependencyShape/Other/Sources/Core/Core.swift create mode 100644 spm/DependencyShape/Package.swift create mode 100644 spm/DependencyShape/Sources/Consumer/Consumer.swift create mode 100644 spm/DependencyShape/Sources/Helper/Helper.swift create mode 100644 spm/DependencyShape/Sources/Local/Local.swift create mode 100644 spm/DependencyShape/Tests/DependencyShapeTests/DependencyShapeTests.swift create mode 100644 spm/DependencyShape/vendor-kit/Package.swift create mode 100644 spm/DependencyShape/vendor-kit/Sources/VendorCore/VendorCore.swift create mode 100644 spm/ProductShapes/Package.swift create mode 100644 spm/ProductShapes/Products/Package.swift create mode 100644 spm/ProductShapes/Products/Snippets/ProductSnippet.swift create mode 100644 spm/ProductShapes/Products/Sources/First/First.swift create mode 100644 spm/ProductShapes/Products/Sources/Second/Second.swift create mode 100644 spm/ProductShapes/Products/Sources/Tool/main.swift create mode 100644 spm/ProductShapes/Sources/ProductShapes/ProductShapes.swift create mode 100644 spm/ProductShapes/Tests/ProductShapesTests/ProductShapesTests.swift diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Executable.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Executable.swift index 0ac28e8..a509131 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Executable.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Executable.swift @@ -43,6 +43,56 @@ extension SwiftPM.Generator { visibility: .public)) } + /// A file under `Snippets/` is an implicit executable target: SwiftPM + /// builds one program per file, with every library target of the package + /// available to it. The manifest never mentions them, so they are found on + /// disk. + /// + /// The link is called `main.swift` because that is what the file is: a + /// snippet is top-level code, which only the entry point may hold. + func buildSnippets( + in package: SwiftPM.Package, + root: Path, + emitted: [String: TargetKind], + builder: CodeBuilder) throws + { + let directory = package.root + "Snippets" + guard directory.isDirectory else { return } + + let dependencies = emitted.compactMap { name, kind -> String? in + switch kind { + case .swift, .clang, .binary, .system: + return ":\(ruleName(of: name, in: package))" + case .macro, .executable, .test, .unsupported: + return nil + } + }.sorted() + + for source in ((try? directory.children()) ?? []) + .filter({ $0.extension == "swift" }) + .sorted(by: { $0.lastComponent < $1.lastComponent }) + { + let name = source.lastComponentWithoutExtension + let prefix = "\(Self.sourcesRoot)/\(name)" + let destination = root + prefix + try destination.mkpath() + let link = destination + "main.swift" + if link.isSymlink || link.exists { try? link.delete() } + try link.symlink(source) + + builder.load(loadableRule: Rules.Swift.swift_binary) + builder.call( + Rules.Swift.Call.swift_binary( + name: name, + copts: ["-DSWIFT_PACKAGE", "-Xcc", "-DSWIFT_PACKAGE"], + deps: .build { dependencies.map { Starlark.Label.named($0) } }, + module_name: Self.moduleName(name), + srcs: Starlark.glob(["\(prefix)/main.swift"]), + tags: Self.manual, + visibility: .public)) + } + } + /// An executable product is the tool under the name a consumer writes, so it /// aliases the target rather than wrapping it: two `swift_binary` rules over /// the same sources would build the tool twice. diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift index 6ea791d..8539a00 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift @@ -34,6 +34,19 @@ extension SwiftPM { /// by name, written with the flags once every rule is generated. var traitGroups: [String: [String]] = [:] + /// Conditions that require both a trait expression and a build + /// configuration, emitted after every package has registered its use. + var conditionGroups: [String: [String]] = [:] + + /// SwiftPM build configurations used by conditional settings. + var configurationConditions: Set = [] + + /// What a consumer calls another package's module: the aliases asked + /// for, by the package that owns the module and the target inside it. + /// Collected before anything is written, because the rule that carries + /// an alias belongs to the package being aliased. + var moduleAliases: [String: [String: Set]] = [:] + /// What a caller tells the user about: where the build differs from what /// the package asked for, and why. private(set) var notes: [String] = [] @@ -82,6 +95,8 @@ extension SwiftPM { report(pluginsOf: package) } + collectModuleAliases() + for package in workspace.packages { try generate(package) } @@ -250,6 +265,12 @@ extension SwiftPM { continue } } + try buildSnippets( + in: package, + root: root, + emitted: emitted, + builder: builder) + for product in package.manifest.products { build(product, emitted: Set(emitted.keys), package: package, builder: builder) @@ -738,40 +759,60 @@ extension SwiftPM { resources: ResourceBundle?, builder: CodeBuilder) { - builder.load(loadableRule: Rules.Swift.swift_library) - builder.call( - Rules.Swift.Call.swift_library( - name: ruleName(of: target.name, in: package), - /// SwiftPM compiles every package target with the developer - /// search paths, which is how a test-support library finds - /// XCTest. - always_include_developer_search_paths: true, - copts: copts(of: target, in: package), - module_name: Self.moduleName(target.name), - /// Which targets `package` visibility reaches: every target of - /// the same package, which is what the name identifies. - package_name: package.manifest.name, - plugins: plugins(of: target, in: package).nonEmpty.map { macros in - .build { macros } - }, - srcs: Starlark.glob( - matching( - sources(of: target, prefix: prefix, extensions: ["swift"]), - relativeFiles(of: target, in: package, prefix: prefix)) - + generated - + (resources?.accessors ?? []), - exclude: excluded(target, prefix: prefix), - /// The plugin's directory is globbed before anything has - /// written into it: `bazel run //:plugins` does that, and - /// a package that cannot load cannot run it. - allowEmpty: true), - deps: deps(of: target, in: package), - data: resources?.label.map { label in - .build { [Starlark.Label.named(label)] } - }, - linkopts: linkopts(of: target, in: package), - tags: Self.manual, - visibility: .public)) + /// The module under its own name, and once more under each name a + /// consumer aliased it to: aliasing is that consumer's view of the + /// module, and a module is named when it is compiled. + for module in [target.name] + aliases(of: target.name, in: package) { + let isAlias = module != target.name + + builder.load(loadableRule: Rules.Swift.swift_library) + builder.call( + Rules.Swift.Call.swift_library( + name: isAlias + ? Self.aliasRuleName(of: target.name, as: module) + : ruleName(of: target.name, in: package), + /// SwiftPM compiles every package target with the developer + /// search paths, which is how a test-support library finds + /// XCTest. + always_include_developer_search_paths: true, + copts: copts(of: target, in: package), + module_name: Self.moduleName(module), + /// Which targets `package` visibility reaches: every target of + /// the same package, which is what the name identifies. + package_name: package.manifest.name, + plugins: plugins(of: target, in: package).nonEmpty.map { macros in + .build { macros } + }, + srcs: Starlark.glob( + matching( + sources(of: target, prefix: prefix, extensions: ["swift"]), + relativeFiles(of: target, in: package, prefix: prefix)) + + generated + + (resources?.accessors ?? []), + exclude: excluded(target, prefix: prefix), + /// The plugin's directory is globbed before anything has + /// written into it: `bazel run //:plugins` does that, and + /// a package that cannot load cannot run it. + allowEmpty: true), + deps: deps(of: target, in: package), + data: resources?.label.map { label in + .build { [Starlark.Label.named(label)] } + }, + linkopts: linkopts(of: target, in: package), + tags: Self.manual, + visibility: .public)) + } + } + + /// What consumers call this package's module instead of its own name. + private func aliases(of target: String, in package: Package) -> [String] { + (moduleAliases[package.directory]?[target] ?? []).sorted() + } + + /// The rule that compiles a target under an alias: a name of its own, + /// because the alias is often what a product is already called. + static func aliasRuleName(of target: String, as alias: String) -> String { + "\(target)_as_\(alias)" } /// An explicit `sources` list names files or directories; without one the @@ -836,20 +877,30 @@ extension SwiftPM { package.manifest.products.map { ($0.name, $0) }, uniquingKeysWith: { first, _ in first }) - func dependencyLabel(_ dependency: SwiftPM.TargetDependency) -> String? { + func dependencyLabels(_ dependency: SwiftPM.TargetDependency) -> [String] { switch dependency.kind { case .target(let name): - guard localTargets.contains(name), !isMacro(name, in: package) else { return nil } - return ":\(ruleName(of: name, in: package))" + guard localTargets.contains(name), !isMacro(name, in: package) else { return [] } + return [":\(ruleName(of: name, in: package))"] case .byName(let name): if localTargets.contains(name) { - guard !isMacro(name, in: package) else { return nil } - return ":\(ruleName(of: name, in: package))" + guard !isMacro(name, in: package) else { return [] } + return [":\(ruleName(of: name, in: package))"] } - if localProducts[name] != nil { return ":\(name)" } - return label(product: name, package: nil, from: package) + if localProducts[name] != nil { return [":\(name)"] } + return label(product: name, package: nil, from: package).map { [$0] } ?? [] case .product(let name, let packageName): - return label(product: name, package: packageName, from: package) + /// An aliased module is compiled under the name this package + /// calls it, so what is linked is that rule rather than the + /// product the module is part of. + if !dependency.moduleAliases.isEmpty { + return aliasLabels( + of: dependency.moduleAliases, + product: name, + package: packageName, + from: package) + } + return label(product: name, package: packageName, from: package).map { [$0] } ?? [] } } @@ -858,15 +909,16 @@ extension SwiftPM { var byCondition: [String: Set] = [:] for dependency in target.dependencies { - guard let label = dependencyLabel(dependency) else { continue } + let labels = dependencyLabels(dependency) + guard !labels.isEmpty else { continue } guard let condition = traitCondition(dependency.traits, in: package) else { - always.insert(label) + always.formUnion(labels) continue } if byCondition[condition] == nil { conditions.append(condition) } - byCondition[condition, default: []].insert(label) + byCondition[condition, default: []].formUnion(labels) } return traitValue( @@ -893,6 +945,57 @@ extension SwiftPM { return "//\(PluginSwiftPM.packagesDirectory)/\(owner.directory):\(product)" } + /// What an aliasing consumer links: the aliased module of every target + /// the product holds, plus each target it did not rename. + private func aliasLabels( + of aliases: [String: String], + product: String, + package name: String?, + from package: Package) -> [String] + { + guard let owner = self.package(ofProduct: product, package: name, from: package) else { + Log.codeGenerate.warning(""" + No package for product \(product, privacy: .public) \ + required by \(package.directory, privacy: .public) + """) + return [] + } + + let directory = "//\(PluginSwiftPM.packagesDirectory)/\(owner.directory)" + let targets = owner.manifest.products + .first { $0.name == product }? + .targets ?? [] + + return targets.map { target in + guard let alias = aliases[target] else { + return "\(directory):\(ruleName(of: target, in: owner))" + } + return "\(directory):\(Self.aliasRuleName(of: target, as: alias))" + } + } + + /// Every alias any package asks for, filed under the package that owns + /// the module: that package's `BUILD` is where the aliased rule goes. + private func collectModuleAliases() { + for package in workspace.packages { + for target in package.manifest.targets { + for dependency in target.dependencies { + guard + !dependency.moduleAliases.isEmpty, + case .product(let product, let owner) = dependency.kind, + let source = self.package(ofProduct: product, package: owner, from: package) + else { + continue + } + + for (module, alias) in dependency.moduleAliases { + moduleAliases[source.directory, default: [:]][module, default: []].insert(alias) + } + } + } + } + } + /// Which package declares a product: the one the dependency names, or the /// one whose identity matches. private func package( diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Manifest.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Manifest.swift index ac18f4a..065c396 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Manifest.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Manifest.swift @@ -237,6 +237,9 @@ extension SwiftPM { /// The last element of the array a dependency is dumped as: the /// platforms it is limited to, and the traits that have to be on. let condition: SettingCondition? + /// `moduleAliases`: what the modules of that product are called here, + /// which is how two packages that both ship a `Core` are both used. + let moduleAliases: [String: String] init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: AnyKey.self) @@ -260,6 +263,9 @@ extension SwiftPM { self.kind = kind condition = values.compactMap(\.condition).last + moduleAliases = values.compactMap(\.aliases).reduce(into: [:]) { all, aliases in + all.merge(aliases) { _, later in later } + } return } @@ -279,6 +285,7 @@ extension SwiftPM { struct DependencyElement: Decodable { let name: String? let condition: SettingCondition? + let aliases: [String: String]? init(from decoder: Decoder) throws { if let single = try? decoder.singleValueContainer(), @@ -286,6 +293,7 @@ extension SwiftPM { { self.name = name condition = nil + aliases = nil return } @@ -297,10 +305,12 @@ extension SwiftPM { container.allKeys.contains(where: { Self.conditionKeys.contains($0.stringValue) }) else { condition = nil + aliases = try? decoder.singleValueContainer().decode([String: String].self) return } condition = try? SettingCondition(from: decoder) + aliases = nil } private static let conditionKeys: Set = ["platformNames", "traits", "config"] diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Settings.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Settings.swift index e87887a..59edcac 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Settings.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Settings.swift @@ -15,8 +15,8 @@ extension SwiftPM.Generator { /// anything the rules model: a `defines` attribute would re-tokenize a value /// and a feature is not a flag the rules know. /// - /// A setting conditional on a trait is a `select` on that trait's flag, so - /// the build decides it rather than this run. + /// A conditional setting becomes a `select` on its traits and build + /// configuration, so the build decides it rather than this run. func copts(of target: SwiftPM.PackageTarget, in package: SwiftPM.Package) -> Starlark.Value? { grouped( target.settings, @@ -28,7 +28,7 @@ extension SwiftPM.Generator { /// everything that depends on the library, and a project's own target /// must not compile as if it were a package — Xcode's generated asset /// symbols, for one, switch on `SWIFT_PACKAGE`. - always: Self.define("SWIFT_PACKAGE"), + always: Self.define("SWIFT_PACKAGE") + Self.aliasFlags(of: target), /// A trait is a compilation condition of the package that declares /// it: `#if Fast` is how a source asks. Swift only — SwiftPM does /// not hand it to clang, so neither is it handed to `-Xcc`. @@ -48,6 +48,20 @@ extension SwiftPM.Generator { } } + /// `-module-alias =` for every + /// module this target renamed: the alias is what keeps two packages that + /// ship the same module name apart, and the sources keep saying what they + /// always said. + private static func aliasFlags(of target: SwiftPM.PackageTarget) -> [String] { + target.dependencies + .flatMap { dependency in + dependency.moduleAliases.sorted { $0.key < $1.key } + } + .flatMap { module, alias in + ["-module-alias", "\(module)=\(alias)"] + } + } + /// A package can name a system library or framework it needs; nothing else in /// the graph knows about it. func linkopts(of target: SwiftPM.PackageTarget, in package: SwiftPM.Package) -> Starlark.Value? { @@ -113,10 +127,11 @@ extension SwiftPM.Generator { ["-D\(name)", "-Xcc", "-D\(name)"] } - /// Settings as one list plus one `select` per trait condition. + /// Settings as one list plus one `select` per trait or configuration + /// condition. /// - /// One `select` per condition rather than one with every key: two traits can - /// be on at once, and a `select` whose keys both match is an error rather + /// One `select` per condition rather than one with every key: two conditions + /// can be on at once, and a `select` whose keys both match is an error rather /// than both lists. func grouped( _ settings: [SwiftPM.Setting], @@ -138,7 +153,7 @@ extension SwiftPM.Generator { let values = flags(setting) guard !values.isEmpty else { continue } - guard let condition = traitCondition(setting.traits, in: package) else { + guard let condition = settingCondition(setting.condition, in: package) else { always += values continue } diff --git a/spm/DependencyShape/Other/Package.swift b/spm/DependencyShape/Other/Package.swift new file mode 100644 index 0000000..89809bf --- /dev/null +++ b/spm/DependencyShape/Other/Package.swift @@ -0,0 +1,14 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +/// Its module is called `Core`, which is also what another package in the graph +/// calls one of its own: whoever uses both has to rename one. +let package = Package( + name: "Other", + products: [ + .library(name: "OtherCore", targets: ["Core"]), + ], + targets: [ + .target(name: "Core"), + ]) diff --git a/spm/DependencyShape/Other/Sources/Core/Core.swift b/spm/DependencyShape/Other/Sources/Core/Core.swift new file mode 100644 index 0000000..f5ecb22 --- /dev/null +++ b/spm/DependencyShape/Other/Sources/Core/Core.swift @@ -0,0 +1 @@ +public let core = "other package" diff --git a/spm/DependencyShape/Package.swift b/spm/DependencyShape/Package.swift new file mode 100644 index 0000000..f3ae14f --- /dev/null +++ b/spm/DependencyShape/Package.swift @@ -0,0 +1,42 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +/// How a target names what it depends on: a target of its own package by +/// `.target` or by name, a product of another package, a package whose identity +/// is not what its manifest calls it, and a module renamed because two packages +/// ship one of the same name. +let package = Package( + name: "DependencyShape", + products: [ + .library(name: "DependencyShape", targets: ["Consumer"]), + ], + dependencies: [ + /// The directory is `vendor-kit`, the manifest says `VendorKit`, and the + /// dependency is written as neither. + .package(name: "Vendor", path: "vendor-kit"), + .package(path: "Other"), + ], + targets: [ + .target(name: "Local"), + .target( + name: "Consumer", + dependencies: [ + /// A target of this package, named as one. + .target(name: "Local"), + /// A target of this package, named by bare string. + "Helper", + /// A product of another package. + .product(name: "VendorCore", package: "Vendor"), + /// A product whose module is called `Core` too, so it is + /// renamed here. + .product( + name: "OtherCore", + package: "Other", + moduleAliases: ["Core": "OtherCore"]), + ]), + .target(name: "Helper"), + .testTarget( + name: "DependencyShapeTests", + dependencies: ["Consumer"]), + ]) diff --git a/spm/DependencyShape/Sources/Consumer/Consumer.swift b/spm/DependencyShape/Sources/Consumer/Consumer.swift new file mode 100644 index 0000000..97918c4 --- /dev/null +++ b/spm/DependencyShape/Sources/Consumer/Consumer.swift @@ -0,0 +1,11 @@ +/// `Core` is what the other package calls its module, and what this source +/// calls it too: the alias renames what is compiled — `OtherCore` — so that +/// name can clash with something else in the graph without this source caring. +import Core +import Helper +import Local +import VendorCore + +public enum Consumer { + public static let everything = [local, helper, vendorCore, core] +} diff --git a/spm/DependencyShape/Sources/Helper/Helper.swift b/spm/DependencyShape/Sources/Helper/Helper.swift new file mode 100644 index 0000000..9857643 --- /dev/null +++ b/spm/DependencyShape/Sources/Helper/Helper.swift @@ -0,0 +1 @@ +public let helper = "helper target" diff --git a/spm/DependencyShape/Sources/Local/Local.swift b/spm/DependencyShape/Sources/Local/Local.swift new file mode 100644 index 0000000..9e2da6e --- /dev/null +++ b/spm/DependencyShape/Sources/Local/Local.swift @@ -0,0 +1 @@ +public let local = "local target" diff --git a/spm/DependencyShape/Tests/DependencyShapeTests/DependencyShapeTests.swift b/spm/DependencyShape/Tests/DependencyShapeTests/DependencyShapeTests.swift new file mode 100644 index 0000000..832e4f8 --- /dev/null +++ b/spm/DependencyShape/Tests/DependencyShapeTests/DependencyShapeTests.swift @@ -0,0 +1,7 @@ +import Consumer +import Testing + +@Test +func everyDependencyShapeResolves() { + #expect(Consumer.everything == ["local target", "helper target", "vendor-kit", "other package"]) +} diff --git a/spm/DependencyShape/vendor-kit/Package.swift b/spm/DependencyShape/vendor-kit/Package.swift new file mode 100644 index 0000000..bcc2d79 --- /dev/null +++ b/spm/DependencyShape/vendor-kit/Package.swift @@ -0,0 +1,15 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +/// The directory is `vendor-kit`, which is the identity SwiftPM files this +/// package under; the name here is what a manifest that depends on it may use +/// instead. +let package = Package( + name: "VendorKit", + products: [ + .library(name: "VendorCore", targets: ["VendorCore"]), + ], + targets: [ + .target(name: "VendorCore"), + ]) diff --git a/spm/DependencyShape/vendor-kit/Sources/VendorCore/VendorCore.swift b/spm/DependencyShape/vendor-kit/Sources/VendorCore/VendorCore.swift new file mode 100644 index 0000000..867112d --- /dev/null +++ b/spm/DependencyShape/vendor-kit/Sources/VendorCore/VendorCore.swift @@ -0,0 +1 @@ +public let vendorCore = "vendor-kit" diff --git a/spm/ProductShapes/Package.swift b/spm/ProductShapes/Package.swift new file mode 100644 index 0000000..53f5ed1 --- /dev/null +++ b/spm/ProductShapes/Package.swift @@ -0,0 +1,24 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +/// Consumes grouped and target-name-colliding products from a local package. +let package = Package( + name: "ProductShapes", + products: [ + .library(name: "ProductShapes", targets: ["ProductShapes"]), + ], + dependencies: [ + .package(path: "Products"), + ], + targets: [ + .target( + name: "ProductShapes", + dependencies: [ + .product(name: "Combined", package: "Products"), + .product(name: "First", package: "Products"), + ]), + .testTarget( + name: "ProductShapesTests", + dependencies: ["ProductShapes"]), + ]) diff --git a/spm/ProductShapes/Products/Package.swift b/spm/ProductShapes/Products/Package.swift new file mode 100644 index 0000000..1fcadab --- /dev/null +++ b/spm/ProductShapes/Products/Package.swift @@ -0,0 +1,19 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "Products", + products: [ + .library(name: "Combined", targets: ["First", "Second"]), + /// The product keeps this name; the target rule must move aside. + .library(name: "First", targets: ["First", "Second"]), + .library(name: "StaticCombined", type: .static, targets: ["First", "Second"]), + .library(name: "DynamicCombined", type: .dynamic, targets: ["First", "Second"]), + .executable(name: "renamed-tool", targets: ["Tool"]), + ], + targets: [ + .target(name: "First"), + .target(name: "Second"), + .executableTarget(name: "Tool"), + ]) diff --git a/spm/ProductShapes/Products/Snippets/ProductSnippet.swift b/spm/ProductShapes/Products/Snippets/ProductSnippet.swift new file mode 100644 index 0000000..c1cc997 --- /dev/null +++ b/spm/ProductShapes/Products/Snippets/ProductSnippet.swift @@ -0,0 +1,4 @@ +import First +import Second + +print(firstValue + secondValue) diff --git a/spm/ProductShapes/Products/Sources/First/First.swift b/spm/ProductShapes/Products/Sources/First/First.swift new file mode 100644 index 0000000..5e4c38a --- /dev/null +++ b/spm/ProductShapes/Products/Sources/First/First.swift @@ -0,0 +1 @@ +public let firstValue = 20 diff --git a/spm/ProductShapes/Products/Sources/Second/Second.swift b/spm/ProductShapes/Products/Sources/Second/Second.swift new file mode 100644 index 0000000..cb84afd --- /dev/null +++ b/spm/ProductShapes/Products/Sources/Second/Second.swift @@ -0,0 +1 @@ +public let secondValue = 22 diff --git a/spm/ProductShapes/Products/Sources/Tool/main.swift b/spm/ProductShapes/Products/Sources/Tool/main.swift new file mode 100644 index 0000000..f09f99c --- /dev/null +++ b/spm/ProductShapes/Products/Sources/Tool/main.swift @@ -0,0 +1 @@ +print("product executable") diff --git a/spm/ProductShapes/Sources/ProductShapes/ProductShapes.swift b/spm/ProductShapes/Sources/ProductShapes/ProductShapes.swift new file mode 100644 index 0000000..bbca31f --- /dev/null +++ b/spm/ProductShapes/Sources/ProductShapes/ProductShapes.swift @@ -0,0 +1,8 @@ +import First +import Second + +public enum ProductShapes { + public static var combinedValue: Int { + firstValue + secondValue + } +} diff --git a/spm/ProductShapes/Tests/ProductShapesTests/ProductShapesTests.swift b/spm/ProductShapes/Tests/ProductShapesTests/ProductShapesTests.swift new file mode 100644 index 0000000..60d2ec6 --- /dev/null +++ b/spm/ProductShapes/Tests/ProductShapesTests/ProductShapesTests.swift @@ -0,0 +1,7 @@ +import ProductShapes +import Testing + +@Test +func groupedProductsExposeEveryTarget() { + #expect(ProductShapes.combinedValue == 42) +} From 4dc887b6aef44229b5cf8ae80a97466bf35926f9 Mon Sep 17 00:00:00 2001 From: yume190 Date: Tue, 22 Sep 2026 11:19:15 +0800 Subject: [PATCH 09/47] test(spm): cover binary targets, system libraries and settings One package per thing: a local zipped XCFramework that links statically, a remote one that links dynamically, system libraries whose module maps link a library and a framework, every SwiftSetting and LinkerSetting made observable, and a plugin's prebuild command. --- spm/BinaryTarget/LocalBinary.xcframework.zip | Bin 0 -> 2264 bytes spm/BinaryTarget/Package.swift | 21 +++++ .../Sources/BinaryTarget/BinaryTarget.swift | 7 ++ .../BinaryTargetTests/BinaryTargetTests.swift | 7 ++ spm/PrebuildPlugin/Package.swift | 23 ++++++ .../Plugins/GeneratePrebuild/Plugin.swift | 23 ++++++ .../PrebuildPlugin/PrebuildPlugin.swift | 5 ++ .../PrebuildPluginTests.swift | 7 ++ spm/RemoteXCFramework/Package.swift | 22 +++++ .../RemoteXCFrameworkTests.swift | 8 ++ spm/SwiftSettings/Package.swift | 45 +++++++++++ .../Sources/SwiftSettings/SwiftSettings.swift | 75 ++++++++++++++++++ .../SwiftSettingsTests.swift | 20 +++++ spm/SystemLibrary/Package.swift | 29 +++++++ .../Sources/CSecurity/module.modulemap | 5 ++ spm/SystemLibrary/Sources/CSecurity/shim.h | 1 + .../Sources/CZlib/module.modulemap | 5 ++ spm/SystemLibrary/Sources/CZlib/shim.h | 1 + .../Sources/SystemLibrary/SystemLibrary.swift | 16 ++++ .../SystemLibraryTests.swift | 12 +++ 20 files changed, 332 insertions(+) create mode 100644 spm/BinaryTarget/LocalBinary.xcframework.zip create mode 100644 spm/BinaryTarget/Package.swift create mode 100644 spm/BinaryTarget/Sources/BinaryTarget/BinaryTarget.swift create mode 100644 spm/BinaryTarget/Tests/BinaryTargetTests/BinaryTargetTests.swift create mode 100644 spm/PrebuildPlugin/Package.swift create mode 100644 spm/PrebuildPlugin/Plugins/GeneratePrebuild/Plugin.swift create mode 100644 spm/PrebuildPlugin/Sources/PrebuildPlugin/PrebuildPlugin.swift create mode 100644 spm/PrebuildPlugin/Tests/PrebuildPluginTests/PrebuildPluginTests.swift create mode 100644 spm/RemoteXCFramework/Package.swift create mode 100644 spm/RemoteXCFramework/Tests/RemoteXCFrameworkTests/RemoteXCFrameworkTests.swift create mode 100644 spm/SwiftSettings/Package.swift create mode 100644 spm/SwiftSettings/Sources/SwiftSettings/SwiftSettings.swift create mode 100644 spm/SwiftSettings/Tests/SwiftSettingsTests/SwiftSettingsTests.swift create mode 100644 spm/SystemLibrary/Package.swift create mode 100644 spm/SystemLibrary/Sources/CSecurity/module.modulemap create mode 100644 spm/SystemLibrary/Sources/CSecurity/shim.h create mode 100644 spm/SystemLibrary/Sources/CZlib/module.modulemap create mode 100644 spm/SystemLibrary/Sources/CZlib/shim.h create mode 100644 spm/SystemLibrary/Sources/SystemLibrary/SystemLibrary.swift create mode 100644 spm/SystemLibrary/Tests/SystemLibraryTests/SystemLibraryTests.swift diff --git a/spm/BinaryTarget/LocalBinary.xcframework.zip b/spm/BinaryTarget/LocalBinary.xcframework.zip new file mode 100644 index 0000000000000000000000000000000000000000..166b5476d09d57fae966980963681dab484ca9b7 GIT binary patch literal 2264 zcmWIWW@h1H0D%WVX0c!fl#l?@KKaRsIZl~*iA9xq70GEuiMgre`9<0K0XTJO!Hqd(h-n}JKObiSeYzz$6q#2ZxnS|`vMC-5l z*X`KY*wozE*vb^n@*H7v&|)~k*7eb#fw7o5;3T(z^F_6d8kUFqyl)2FKjFa0$a7Yg zm&d2)?AgYqWo&H@f*o5HHa5=e>}+b1j45Oauh|ne)^k>H|7fbDv=2>u}|9 z=$24vu&`6Nw=`6@(=^uKxWZ_kj^T0>Q62tG*O|KaE?Z_$tti&0%X+b(TX;u}gPT>O z@0GP147$yYFP?6XI$iMZ`tAAy3<)f(3>WV6=s4_QJ`&Jy!kcL$t1`1+0K?LJgGHZi zN(wM?GkyH1x>8h?qxR!R#dU&;GoBSH&ph6wbC8KoN#*a!pF3GQPpW=gTKRNlrRK+v zc6uj1F$GAy;%yV}5!hZ~;P{7=#b?E{rD;c&i>f|bmQ+5UaY|XkB^e)%{f>9%Hs;I| zWLB|LENh%J+2l^pgbIc$vYb8wb|>yL@~mhOTsOZ&;MK*SCGBDp1gj-n=SU}-a0xWt zj1)a`AVTqg#(~4<3>jB=v1+I>ACdm&%%D>zA@h^v){HroHv{gTW;2>0f3rk}vueXL zbz$!}KTlq{!joO+rE|*loZo{#N0@ozBkE2$oc7oMaAY>yly?a(PY-_**10G&dw#-0 zMk9XJgOg{>Pk83A_n-metoddJN$34rF8$|QQ5Ud}<)@(KUt>o@5B(xJWmby~W-PJ^ zef!&<)v4uVIldHoVr93aM4Ri8vEm=!kX`3APwE=5Oy2OQiID-3c2A?Fcw^F%oJVS6 zN@`ItQW^#sIJ5tps|V1aP#|_7&p_maoDqD^=d`z{Z}5kY; zmakWklUZCccT#MAlYvOv`j6oT@0La<^+XCR)|e@I)8ok5*qO>9xjIv1R-M=%FZeO} z)!BEm=6$xQy)CzTd;iXTi)R>4Ea?^heDgx4P*;cQ>UD;^chZi(xXJrE^T$gT`+pw( zmZz7z^Jl+z<6^Ix;$M@eQzj%DT&c~s;VgNokn$z*#j&VUU#3p5Iq$2n`%J)*)MU3B z5lz>~3a@J+4U#8M1RM|fw%Q`gJZa$;=~-JBw%)zZ_;|yEE6Lm}DJGjecCJ6w!12^+ zHRD<*HXTNtnhCFWg^C(=zUned=k{+ouv$)7r$}ec3v;fCr;6q6#qw8b#TxcyDBYYC zxF}6%T_vgF1BmSR?F1bUQuW8_R-06*PItYdx&jzuKp?>I))7P_r(x8Z z1Gg!lx&s10rkDYlxJ [Command] { + let directory = context.pluginWorkDirectoryURL.appending(component: "Prebuild") + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + + let output = directory.appending(component: "Prebuilt.generated.swift") + + return [ + .prebuildCommand( + displayName: "Write a source before the build", + executable: URL(fileURLWithPath: "/bin/sh"), + arguments: [ + "-c", + "printf '%s\\n' 'public let prebuilt = \"written before the build\"' > \(output.path())", + ], + outputFilesDirectory: directory), + ] + } +} diff --git a/spm/PrebuildPlugin/Sources/PrebuildPlugin/PrebuildPlugin.swift b/spm/PrebuildPlugin/Sources/PrebuildPlugin/PrebuildPlugin.swift new file mode 100644 index 0000000..7ece909 --- /dev/null +++ b/spm/PrebuildPlugin/Sources/PrebuildPlugin/PrebuildPlugin.swift @@ -0,0 +1,5 @@ +/// `prebuilt` is not here: the plugin's prebuild command writes it, and this +/// only passes it on. +public func prebuiltValue() -> String { + prebuilt +} diff --git a/spm/PrebuildPlugin/Tests/PrebuildPluginTests/PrebuildPluginTests.swift b/spm/PrebuildPlugin/Tests/PrebuildPluginTests/PrebuildPluginTests.swift new file mode 100644 index 0000000..bdc7eb9 --- /dev/null +++ b/spm/PrebuildPlugin/Tests/PrebuildPluginTests/PrebuildPluginTests.swift @@ -0,0 +1,7 @@ +import PrebuildPlugin +import Testing + +@Test +func thePrebuildCommandGeneratedTheSource() { + #expect(prebuiltValue() == "written before the build") +} diff --git a/spm/RemoteXCFramework/Package.swift b/spm/RemoteXCFramework/Package.swift new file mode 100644 index 0000000..1dd62b3 --- /dev/null +++ b/spm/RemoteXCFramework/Package.swift @@ -0,0 +1,22 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +/// A remote dynamic XCFramework fetched and unpacked by SwiftPM. +let package = Package( + name: "RemoteXCFramework", + platforms: [ + .macOS(.v12), + ], + products: [ + .library(name: "RemoteXCFramework", targets: ["Sparkle"]), + ], + targets: [ + .binaryTarget( + name: "Sparkle", + url: "https://github.com/sparkle-project/Sparkle/releases/download/2.3.0/Sparkle-for-Swift-Package-Manager.zip", + checksum: "a32f43511071c4df4e3aa766ed3e8e0fc03dd5912d8a0db9e266794735ad247f"), + .testTarget( + name: "RemoteXCFrameworkTests", + dependencies: ["Sparkle"]), + ]) diff --git a/spm/RemoteXCFramework/Tests/RemoteXCFrameworkTests/RemoteXCFrameworkTests.swift b/spm/RemoteXCFramework/Tests/RemoteXCFrameworkTests/RemoteXCFrameworkTests.swift new file mode 100644 index 0000000..4799a62 --- /dev/null +++ b/spm/RemoteXCFramework/Tests/RemoteXCFrameworkTests/RemoteXCFrameworkTests.swift @@ -0,0 +1,8 @@ +import Sparkle +import Testing + +@Test +func aRemoteXCFrameworkIsFetchedImportedAndLinked() { + let result = SUStandardVersionComparator.default.compareVersion("2.0", toVersion: "1.0") + #expect(result == .orderedDescending) +} diff --git a/spm/SwiftSettings/Package.swift b/spm/SwiftSettings/Package.swift new file mode 100644 index 0000000..18646c1 --- /dev/null +++ b/spm/SwiftSettings/Package.swift @@ -0,0 +1,45 @@ +// swift-tools-version: 6.2 + +import PackageDescription + +/// What a target compiles and links with beyond the defaults: every kind of +/// `SwiftSetting` and `LinkerSetting`, each one observable — a setting that did +/// not reach the compiler fails the build, and one that did not reach the +/// linker leaves a symbol undefined. +let package = Package( + name: "SwiftSettings", + platforms: [.macOS(.v12)], + products: [ + .library(name: "SwiftSettings", targets: ["SwiftSettings"]), + ], + targets: [ + .target( + name: "SwiftSettings", + swiftSettings: [ + .define("MANIFEST_DEFINE"), + .swiftLanguageMode(.v5), + .enableUpcomingFeature("MemberImportVisibility"), + .enableExperimentalFeature("Extern"), + .strictMemorySafety(), + .defaultIsolation(MainActor.self), + .unsafeFlags(["-DUNSAFE_DEFINE"]), + ], + linkerSettings: [ + /// `crc32` is in libz and nowhere else, so the call only links + /// when the library does. + .linkedLibrary("z"), + /// Nothing here imports Security, so the framework is linked + /// because the manifest says so or not at all. + .linkedFramework("Security"), + /// An alias the program calls: without the flags there is no + /// such symbol. + .unsafeFlags([ + "-Xlinker", "-alias", + "-Xlinker", "_swiftsettings_probe", + "-Xlinker", "_swiftsettings_probe_alias", + ]), + ]), + .testTarget( + name: "SwiftSettingsTests", + dependencies: ["SwiftSettings"]), + ]) diff --git a/spm/SwiftSettings/Sources/SwiftSettings/SwiftSettings.swift b/spm/SwiftSettings/Sources/SwiftSettings/SwiftSettings.swift new file mode 100644 index 0000000..27a5a35 --- /dev/null +++ b/spm/SwiftSettings/Sources/SwiftSettings/SwiftSettings.swift @@ -0,0 +1,75 @@ +import Foundation + +/// `swiftLanguageMode(.v5)`: the target compiles as Swift 5 whatever the +/// manifest's tools version is. +#if swift(>=6.0) +#error("The target must compile in Swift 5 language mode") +#endif + +/// `define`. +#if !MANIFEST_DEFINE +#error("The manifest define must reach Swift sources") +#endif + +/// `unsafeFlags`. +#if !UNSAFE_DEFINE +#error("Unsafe Swift flags must reach Swift sources") +#endif + +/// `enableUpcomingFeature`. +#if !hasFeature(MemberImportVisibility) +#error("The upcoming feature must be enabled") +#endif + +/// `enableExperimentalFeature`: `@_extern` below is the feature's syntax, and +/// only parses when the compiler was told to enable it. + +/// `strictMemorySafety`. +#if !hasFeature(StrictMemorySafety) +#error("Strict memory safety must be enabled") +#endif + +/// The symbol the linker's `-alias` flags point at. +@_cdecl("swiftsettings_probe") +public func swiftSettingsProbe() -> Int32 { + 42 +} + +/// The alias itself: the linker made it, or this does not link. +@_extern(c, "swiftsettings_probe_alias") +func swiftSettingsProbeAlias() -> Int32 + +/// `crc32`, from the library the manifest links. +@_extern(c, "crc32") +func zlibCRC32(_ crc: UInt, _ buffer: UnsafePointer?, _ length: UInt32) -> UInt + +/// `SecCopyErrorMessageString`, from the framework the manifest links. +@_extern(c, "SecCopyErrorMessageString") +func secCopyErrorMessageString(_ status: Int32, _ reserved: UnsafeMutableRawPointer?) -> OpaquePointer? + +public enum SettingProbe { + /// Isolated to the main actor by `defaultIsolation`, not by an attribute: + /// calling it from anywhere else has to hop, and `assumeIsolated` traps if + /// it did not. + public static func isolation() -> Bool { + MainActor.assumeIsolated { true } + } + + /// The linked library's answer for `abc`. + public static func checksum() -> UInt { + let bytes: [UInt8] = Array("abc".utf8) + return unsafe zlibCRC32(0, bytes, UInt32(bytes.count)) + } + + /// The linked framework's message for "no error". + public static func frameworkMessage() -> String? { + guard let message = unsafe secCopyErrorMessageString(0, nil) else { return nil } + return unsafe Unmanaged.fromOpaque(UnsafeRawPointer(message)) + .takeRetainedValue() as String + } + + /// What the aliased symbol answers, which is what the original does. + public static func aliased() -> Int32 { + swiftSettingsProbeAlias() + } +} diff --git a/spm/SwiftSettings/Tests/SwiftSettingsTests/SwiftSettingsTests.swift b/spm/SwiftSettings/Tests/SwiftSettingsTests/SwiftSettingsTests.swift new file mode 100644 index 0000000..f0c9d62 --- /dev/null +++ b/spm/SwiftSettings/Tests/SwiftSettingsTests/SwiftSettingsTests.swift @@ -0,0 +1,20 @@ +import SwiftSettings +import Testing + +@Test +func defaultIsolationPutsTheTargetOnTheMainActor() async { + /// Off the main actor to begin with: reaching it is the setting's doing. + let isolated = await Task.detached { await SettingProbe.isolation() }.value + #expect(isolated) +} + +@Test @MainActor +func linkedLibraryAndFrameworkAreLinked() { + #expect(SettingProbe.checksum() == 0x3524_41C2) + #expect(SettingProbe.frameworkMessage()?.isEmpty == false) +} + +@Test @MainActor +func linkerUnsafeFlagsReachTheLink() { + #expect(SettingProbe.aliased() == 42) +} diff --git a/spm/SystemLibrary/Package.swift b/spm/SystemLibrary/Package.swift new file mode 100644 index 0000000..a675aab --- /dev/null +++ b/spm/SystemLibrary/Package.swift @@ -0,0 +1,29 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +/// System-library targets whose module maps supply their headers and linker +/// input: one that links a library, one that links a framework. +let package = Package( + name: "SystemLibrary", + products: [ + .library(name: "SystemLibrary", targets: ["SystemLibrary"]), + ], + targets: [ + .systemLibrary( + name: "CZlib", + pkgConfig: "zlib", + providers: [ + .brew(["zlib"]), + .apt(["zlib1g-dev"]), + ]), + /// `link framework` rather than `link`: the other half of what a module + /// map can ask the linker for. + .systemLibrary(name: "CSecurity"), + .target( + name: "SystemLibrary", + dependencies: ["CZlib", "CSecurity"]), + .testTarget( + name: "SystemLibraryTests", + dependencies: ["SystemLibrary"]), + ]) diff --git a/spm/SystemLibrary/Sources/CSecurity/module.modulemap b/spm/SystemLibrary/Sources/CSecurity/module.modulemap new file mode 100644 index 0000000..5ed9232 --- /dev/null +++ b/spm/SystemLibrary/Sources/CSecurity/module.modulemap @@ -0,0 +1,5 @@ +module CSecurity [system] { + header "shim.h" + link framework "Security" + export * +} diff --git a/spm/SystemLibrary/Sources/CSecurity/shim.h b/spm/SystemLibrary/Sources/CSecurity/shim.h new file mode 100644 index 0000000..0a83641 --- /dev/null +++ b/spm/SystemLibrary/Sources/CSecurity/shim.h @@ -0,0 +1 @@ +#include diff --git a/spm/SystemLibrary/Sources/CZlib/module.modulemap b/spm/SystemLibrary/Sources/CZlib/module.modulemap new file mode 100644 index 0000000..4089437 --- /dev/null +++ b/spm/SystemLibrary/Sources/CZlib/module.modulemap @@ -0,0 +1,5 @@ +module CZlib [system] { + header "shim.h" + link "z" + export * +} diff --git a/spm/SystemLibrary/Sources/CZlib/shim.h b/spm/SystemLibrary/Sources/CZlib/shim.h new file mode 100644 index 0000000..4470a1f --- /dev/null +++ b/spm/SystemLibrary/Sources/CZlib/shim.h @@ -0,0 +1 @@ +#include diff --git a/spm/SystemLibrary/Sources/SystemLibrary/SystemLibrary.swift b/spm/SystemLibrary/Sources/SystemLibrary/SystemLibrary.swift new file mode 100644 index 0000000..75f7358 --- /dev/null +++ b/spm/SystemLibrary/Sources/SystemLibrary/SystemLibrary.swift @@ -0,0 +1,16 @@ +import CSecurity +import CZlib +import Foundation + +public enum SystemLibrary { + /// From the module that links a library. + public static var version: String { + String(cString: zlibVersion()) + } + + /// From the module that links a framework: `Security` is linked because + /// its module map says so, and nothing else here pulls it in. + public static var securityMessage: String? { + SecCopyErrorMessageString(errSecSuccess, nil).map { $0 as String } + } +} diff --git a/spm/SystemLibrary/Tests/SystemLibraryTests/SystemLibraryTests.swift b/spm/SystemLibrary/Tests/SystemLibraryTests/SystemLibraryTests.swift new file mode 100644 index 0000000..7149e3f --- /dev/null +++ b/spm/SystemLibrary/Tests/SystemLibraryTests/SystemLibraryTests.swift @@ -0,0 +1,12 @@ +import SystemLibrary +import Testing + +@Test +func aSystemLibraryImportsHeadersAndLinksItsLibrary() { + #expect(!SystemLibrary.version.isEmpty) +} + +@Test +func aModuleMapLinksTheFrameworkItNames() { + #expect(SystemLibrary.securityMessage?.isEmpty == false) +} From 68589019046f3d878adfbf9a6f0656e6729bc0a7 Mon Sep 17 00:00:00 2001 From: yume190 Date: Tue, 22 Sep 2026 11:19:15 +0800 Subject: [PATCH 10/47] test(spm): cover the C-family shapes and every resource rule Objective-C, Objective-C++, assembly, a module map the package ships, and C and C++ unsafe flags; localized resources, an asset catalogue, a xib, a shader, a string catalogue, and the .docc and .xcprivacy SwiftPM ignores. --- spm/Clang/Package.swift | 7 ++++- spm/Clang/Sources/CObject/Assembly.S | 15 ++++++++++ spm/Clang/Sources/CObject/CObject.m | 3 ++ spm/Clang/Sources/CObject/headers/CObject.h | 10 +++++++ spm/Clang/Sources/Consumer/Consumer.swift | 8 ++++++ spm/Clang/Sources/CxxLib/ObjectiveCxx.mm | 11 ++++++++ spm/Clang/Sources/CxxLib/include/CxxLib.hpp | 1 + .../Sources/CxxLib/include/module.modulemap | 4 +++ spm/Clang/Tests/ClangTests/ClangTests.swift | 6 ++++ spm/TargetResource/Package.swift | 12 ++++++-- .../Brand.colorset/Contents.json | 20 +++++++++++++ .../Assets.xcassets/Contents.json | 6 ++++ .../Sources/TargetResource/Catalog.xcstrings | 16 +++++++++++ .../TargetResource/Localized/Explicit.strings | 1 + .../Sources/TargetResource/Panel.xib | 15 ++++++++++ .../TargetResource/PrivacyInfo.xcprivacy | 12 ++++++++ .../Sources/TargetResource/Shader.metal | 8 ++++++ .../TargetResource.docc/TargetResource.md | 8 ++++++ .../TargetResource/TargetResource.swift | 25 +++++++++++++++++ .../en.lproj/Localizable.strings | 1 + .../TargetResourceTests.swift | 28 +++++++++++++++++++ 21 files changed, 214 insertions(+), 3 deletions(-) create mode 100644 spm/Clang/Sources/CObject/Assembly.S create mode 100644 spm/Clang/Sources/CxxLib/ObjectiveCxx.mm create mode 100644 spm/Clang/Sources/CxxLib/include/module.modulemap create mode 100644 spm/TargetResource/Sources/TargetResource/Assets.xcassets/Brand.colorset/Contents.json create mode 100644 spm/TargetResource/Sources/TargetResource/Assets.xcassets/Contents.json create mode 100644 spm/TargetResource/Sources/TargetResource/Catalog.xcstrings create mode 100644 spm/TargetResource/Sources/TargetResource/Localized/Explicit.strings create mode 100644 spm/TargetResource/Sources/TargetResource/Panel.xib create mode 100644 spm/TargetResource/Sources/TargetResource/PrivacyInfo.xcprivacy create mode 100644 spm/TargetResource/Sources/TargetResource/Shader.metal create mode 100644 spm/TargetResource/Sources/TargetResource/TargetResource.docc/TargetResource.md create mode 100644 spm/TargetResource/Sources/TargetResource/en.lproj/Localizable.strings diff --git a/spm/Clang/Package.swift b/spm/Clang/Package.swift index 8932119..cc41528 100644 --- a/spm/Clang/Package.swift +++ b/spm/Clang/Package.swift @@ -24,8 +24,13 @@ let package = Package( .headerSearchPath("internal"), .define("C_FLAG"), .define("C_VALUE", to: "7"), + .unsafeFlags(["-DC_UNSAFE_FLAG"]), + ]), + .target( + name: "CxxLib", + cxxSettings: [ + .unsafeFlags(["-DCXX_UNSAFE_FLAG"]), ]), - .target(name: "CxxLib"), .target( name: "Consumer", dependencies: ["CObject", "CxxLib"], diff --git a/spm/Clang/Sources/CObject/Assembly.S b/spm/Clang/Sources/CObject/Assembly.S new file mode 100644 index 0000000..d4e397e --- /dev/null +++ b/spm/Clang/Sources/CObject/Assembly.S @@ -0,0 +1,15 @@ +#if defined(__arm64__) +.globl _assembly_value +.p2align 2 +_assembly_value: + mov w0, #9 + ret +#elif defined(__x86_64__) +.globl _assembly_value +.p2align 4, 0x90 +_assembly_value: + movl $9, %eax + retq +#else +#error Unsupported architecture +#endif diff --git a/spm/Clang/Sources/CObject/CObject.m b/spm/Clang/Sources/CObject/CObject.m index 342e247..5a482ff 100644 --- a/spm/Clang/Sources/CObject/CObject.m +++ b/spm/Clang/Sources/CObject/CObject.m @@ -1,6 +1,9 @@ #import "CObject.h" #import "CInternal.h" +#ifndef C_UNSAFE_FLAG +#error "C unsafe flags must reach Objective-C sources" +#endif @implementation CObject diff --git a/spm/Clang/Sources/CObject/headers/CObject.h b/spm/Clang/Sources/CObject/headers/CObject.h index 79f097a..fae13b3 100644 --- a/spm/Clang/Sources/CObject/headers/CObject.h +++ b/spm/Clang/Sources/CObject/headers/CObject.h @@ -1,5 +1,15 @@ #import +#ifdef __cplusplus +extern "C" { +#endif +/// What the target's assembly source defines: a C symbol, so it keeps its name +/// when this header is read as C++ too. +int assembly_value(void); +#ifdef __cplusplus +} +#endif + NS_ASSUME_NONNULL_BEGIN @interface CObject : NSObject diff --git a/spm/Clang/Sources/Consumer/Consumer.swift b/spm/Clang/Sources/Consumer/Consumer.swift index 1ca3366..201bf08 100644 --- a/spm/Clang/Sources/Consumer/Consumer.swift +++ b/spm/Clang/Sources/Consumer/Consumer.swift @@ -18,7 +18,15 @@ public enum Consumer { CObject.greeting() } + public static var assembly: Int32 { + assembly_value() + } + public static var twice: Int32 { demo.twice(21) } + + public static var objectiveCxxLength: Int32 { + demo.objectiveCxxLength() + } } diff --git a/spm/Clang/Sources/CxxLib/ObjectiveCxx.mm b/spm/Clang/Sources/CxxLib/ObjectiveCxx.mm new file mode 100644 index 0000000..17e65d6 --- /dev/null +++ b/spm/Clang/Sources/CxxLib/ObjectiveCxx.mm @@ -0,0 +1,11 @@ +#import + +#include "CxxLib.hpp" + +#ifndef CXX_UNSAFE_FLAG +#error "C++ unsafe flags must reach Objective-C++ sources" +#endif + +int demo::objectiveCxxLength() { + return (int)[@"objcxx" length]; +} diff --git a/spm/Clang/Sources/CxxLib/include/CxxLib.hpp b/spm/Clang/Sources/CxxLib/include/CxxLib.hpp index e01c7e7..1be6dbe 100644 --- a/spm/Clang/Sources/CxxLib/include/CxxLib.hpp +++ b/spm/Clang/Sources/CxxLib/include/CxxLib.hpp @@ -4,4 +4,5 @@ namespace demo { /// A C++ function, called from Swift only because the target that calls it is /// compiled in C++ interoperability mode. int twice(int value); +int objectiveCxxLength(); } diff --git a/spm/Clang/Sources/CxxLib/include/module.modulemap b/spm/Clang/Sources/CxxLib/include/module.modulemap new file mode 100644 index 0000000..ffc77e5 --- /dev/null +++ b/spm/Clang/Sources/CxxLib/include/module.modulemap @@ -0,0 +1,4 @@ +module CxxLib { + header "CxxLib.hpp" + export * +} diff --git a/spm/Clang/Tests/ClangTests/ClangTests.swift b/spm/Clang/Tests/ClangTests/ClangTests.swift index e6f1ed0..d4883ea 100644 --- a/spm/Clang/Tests/ClangTests/ClangTests.swift +++ b/spm/Clang/Tests/ClangTests/ClangTests.swift @@ -21,3 +21,9 @@ func aCTargetReachesItsOwnBundle() { func cxxInteroperabilityWorks() { #expect(Consumer.twice == 42) } + +@Test +func assemblyAndObjectiveCxxCompile() { + #expect(Consumer.assembly == 9) + #expect(Consumer.objectiveCxxLength == 6) +} diff --git a/spm/TargetResource/Package.swift b/spm/TargetResource/Package.swift index dce0070..0fd87a7 100644 --- a/spm/TargetResource/Package.swift +++ b/spm/TargetResource/Package.swift @@ -2,10 +2,12 @@ import PackageDescription -/// The three things a target can do with a resource: copy it as it is, let the -/// platform process it, or compile it into the binary. +/// What a target can do with a resource: copy it as it is, let the platform +/// process it, or compile it into the binary — plus the localized and +/// platform-compiled kinds, and the directories SwiftPM ignores. let package = Package( name: "TargetResource", + defaultLocalization: "en", products: [ .library(name: "TargetResource", targets: ["TargetResource"]), ], @@ -15,7 +17,13 @@ let package = Package( resources: [ .copy("Copied"), .process("Processed"), + .process("Localized", localization: .default), .embedInCode("Embedded/greeting.txt"), + /// Kinds the platform compiles rather than copies. + .process("Assets.xcassets"), + .process("Panel.xib"), + .process("Shader.metal"), + .process("Catalog.xcstrings"), ]), .testTarget( name: "TargetResourceTests", diff --git a/spm/TargetResource/Sources/TargetResource/Assets.xcassets/Brand.colorset/Contents.json b/spm/TargetResource/Sources/TargetResource/Assets.xcassets/Brand.colorset/Contents.json new file mode 100644 index 0000000..72ef4c0 --- /dev/null +++ b/spm/TargetResource/Sources/TargetResource/Assets.xcassets/Brand.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.500", + "green" : "0.250", + "red" : "0.750" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/spm/TargetResource/Sources/TargetResource/Assets.xcassets/Contents.json b/spm/TargetResource/Sources/TargetResource/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/spm/TargetResource/Sources/TargetResource/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/spm/TargetResource/Sources/TargetResource/Catalog.xcstrings b/spm/TargetResource/Sources/TargetResource/Catalog.xcstrings new file mode 100644 index 0000000..73f3c71 --- /dev/null +++ b/spm/TargetResource/Sources/TargetResource/Catalog.xcstrings @@ -0,0 +1,16 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "catalog" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "from catalog" + } + } + } + } + }, + "version" : "1.0" +} diff --git a/spm/TargetResource/Sources/TargetResource/Localized/Explicit.strings b/spm/TargetResource/Sources/TargetResource/Localized/Explicit.strings new file mode 100644 index 0000000..6345375 --- /dev/null +++ b/spm/TargetResource/Sources/TargetResource/Localized/Explicit.strings @@ -0,0 +1 @@ +"explicit" = "explicit localization"; diff --git a/spm/TargetResource/Sources/TargetResource/Panel.xib b/spm/TargetResource/Sources/TargetResource/Panel.xib new file mode 100644 index 0000000..4ab80ee --- /dev/null +++ b/spm/TargetResource/Sources/TargetResource/Panel.xib @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/spm/TargetResource/Sources/TargetResource/PrivacyInfo.xcprivacy b/spm/TargetResource/Sources/TargetResource/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..d2d54f1 --- /dev/null +++ b/spm/TargetResource/Sources/TargetResource/PrivacyInfo.xcprivacy @@ -0,0 +1,12 @@ + + + + + NSPrivacyTracking + + NSPrivacyCollectedDataTypes + + NSPrivacyAccessedAPITypes + + + diff --git a/spm/TargetResource/Sources/TargetResource/Shader.metal b/spm/TargetResource/Sources/TargetResource/Shader.metal new file mode 100644 index 0000000..ab863d3 --- /dev/null +++ b/spm/TargetResource/Sources/TargetResource/Shader.metal @@ -0,0 +1,8 @@ +#include + +using namespace metal; + +kernel void doubleValues(device float *values [[buffer(0)]], + uint index [[thread_position_in_grid]]) { + values[index] = values[index] * 2.0; +} diff --git a/spm/TargetResource/Sources/TargetResource/TargetResource.docc/TargetResource.md b/spm/TargetResource/Sources/TargetResource/TargetResource.docc/TargetResource.md new file mode 100644 index 0000000..f81e847 --- /dev/null +++ b/spm/TargetResource/Sources/TargetResource/TargetResource.docc/TargetResource.md @@ -0,0 +1,8 @@ +# ``TargetResource`` + +A documentation catalogue: SwiftPM ignores it, so nothing in it may reach the +bundle. + +## Topics + +- ``TargetResource/copied`` diff --git a/spm/TargetResource/Sources/TargetResource/TargetResource.swift b/spm/TargetResource/Sources/TargetResource/TargetResource.swift index f725ca1..97b8fc3 100644 --- a/spm/TargetResource/Sources/TargetResource/TargetResource.swift +++ b/spm/TargetResource/Sources/TargetResource/TargetResource.swift @@ -18,6 +18,31 @@ public enum TargetResource { public static var embedded: String { String(decoding: PackageResources.greeting_txt, as: UTF8.self).trimmed } + + /// The explicitly localized resource: declared with `localization:`, so it + /// is filed under the package's default localization. + public static var explicitLocalization: String? { + guard let url = Bundle.module.url(forResource: "Explicit", withExtension: "strings") else { return nil } + return try? String(contentsOf: url, encoding: .utf8).trimmed + } + + /// A `.lproj` directory is a localization without anything being declared. + public static var lprojLocalization: String { + NSLocalizedString("lproj", bundle: .module, comment: "") + } + + /// What is in the bundle, by name. A platform resource is compiled by + /// whoever builds it — `Assets.car`, `Panel.nib`, `default.metallib` — and + /// copied as it is by whoever cannot, so both names are the same resource + /// having arrived. + public static var bundled: Set { + guard let root = Bundle.module.resourceURL, + let entries = try? FileManager.default.subpathsOfDirectory(atPath: root.path) + else { + return [] + } + return Set(entries) + } } extension String { diff --git a/spm/TargetResource/Sources/TargetResource/en.lproj/Localizable.strings b/spm/TargetResource/Sources/TargetResource/en.lproj/Localizable.strings new file mode 100644 index 0000000..525abbb --- /dev/null +++ b/spm/TargetResource/Sources/TargetResource/en.lproj/Localizable.strings @@ -0,0 +1 @@ +"lproj" = "from lproj"; diff --git a/spm/TargetResource/Tests/TargetResourceTests/TargetResourceTests.swift b/spm/TargetResource/Tests/TargetResourceTests/TargetResourceTests.swift index 0d24b15..89a8493 100644 --- a/spm/TargetResource/Tests/TargetResourceTests/TargetResourceTests.swift +++ b/spm/TargetResource/Tests/TargetResourceTests/TargetResourceTests.swift @@ -15,3 +15,31 @@ func aProcessedResourceIsInTheBundle() { func anEmbeddedResourceIsInTheBinary() { #expect(TargetResource.embedded == "embedded") } + +@Test +func localizedResourcesAreFiledUnderTheirLocalization() { + #expect(TargetResource.explicitLocalization == #""explicit" = "explicit localization";"#) + #expect(TargetResource.lprojLocalization == "from lproj") +} + +@Test +func platformResourcesReachTheBundle() { + let bundled = TargetResource.bundled + + func has(_ names: String...) -> Bool { + names.contains { name in bundled.contains { $0 == name || $0.hasSuffix("/\(name)") } } + } + + #expect(has("Assets.car", "Assets.xcassets")) + #expect(has("Panel.nib", "Panel.xib")) + #expect(has("default.metallib", "Shader.metal")) + #expect(has("Catalog.xcstrings", "Catalog.strings")) +} + +@Test +func documentationAndPrivacyAreNotResources() { + let bundled = TargetResource.bundled + + #expect(!bundled.contains { $0.hasSuffix(".docc") || $0.hasSuffix(".md") }) + #expect(!bundled.contains { $0.hasSuffix(".xcprivacy") }) +} From 3e83ad645a30daddbf5617276652287cda43a6af Mon Sep 17 00:00:00 2001 From: yume190 Date: Tue, 22 Sep 2026 11:19:15 +0800 Subject: [PATCH 11/47] ci(spm): run every fixture, and say what each one holds A lane per package in spm/, and each trait selected through the config the workspace generated for it rather than by flipping one flag. --- .github/workflows/swift.yml | 37 +++++++++++++++++++++++-------------- spm/README.md | 19 +++++++++++++++++-- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 667a681..8413614 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -316,21 +316,34 @@ jobs: fail-fast: false matrix: include: + - name: ArtifactBundle + plugins: true + - name: BinaryTarget - name: BuildToolPlugin plugins: true - - name: CommandPlugin - name: Clang + - name: CommandPlugin + - name: ConfigurationCondition - name: DependencyCondition # A trait is a flag, so the build that turns one on is a build to # test as well: the dependency behind it is linked only then. config: DependencyCondition.Extras + - name: DependencyShape - name: Macro - - name: Trait - traits: Fast,Slow - - name: TargetSources - - name: TargetPath + - name: PrebuildPlugin + plugins: true + - name: ProductShapes + - name: RemoteXCFramework + - name: SwiftSettings + - name: SystemLibrary - name: TargetExclude + - name: TargetPath - name: TargetResource + - name: TargetSources + - name: Trait + traits: Fast,Slow + - name: TraitGraph + traits: Alternative,Leaf,Middle,Top steps: - uses: actions/checkout@v6 @@ -391,9 +404,10 @@ jobs: ! bazel aquery 'mnemonic("SwiftCompile", //...)' | grep -q -- "-D${trait#*.}" bazel aquery --config=${{ matrix.config }} 'mnemonic("SwiftCompile", //...)' | grep -q -- "-D${trait#*.}" - # Select each declared trait on its own. This mirrors - # `swift test --traits ` rather than carrying default traits into - # every explicit selection. + # Select each declared trait on its own, through the `--config` the + # workspace generated for it. That is what `swift test --traits ` + # does: the selection replaces the package's defaults, and carries + # whatever that trait enables. - name: Test Each Package Trait if: matrix.traits working-directory: spm/${{ matrix.name }}/App @@ -402,11 +416,6 @@ jobs: TRAITS: ${{ matrix.traits }} run: | IFS=',' read -ra traits <<< "$TRAITS" - disabled=() - for trait in "${traits[@]}"; do - disabled+=("--//Packages:trait_${PACKAGE}_${trait}=false") - done for trait in "${traits[@]}"; do - bazel test //... "${disabled[@]}" \ - "--//Packages:trait_${PACKAGE}_${trait}=true" + bazel test //... "--config=${PACKAGE}.${trait}" done diff --git a/spm/README.md b/spm/README.md index 6bbd9b7..5c2f829 100644 --- a/spm/README.md +++ b/spm/README.md @@ -7,16 +7,26 @@ would not tell us anything. | package | what it holds bazelize to | |---|---| +| `ArtifactBundle` | a binary target that ships a program: the plugin's tool comes out of an `.artifactbundle`, and the source it writes is what the target compiles | +| `BinaryTarget` | a local zipped XCFramework, which links statically | | `BuildToolPlugin` | a build tool plugin and the tool it runs: the test target only compiles through a source the plugin generates | -| `Clang` | a C-family target: public headers somewhere of its own, a private header search path, defines with and without a value, C++ interoperability, and the bundle a C target reaches without importing anything | +| `Clang` | the C-family shapes: Objective-C, Objective-C++, assembly, public headers somewhere of its own, a private header search path, defines with and without a value, a module map the package ships, C and C++ `unsafeFlags`, C++ interoperability, and the bundle a C target reaches without importing anything | | `CommandPlugin` | a plugin that is run on demand rather than while building, which nothing in a build may try to run | +| `ConfigurationCondition` | settings conditional on debug and release, which the build decides rather than the generator | | `DependencyCondition` | dependencies conditional on a platform and on a trait: what the condition excludes must not be built | +| `DependencyShape` | how a dependency is named: `.target`, by name, `.product`, a package whose identity is neither its directory nor its manifest name, and `moduleAliases` renaming a module that would otherwise clash | | `Macro` | a macro target, loaded by the compiler while the target beside it is compiled | +| `PrebuildPlugin` | a plugin's `.prebuildCommand`, which names a directory rather than the files it writes | +| `ProductShapes` | products over several targets: `.static`, `.dynamic` and automatic libraries, a product named after one of its own targets, an executable product under another name, and a `Snippets/` program | +| `RemoteXCFramework` | a remote XCFramework SwiftPM fetches, which links dynamically | +| `SwiftSettings` | every `SwiftSetting` and `LinkerSetting`: language mode, upcoming and experimental features, strict memory safety, default isolation, unsafe flags, a linked library, a linked framework, and linker flags — each one observable, so a setting that went missing fails the build | +| `SystemLibrary` | system-library targets: `pkgConfig`, providers, and module maps whose `link` and `link framework` directives are the only thing that says what to link | | `Trait` | the package's own traits, a default one, and a dependency whose trait is turned on by name | +| `TraitGraph` | the whole trait graph: traits that enable traits, a condition naming several, and dependencies taking `.defaults`, nothing, or a named selection | | `TargetSources` | `sources:`, where a file beside the listed ones must not be compiled | | `TargetPath` | `path:`, where neither the target nor its tests are under `Sources/` | | `TargetExclude` | `exclude:`, where a named file and a named directory must not be compiled | -| `TargetResource` | the three resource rules: `.copy`, `.process`, and `.embedInCode` | +| `TargetResource` | every resource rule: `.copy`, `.process`, `.embedInCode`, an explicit localization, a `.lproj` directory, an asset catalogue, a xib, a shader, a string catalogue, and the `.docc` and `.xcprivacy` SwiftPM ignores | A package that must not compile a file says so in the file: it is a `#error(…)`, so a generator that globs too much fails loudly instead of @@ -35,6 +45,11 @@ bazel run //tools:list-trait # which traits its packages declare, and which a bazel test //... --config=. # …with one of them turned on ``` +`bazel test //... --config=.` is that package built with that +trait selected: the selection replaces the package's defaults and carries +whatever the trait enables, which is what `swift test --traits ` does. +The flags underneath are there for a build that wants some other combination. + `bazel run //:plugins` runs this workspace's build tool plugins and writes what they generate into `Packages//Generated/`. It is a separate step because a plugin is a program: Bazel builds it, and bazelize runs it as SwiftPM From bd69ddeb993e638c862339eda6b39cf6fb4ba8da Mon Sep 17 00:00:00 2001 From: yume190 Date: Tue, 22 Sep 2026 11:53:04 +0800 Subject: [PATCH 12/47] ci(spm): run on fixture changes, and run the programs a package builds The path filter left spm/ out, so a commit that only touched a fixture ran nothing at all. A package rule carries `manual`, so `bazel test //...` never builds an executable product, a snippet, or a bundled tool: those are named and run. The trait probe says what each selection resolved to exactly, which a test asserting the graph is consistent cannot. --- .github/workflows/swift.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 8413614..2745ee0 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -7,12 +7,14 @@ on: paths: - 'Sources/**' - 'Tests/**' + - 'spm/**' - '.github/workflows/**' - 'Package.swift' pull_request: paths: - 'Sources/**' - 'Tests/**' + - 'spm/**' - '.github/workflows/**' - 'Package.swift' @@ -318,6 +320,10 @@ jobs: include: - name: ArtifactBundle plugins: true + # A package rule carries `manual`, so a wildcard build never picks + # one up: a program a package builds is named to be run. + programs: | + //Packages/ArtifactBundle:GreetTool -- /tmp/artifact-bundle.swift - name: BinaryTarget - name: BuildToolPlugin plugins: true @@ -333,6 +339,9 @@ jobs: - name: PrebuildPlugin plugins: true - name: ProductShapes + programs: | + //Packages/Products:renamed-tool + //Packages/Products:ProductSnippet - name: RemoteXCFramework - name: SwiftSettings - name: SystemLibrary @@ -392,6 +401,19 @@ jobs: working-directory: spm/${{ matrix.name }}/App run: bazel test //... + # A program is not a test: the rules that build one carry `manual`, so + # `//...` never builds it. Running it is what says it links and works. + - name: Run Package Programs + if: matrix.programs + working-directory: spm/${{ matrix.name }}/App + env: + PROGRAMS: ${{ matrix.programs }} + run: | + while read -r program; do + [ -n "$program" ] || continue + bazel run $program + done <<< "$PROGRAMS" + # Both ways round, and not only that both pass: a trait is a compilation # condition named after itself, so the flag reaching swiftc is the thing # to see. @@ -419,3 +441,13 @@ jobs: for trait in "${traits[@]}"; do bazel test //... "--config=${PACKAGE}.${trait}" done + + # What each selection resolves to, exactly: the closure of the trait + # asked for, and nothing of the defaults it replaced. + - name: Probe Trait Selections + if: matrix.name == 'TraitGraph' + working-directory: spm/TraitGraph/App + run: | + bazel run //Packages/TraitGraph:TraitProbe -- Leaf Middle Top + bazel run //Packages/TraitGraph:TraitProbe --config=TraitGraph.Middle -- Leaf Middle + bazel run //Packages/TraitGraph:TraitProbe --config=TraitGraph.Alternative -- Alternative From b7830695fc2901df34435d13b7ebe4a6cefd78fc Mon Sep 17 00:00:00 2001 From: yume190 Date: Tue, 22 Sep 2026 17:50:15 +0800 Subject: [PATCH 13/47] feat(spm): compile a package in the language mode it declares MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit swiftLanguageModes was read by nothing: a package that says it is Swift 5 was compiled as whatever the rules default to, which changes what its sources mean rather than whether they build. The mode is the newest one declared that the package's own tools version reaches — swift-syntax declares 5 and 6 from a 5.8 manifest and is Swift 5, where @retroactive on a type of the same package is a warning rather than an error — and a target naming a mode of its own still wins. --- .../SwiftPM/SwiftPM+Manifest.swift | 32 +++++++++++++++ .../SwiftPM/SwiftPM+Settings.swift | 40 ++++++++++++++++++- spm/SwiftSettings/Package.swift | 15 +++++-- .../LanguageModeOverride.swift | 7 ++++ .../Sources/SwiftSettings/SwiftSettings.swift | 5 ++- .../SwiftSettingsTests.swift | 6 +++ 6 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 spm/SwiftSettings/Sources/LanguageModeOverride/LanguageModeOverride.swift diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Manifest.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Manifest.swift index 065c396..715fd16 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Manifest.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Manifest.swift @@ -28,6 +28,10 @@ extension SwiftPM { let traits: [Trait] let cLanguageStandard: String? let cxxLanguageStandard: String? + /// `swiftLanguageModes`, which the dump still calls by its old name: + /// the language modes the package's targets compile in unless one of + /// them says otherwise. + let swiftLanguageModes: [String] /// `{"_version": "6.0.0"}`: which `PackageDescription` the manifest was /// written against, which a plugin has to be compiled against too. let toolsVersion: String @@ -42,6 +46,7 @@ extension SwiftPM { traits = container.list(Trait.self, "traits") cLanguageStandard = container.value(String.self, "cLanguageStandard") cxxLanguageStandard = container.value(String.self, "cxxLanguageStandard") + swiftLanguageModes = container.list(String.self, "swiftLanguageVersions") toolsVersion = container.value([String: String].self, "toolsVersion")?["_version"] ?? "5.9.0" } @@ -111,6 +116,11 @@ extension SwiftPM { /// A binary target's remote archive. let url: String? let checksum: String? + /// A system library target's `pkg-config` name, which is how the + /// machine is asked where that library is. + let pkgConfig: String? + /// What installs the library this target wraps, by package manager. + let providers: [Provider] init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: AnyKey.self) @@ -126,6 +136,28 @@ extension SwiftPM { pluginUsages = container.list(PluginUsage.self, "pluginUsages") url = container.value(String.self, "url") checksum = container.value(String.self, "checksum") + pkgConfig = container.value(String.self, "pkgConfig") + providers = container.list(Provider.self, "providers") + } + } + + /// `{"brew": [["zlib"]]}`: a package manager, and what it installs. + struct Provider: Decodable { + let manager: String + let packages: [String] + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: AnyKey.self) + + for key in container.allKeys { + guard let names = container.list([String].self, key.stringValue).first else { continue } + manager = key.stringValue + packages = names + return + } + + throw DecodingError.dataCorrupted( + .init(codingPath: decoder.codingPath, debugDescription: "Unknown provider")) } } diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Settings.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Settings.swift index 59edcac..318117a 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Settings.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Settings.swift @@ -28,7 +28,9 @@ extension SwiftPM.Generator { /// everything that depends on the library, and a project's own target /// must not compile as if it were a package — Xcode's generated asset /// symbols, for one, switch on `SWIFT_PACKAGE`. - always: Self.define("SWIFT_PACKAGE") + Self.aliasFlags(of: target), + always: Self.define("SWIFT_PACKAGE") + + Self.aliasFlags(of: target) + + Self.languageMode(of: target, in: package), /// A trait is a compilation condition of the package that declares /// it: `#if Fast` is how a source asks. Swift only — SwiftPM does /// not hand it to clang, so neither is it handed to `-Xcc`. @@ -62,6 +64,42 @@ extension SwiftPM.Generator { } } + /// `-swift-version` for a package that declares which language modes it + /// compiles in. + /// + /// SwiftPM builds such a package in the newest mode it declares that its + /// own tools version reaches: swift-syntax declares 5 and 6 from a 5.8 + /// manifest, and is compiled as Swift 5 — where `@retroactive` on a type of + /// the same package is a warning rather than an error. + /// + /// A target that names a mode of its own is compiled in that instead, so + /// the package's is only passed when the target says nothing. + private static func languageMode( + of target: SwiftPM.PackageTarget, + in package: SwiftPM.Package) -> [String] + { + let named: Set = ["swiftLanguageMode", "swiftLanguageVersion"] + let overridden = target.settings.contains { setting in + setting.tool == "swift" && named.contains(setting.name ?? "") + } + guard !overridden else { return [] } + + let tools = version(package.manifest.toolsVersion) + let newest = package.manifest.swiftLanguageModes + .filter { !tools.lexicographicallyPrecedes(version($0)) } + .max { left, right in + version(left).lexicographicallyPrecedes(version(right)) + } + guard let newest else { return [] } + + return ["-swift-version", newest] + } + + /// `6.1` as the numbers it compares by. + private static func version(_ value: String) -> [Int] { + value.split(separator: ".").map { Int($0) ?? 0 } + } + /// A package can name a system library or framework it needs; nothing else in /// the graph knows about it. func linkopts(of target: SwiftPM.PackageTarget, in package: SwiftPM.Package) -> Starlark.Value? { diff --git a/spm/SwiftSettings/Package.swift b/spm/SwiftSettings/Package.swift index 18646c1..4250faa 100644 --- a/spm/SwiftSettings/Package.swift +++ b/spm/SwiftSettings/Package.swift @@ -17,7 +17,8 @@ let package = Package( name: "SwiftSettings", swiftSettings: [ .define("MANIFEST_DEFINE"), - .swiftLanguageMode(.v5), + /// Not the language mode: that one the package declares, and + /// this target inherits. .enableUpcomingFeature("MemberImportVisibility"), .enableExperimentalFeature("Extern"), .strictMemorySafety(), @@ -39,7 +40,15 @@ let package = Package( "-Xlinker", "_swiftsettings_probe_alias", ]), ]), + /// The package's mode is the default, not the rule: a target that names + /// one of its own is compiled in that. + .target( + name: "LanguageModeOverride", + swiftSettings: [ + .swiftLanguageMode(.v6), + ]), .testTarget( name: "SwiftSettingsTests", - dependencies: ["SwiftSettings"]), - ]) + dependencies: ["LanguageModeOverride", "SwiftSettings"]), + ], + swiftLanguageModes: [.v5]) diff --git a/spm/SwiftSettings/Sources/LanguageModeOverride/LanguageModeOverride.swift b/spm/SwiftSettings/Sources/LanguageModeOverride/LanguageModeOverride.swift new file mode 100644 index 0000000..56105be --- /dev/null +++ b/spm/SwiftSettings/Sources/LanguageModeOverride/LanguageModeOverride.swift @@ -0,0 +1,7 @@ +/// The package compiles in Swift 5; this target said Swift 6, and a target's +/// own mode is the one it gets. +#if !swift(>=6.0) +#error("The target's own language mode must win over the package's") +#endif + +public let languageModeOverride = 6 diff --git a/spm/SwiftSettings/Sources/SwiftSettings/SwiftSettings.swift b/spm/SwiftSettings/Sources/SwiftSettings/SwiftSettings.swift index 27a5a35..55e3006 100644 --- a/spm/SwiftSettings/Sources/SwiftSettings/SwiftSettings.swift +++ b/spm/SwiftSettings/Sources/SwiftSettings/SwiftSettings.swift @@ -1,7 +1,8 @@ import Foundation -/// `swiftLanguageMode(.v5)`: the target compiles as Swift 5 whatever the -/// manifest's tools version is. +/// The package's `swiftLanguageModes`: this target names no mode of its own, +/// so it compiles in the one the package declares, whatever the manifest's +/// tools version is. #if swift(>=6.0) #error("The target must compile in Swift 5 language mode") #endif diff --git a/spm/SwiftSettings/Tests/SwiftSettingsTests/SwiftSettingsTests.swift b/spm/SwiftSettings/Tests/SwiftSettingsTests/SwiftSettingsTests.swift index f0c9d62..4207251 100644 --- a/spm/SwiftSettings/Tests/SwiftSettingsTests/SwiftSettingsTests.swift +++ b/spm/SwiftSettings/Tests/SwiftSettingsTests/SwiftSettingsTests.swift @@ -1,3 +1,4 @@ +import LanguageModeOverride import SwiftSettings import Testing @@ -18,3 +19,8 @@ func linkedLibraryAndFrameworkAreLinked() { func linkerUnsafeFlagsReachTheLink() { #expect(SettingProbe.aliased() == 42) } + +@Test @MainActor +func aTargetsOwnLanguageModeWinsOverThePackages() { + #expect(languageModeOverride == 6) +} From 7736d3f23b855e27aa927c887f6be5b07fd23f64 Mon Sep 17 00:00:00 2001 From: yume190 Date: Tue, 22 Sep 2026 17:50:15 +0800 Subject: [PATCH 14/47] feat(spm): ask pkg-config where a system library is MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pkgConfig and providers were read by nothing, so a library that is not where the compiler already looks was never found: only what the module map named reached the build. pkg-config is asked the same question SwiftPM asks it. An include path is linked into the workspace and carried as `includes`, which reaches whoever imports the module — `copts` would not — a define becomes `defines`, and the libraries join what the module map named, deduplicated. When pkg-config knows nothing, the note names the providers that install it. --- Sources/BazelRules/Rules+Cc.swift | 2 + .../SwiftPM/SwiftPM+SystemLibrary.swift | 161 +++++++++++++++++- spm/SystemLibrary/Package.swift | 17 +- .../Sources/CGreet/module.modulemap | 4 + spm/SystemLibrary/Sources/CGreet/shim.h | 2 + .../Sources/SystemLibrary/SystemLibrary.swift | 6 + .../SystemLibraryTests.swift | 5 + spm/SystemLibrary/vendor/include/greet.h | 7 + .../vendor/pkgconfig/bazelize-greet.pc | 7 + 9 files changed, 205 insertions(+), 6 deletions(-) create mode 100644 spm/SystemLibrary/Sources/CGreet/module.modulemap create mode 100644 spm/SystemLibrary/Sources/CGreet/shim.h create mode 100644 spm/SystemLibrary/vendor/include/greet.h create mode 100644 spm/SystemLibrary/vendor/pkgconfig/bazelize-greet.pc diff --git a/Sources/BazelRules/Rules+Cc.swift b/Sources/BazelRules/Rules+Cc.swift index 9ce070e..b263ce3 100644 --- a/Sources/BazelRules/Rules+Cc.swift +++ b/Sources/BazelRules/Rules+Cc.swift @@ -49,6 +49,7 @@ extension Rules.Cc { hdrs: Starlark.Value? = nil, deps: Starlark.Value? = nil, copts: Starlark.Value? = nil, + defines: [String]? = nil, includes: [String]? = nil, linkopts: Starlark.Value? = nil, tags: [String]? = nil, @@ -63,6 +64,7 @@ extension Rules.Cc { if let hdrs { "hdrs" => hdrs } if let deps { "deps" => deps } if let copts { "copts" => copts } + if let defines { "defines" => defines } if let includes { "includes" => includes } if let linkopts { "linkopts" => linkopts } if let tags { "tags" => tags } diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+SystemLibrary.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+SystemLibrary.swift index 76c5650..fa4323c 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+SystemLibrary.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+SystemLibrary.swift @@ -36,6 +36,15 @@ extension SwiftPM.Generator { let name = ruleName(of: target.name, in: package) let hint = "\(name)_interop" let files = relativeFiles(of: target, in: package, prefix: prefix) + let pkgConfig = self.pkgConfig(of: target, in: package) + /// An include path of the machine's is linked into the workspace, so + /// what the module needs is a directory of this build rather than a + /// path only this machine has — and `includes` reaches whoever imports + /// the module, which `copts` would not. + let included = (try? materialize( + includePaths: pkgConfig.includePaths, + of: target, + at: root)) ?? [] builder.load(loadableRule: Rules.Swift.swift_interop_hint) builder.call( @@ -49,17 +58,163 @@ extension SwiftPM.Generator { Rules.Cc.Call.cc_library( name: name, aspect_hints: .build { [Starlark.Label.named(":\(hint)")] }, - hdrs: matching(Self.headerExtensions.map { "\(prefix)/**/*.\($0)" }, files) + hdrs: (matching(Self.headerExtensions.map { "\(prefix)/**/*.\($0)" }, files) + + included.map { "\($0)/**" }) .nonEmpty .map { Starlark.glob($0) }, - includes: [prefix], - linkopts: Self.linkopts(moduleMap: root + moduleMap).starlark, + copts: pkgConfig.otherFlags.nonEmpty?.starlark, + defines: pkgConfig.defines.nonEmpty, + includes: [prefix] + included, + /// What the module map names, and what `pkg-config` says: a + /// library installed somewhere of its own is only found through + /// the second, and both usually name the same `-l`. + linkopts: Self.deduplicated( + Self.linkopts(moduleMap: root + moduleMap) + pkgConfig.linkerFlags).starlark, tags: Self.manual, visibility: .public)) return true } + /// One link per include path `pkg-config` reported, under the target's own + /// artifact directory, answering where they sit in the workspace. + private func materialize( + includePaths: [String], + of target: SwiftPM.PackageTarget, + at root: Path) throws -> [String] + { + guard !includePaths.isEmpty else { return [] } + + let directory = root + Self.artifactsRoot + target.name + try directory.mkpath() + + return try includePaths.enumerated().compactMap { index, path in + let source = Path(path).normalize() + guard source.isDirectory else { return nil } + + let relative = "\(Self.artifactsRoot)/\(target.name)/include\(index)" + let link = root + relative + if link.isSymlink || link.exists { try? link.delete() } + try link.symlink(source) + + return relative + } + } + + /// Linker flags with the repeats dropped, in the order they were given. + /// + /// `-framework` takes what follows it, so flags are compared in the groups + /// they are passed in rather than one word at a time. + private static func deduplicated(_ flags: [String]) -> [String] { + var groups: [[String]] = [] + var index = flags.startIndex + + while index < flags.endIndex { + let flag = flags[index] + let takesValue = flag == "-framework" || flag == "-weak_framework" || flag == "-Xlinker" + + if takesValue, flags.index(after: index) < flags.endIndex { + groups.append([flag, flags[flags.index(after: index)]]) + index = flags.index(index, offsetBy: 2) + } else { + groups.append([flag]) + index = flags.index(after: index) + } + } + + var seen: Set<[String]> = [] + return groups.filter { seen.insert($0).inserted }.flatMap { $0 } + } + + /// What `pkg-config` says about the library a target wraps, split the way + /// the rule takes it. + /// + /// SwiftPM asks the same question and for the same reason: a module map + /// names a header and a library, and only the machine knows where either + /// one is. When the answer is missing, the providers the manifest names are + /// what installs it, so that is what the note says. + private func pkgConfig( + of target: SwiftPM.PackageTarget, + in package: SwiftPM.Package) -> PkgConfig + { + guard let name = target.pkgConfig else { return .none } + + guard let compilerFlags = Self.pkgConfig(["--cflags", name]), + let linkerFlags = Self.pkgConfig(["--libs", name]) + else { + let installs = target.providers + .map { "\($0.manager) \($0.packages.joined(separator: " "))" } + .joined(separator: ", ") + + note(""" + \(package.directory)/\(target.name) wraps the \(name) library, which \ + pkg-config does not know about: what its module map names is all the \ + build has.\(installs.isEmpty ? "" : " The package says it comes from: \(installs).") + """) + return .none + } + + var pkgConfig = PkgConfig(linkerFlags: linkerFlags) + var pending: String? + + for flag in compilerFlags { + if let waiting = pending { + if waiting == "-I" { pkgConfig.includePaths.append(flag) } else { pkgConfig.defines.append(flag) } + pending = nil + continue + } + + switch true { + case flag == "-I", flag == "-D": + pending = flag + case flag.hasPrefix("-I"): + pkgConfig.includePaths.append(String(flag.dropFirst(2))) + case flag.hasPrefix("-D"): + pkgConfig.defines.append(String(flag.dropFirst(2))) + default: + pkgConfig.otherFlags.append(flag) + } + } + + return pkgConfig + } + + /// An include path reaches whoever imports the module, a define reaches + /// them too, and anything else is only this module's to compile with. + private struct PkgConfig { + var includePaths: [String] = [] + var defines: [String] = [] + var otherFlags: [String] = [] + var linkerFlags: [String] = [] + + static let none = PkgConfig() + } + + /// One `pkg-config` question, or `nil` when it cannot be answered. + private static func pkgConfig(_ arguments: [String]) -> [String]? { + let process = Process() + process.executableURL = URL(fileURLWithPath: "/usr/bin/env") + process.arguments = ["pkg-config"] + arguments + + let output = Pipe() + process.standardOutput = output + process.standardError = Pipe() + + do { + try process.run() + } catch { + return nil + } + + let data = output.fileHandleForReading.readDataToEndOfFile() + process.waitUntilExit() + guard process.terminationStatus == 0 else { return nil } + + return String(data: data, encoding: .utf8)? + .split(whereSeparator: \.isWhitespace) + .map(String.init) ?? [] + } + /// `link "z"` and `link framework "Cocoa"` in a module map are what the module /// needs at link time; nothing else in the manifest says so. static func linkopts(moduleMap: Path) -> [String] { diff --git a/spm/SystemLibrary/Package.swift b/spm/SystemLibrary/Package.swift index a675aab..298fb90 100644 --- a/spm/SystemLibrary/Package.swift +++ b/spm/SystemLibrary/Package.swift @@ -2,8 +2,10 @@ import PackageDescription -/// System-library targets whose module maps supply their headers and linker -/// input: one that links a library, one that links a framework. +/// System-library targets: one whose module map links a library, one whose +/// module map links a framework, and one whose headers only `pkg-config` knows +/// the way to — which is why `PKG_CONFIG_PATH` has to name `vendor/pkgconfig` +/// for this package to build at all. let package = Package( name: "SystemLibrary", products: [ @@ -20,9 +22,18 @@ let package = Package( /// `link framework` rather than `link`: the other half of what a module /// map can ask the linker for. .systemLibrary(name: "CSecurity"), + /// Nothing in the package says where this one's header is: `pkg-config` + /// does, and its `Cflags` are the whole reason the module compiles. + .systemLibrary( + name: "CGreet", + pkgConfig: "bazelize-greet", + providers: [ + .brew(["bazelize-greet"]), + .apt(["bazelize-greet-dev"]), + ]), .target( name: "SystemLibrary", - dependencies: ["CZlib", "CSecurity"]), + dependencies: ["CZlib", "CSecurity", "CGreet"]), .testTarget( name: "SystemLibraryTests", dependencies: ["SystemLibrary"]), diff --git a/spm/SystemLibrary/Sources/CGreet/module.modulemap b/spm/SystemLibrary/Sources/CGreet/module.modulemap new file mode 100644 index 0000000..fda1062 --- /dev/null +++ b/spm/SystemLibrary/Sources/CGreet/module.modulemap @@ -0,0 +1,4 @@ +module CGreet [system] { + header "shim.h" + export * +} diff --git a/spm/SystemLibrary/Sources/CGreet/shim.h b/spm/SystemLibrary/Sources/CGreet/shim.h new file mode 100644 index 0000000..5fa94ea --- /dev/null +++ b/spm/SystemLibrary/Sources/CGreet/shim.h @@ -0,0 +1,2 @@ +/// Not beside this file: the include path comes from `pkg-config`. +#include diff --git a/spm/SystemLibrary/Sources/SystemLibrary/SystemLibrary.swift b/spm/SystemLibrary/Sources/SystemLibrary/SystemLibrary.swift index 75f7358..6ae6aa1 100644 --- a/spm/SystemLibrary/Sources/SystemLibrary/SystemLibrary.swift +++ b/spm/SystemLibrary/Sources/SystemLibrary/SystemLibrary.swift @@ -1,3 +1,4 @@ +import CGreet import CSecurity import CZlib import Foundation @@ -13,4 +14,9 @@ public enum SystemLibrary { public static var securityMessage: String? { SecCopyErrorMessageString(errSecSuccess, nil).map { $0 as String } } + + /// From the module whose header `pkg-config` alone knows the way to. + public static var greeting: Int32 { + greet_value() + } } diff --git a/spm/SystemLibrary/Tests/SystemLibraryTests/SystemLibraryTests.swift b/spm/SystemLibrary/Tests/SystemLibraryTests/SystemLibraryTests.swift index 7149e3f..a6011c7 100644 --- a/spm/SystemLibrary/Tests/SystemLibraryTests/SystemLibraryTests.swift +++ b/spm/SystemLibrary/Tests/SystemLibraryTests/SystemLibraryTests.swift @@ -10,3 +10,8 @@ func aSystemLibraryImportsHeadersAndLinksItsLibrary() { func aModuleMapLinksTheFrameworkItNames() { #expect(SystemLibrary.securityMessage?.isEmpty == false) } + +@Test +func pkgConfigSuppliesTheIncludePath() { + #expect(SystemLibrary.greeting == 7) +} diff --git a/spm/SystemLibrary/vendor/include/greet.h b/spm/SystemLibrary/vendor/include/greet.h new file mode 100644 index 0000000..93ab1c7 --- /dev/null +++ b/spm/SystemLibrary/vendor/include/greet.h @@ -0,0 +1,7 @@ +#pragma once + +/// Header-only: what it answers is the proof the header was found at all, and +/// it is found only through the include path `pkg-config` reports. +static inline int greet_value(void) { + return 7; +} diff --git a/spm/SystemLibrary/vendor/pkgconfig/bazelize-greet.pc b/spm/SystemLibrary/vendor/pkgconfig/bazelize-greet.pc new file mode 100644 index 0000000..8568458 --- /dev/null +++ b/spm/SystemLibrary/vendor/pkgconfig/bazelize-greet.pc @@ -0,0 +1,7 @@ +prefix=${pcfiledir}/.. +includedir=${prefix}/include + +Name: bazelize-greet +Description: A header the fixture ships, which nothing but this file says where to find. +Version: 1.0.0 +Cflags: -I${includedir} From 90e5057e6e0cd36cb932d1c17c5e9f9a2586aa7c Mon Sep 17 00:00:00 2001 From: yume190 Date: Tue, 22 Sep 2026 17:50:16 +0800 Subject: [PATCH 15/47] feat(spm): find a package a registry supplied Only .build/checkouts was read, and a registry package is not a checkout: SwiftPM unpacks it under registry/downloads///, so its targets were missing and every product of it resolved to nothing. The version is the directory, so the package is named by the two above it, and a dependency naming it by registry identity or by name both resolve. --- .../SwiftPM/SwiftPM+Workspace.swift | 87 ++++++++++++++----- Tests/XcodeTests/PackageRegistryTests.swift | 87 +++++++++++++++++++ 2 files changed, 150 insertions(+), 24 deletions(-) create mode 100644 Tests/XcodeTests/PackageRegistryTests.swift diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Workspace.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Workspace.swift index dbb6e46..3c6c39e 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Workspace.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Workspace.swift @@ -80,7 +80,7 @@ extension SwiftPM { try await resolve(output: output) - let checkouts = output + ".build/checkouts" + let scratch = output + ".build" var manifests: [(root: Root, manifest: Manifest)] = [] var directoryByIdentity: [String: String] = [:] @@ -88,7 +88,7 @@ extension SwiftPM { /// `path:` dependencies of its own, and those are not in /// `.build/checkouts` either — the manifest that declares one is the /// only thing that knows where it is. - var pending = try roots(checkouts: checkouts, locals: locals) + var pending = try roots(scratch: scratch, locals: locals) var seen: Set = [] while !pending.isEmpty { @@ -97,7 +97,7 @@ extension SwiftPM { guard let manifest = try await manifest(at: root.path) else { continue } manifests.append((root, manifest)) - for identity in [manifest.name, root.directory, root.path.lastComponent] { + for identity in [manifest.name] + root.identities { directoryByIdentity[identity.lowercased()] = root.directory } @@ -185,42 +185,62 @@ extension SwiftPM { return enabled } - // MARK: Private + // MARK: Internal - private struct Root { + struct Root { let directory: String let path: Path let isLocal: Bool - } - - /// `swift package resolve` fetches what `Package.resolved` pins; without it - /// there are no checkouts to read. - private static func resolve(output: Path) async throws { - Log.codeGenerate.info("swift package resolve at \(output.string, privacy: .public)") - - let result = try await Subprocess.run( - .name("swift"), - arguments: Arguments(["package", "resolve"]), - workingDirectory: FilePath(output.string), - output: .discarded, - error: .currentStandardError) - - guard result.terminationStatus.isSuccess else { - throw SwiftPMError.resolveFailed(status: "\(result.terminationStatus)") + /// What a dependency can call this package, beside its manifest's own + /// name: the directory it is filed under, and — for one downloaded from + /// a registry — the name inside its scope. + let identities: [String] + + init(directory: String, path: Path, isLocal: Bool, identities: [String]? = nil) { + self.directory = directory + self.path = path + self.isLocal = isLocal + self.identities = identities ?? [directory, path.lastComponent] } } - /// Remote packages live in `.build/checkouts`; a local one is wherever its - /// manifest is, and is read in place. - private static func roots(checkouts: Path, locals: [Path]) throws -> [Root] { + /// Where the packages of a resolved workspace are. + /// + /// A package from source control is a checkout, a local one is read in + /// place, and one from a registry is an archive SwiftPM unpacked under + /// `registry/downloads///` — the version is the + /// directory, so what names the package is the two above it. + static func roots(scratch: Path, locals: [Path]) throws -> [Root] { var roots: [Root] = [] + let checkouts = scratch + "checkouts" if checkouts.exists { for child in try checkouts.children() where child.isDirectory { roots.append(.init(directory: child.lastComponent, path: child, isLocal: false)) } } + let downloads = scratch + "registry/downloads" + if downloads.exists { + for scope in try downloads.children() where scope.isDirectory { + for package in try scope.children() where package.isDirectory { + /// One version is resolved, and a stale one is left behind: + /// the one with a manifest is the one that was unpacked. + let versions = try package.children() + .filter { ($0 + "Package.swift").exists } + .sorted { $0.lastComponent < $1.lastComponent } + guard let version = versions.last else { continue } + + let identity = "\(scope.lastComponent).\(package.lastComponent)" + roots.append(.init( + directory: identity, + path: version, + isLocal: false, + identities: [identity, package.lastComponent])) + } + } + } + for path in locals { let root = path.absolute().normalize() guard root.exists else { continue } @@ -230,6 +250,25 @@ extension SwiftPM { return roots.sorted { $0.directory < $1.directory } } + // MARK: Private + + /// `swift package resolve` fetches what `Package.resolved` pins; without it + /// there are no checkouts to read. + private static func resolve(output: Path) async throws { + Log.codeGenerate.info("swift package resolve at \(output.string, privacy: .public)") + + let result = try await Subprocess.run( + .name("swift"), + arguments: Arguments(["package", "resolve"]), + workingDirectory: FilePath(output.string), + output: .discarded, + error: .currentStandardError) + + guard result.terminationStatus.isSuccess else { + throw SwiftPMError.resolveFailed(status: "\(result.terminationStatus)") + } + } + private static func manifest(at root: Path) async throws -> Manifest? { let result = try await Subprocess.run( .name("swift"), diff --git a/Tests/XcodeTests/PackageRegistryTests.swift b/Tests/XcodeTests/PackageRegistryTests.swift new file mode 100644 index 0000000..ca23a52 --- /dev/null +++ b/Tests/XcodeTests/PackageRegistryTests.swift @@ -0,0 +1,87 @@ +@testable import BazelizeKit +import Foundation +import PathKit +import Testing + +/// Where a resolved workspace's packages are found. A registry package is not a +/// checkout: SwiftPM unpacks it under `registry/downloads///`, +/// so the directory holding the manifest is a version number and what names the +/// package is the two directories above it. +struct PackageRegistryTests { + private func scratch(_ build: (Path) throws -> Void) throws -> Path { + let root = Path(NSTemporaryDirectory()) + "bazelize-registry-\(UUID().uuidString)" + try (root + "registry/downloads").mkpath() + try build(root) + return root + } + + private func write(manifest directory: Path) throws { + try directory.mkpath() + try (directory + "Package.swift").write("// swift-tools-version: 6.0\n") + } + + @Test + func aRegistryPackageIsNamedByItsScopeAndName() throws { + let scratch = try scratch { root in + try write(manifest: root + "registry/downloads/apple/swift-argument-parser/1.2.0") + } + defer { try? scratch.delete() } + + let roots = try SwiftPM.roots(scratch: scratch, locals: []) + + #expect(roots.count == 1) + #expect(roots.first?.directory == "apple.swift-argument-parser") + #expect(roots.first?.path.lastComponent == "1.2.0") + /// A dependency names it by its registry identity, and a product of it + /// by the package's own name: both have to resolve. + #expect(roots.first?.identities == ["apple.swift-argument-parser", "swift-argument-parser"]) + } + + @Test + func aVersionLeftBehindWithoutAManifestIsNotThePackage() throws { + let scratch = try scratch { root in + try (root + "registry/downloads/apple/swift-argument-parser/1.1.0").mkpath() + try write(manifest: root + "registry/downloads/apple/swift-argument-parser/1.2.0") + } + defer { try? scratch.delete() } + + let roots = try SwiftPM.roots(scratch: scratch, locals: []) + + #expect(roots.count == 1) + #expect(roots.first?.path.lastComponent == "1.2.0") + } + + @Test + func checkoutsAndRegistryDownloadsAreBothPackages() throws { + let scratch = try scratch { root in + try write(manifest: root + "checkouts/Yams") + try write(manifest: root + "registry/downloads/apple/swift-argument-parser/1.2.0") + } + defer { try? scratch.delete() } + + let roots = try SwiftPM.roots(scratch: scratch, locals: []) + + #expect(roots.map(\.directory) == ["Yams", "apple.swift-argument-parser"]) + } + + /// A registry dependency is dumped in a shape of its own — no URL, no path, + /// only the identity the registry files it under. + @Test + func aRegistryDependencyIsReadByItsIdentity() throws { + let json = """ + {"name": "Package", "platforms": [], "products": [], "targets": [], "dependencies": [ + {"registry": [{ + "identity": "apple.swift-argument-parser", + "requirement": {"range": [{"lowerBound": "1.2.0", "upperBound": "2.0.0"}]}, + "traits": [{"name": "default"}] + }]} + ]} + """ + + let manifest = try JSONDecoder().decode(SwiftPM.Manifest.self, from: Data(json.utf8)) + + #expect(manifest.dependencies.map(\.identity) == ["apple.swift-argument-parser"]) + #expect(manifest.dependencies.first?.traits == ["default"]) + #expect(manifest.dependencies.first?.path == nil) + } +} From 465e5e04bca8b24b78fa99968f4f8b1cc984a27a Mon Sep 17 00:00:00 2001 From: yume190 Date: Tue, 22 Sep 2026 17:50:16 +0800 Subject: [PATCH 16/47] ci(spm): tell the generator where the fixture's pkg-config file is --- .github/workflows/swift.yml | 5 +++++ spm/README.md | 18 ++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 2745ee0..bc68560 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -376,7 +376,12 @@ jobs: with: name: bazelize + # A package that wraps a system library is found through `pkg-config`, + # and a fixture that ships its own `.pc` is only found when that file's + # directory is on the path. - name: Bazel Generation + env: + PKG_CONFIG_PATH: ${{ github.workspace }}/spm/${{ matrix.name }}/vendor/pkgconfig run: | chmod +x bazelize ./bazelize --project "spm/${{ matrix.name }}" --output "spm/${{ matrix.name }}/App" diff --git a/spm/README.md b/spm/README.md index 5c2f829..dc94cbc 100644 --- a/spm/README.md +++ b/spm/README.md @@ -19,8 +19,8 @@ would not tell us anything. | `PrebuildPlugin` | a plugin's `.prebuildCommand`, which names a directory rather than the files it writes | | `ProductShapes` | products over several targets: `.static`, `.dynamic` and automatic libraries, a product named after one of its own targets, an executable product under another name, and a `Snippets/` program | | `RemoteXCFramework` | a remote XCFramework SwiftPM fetches, which links dynamically | -| `SwiftSettings` | every `SwiftSetting` and `LinkerSetting`: language mode, upcoming and experimental features, strict memory safety, default isolation, unsafe flags, a linked library, a linked framework, and linker flags — each one observable, so a setting that went missing fails the build | -| `SystemLibrary` | system-library targets: `pkgConfig`, providers, and module maps whose `link` and `link framework` directives are the only thing that says what to link | +| `SwiftSettings` | every `SwiftSetting` and `LinkerSetting`, plus the package's own `swiftLanguageModes` and a target that overrides it: upcoming and experimental features, strict memory safety, default isolation, unsafe flags, a linked library, a linked framework, and linker flags — each one observable, so a setting that went missing fails the build | +| `SystemLibrary` | system-library targets: module maps whose `link` and `link framework` directives say what to link, and a library whose header only `pkg-config` knows the way to — which is why this one needs `PKG_CONFIG_PATH` (see below) | | `Trait` | the package's own traits, a default one, and a dependency whose trait is turned on by name | | `TraitGraph` | the whole trait graph: traits that enable traits, a condition naming several, and dependencies taking `.defaults`, nothing, or a named selection | | `TargetSources` | `sources:`, where a file beside the listed ones must not be compiled | @@ -45,6 +45,20 @@ bazel run //tools:list-trait # which traits its packages declare, and which a bazel test //... --config=. # …with one of them turned on ``` +A package that wraps a system library is found through `pkg-config`, and the +one `SystemLibrary` ships is in the fixture rather than on the machine, so both +SwiftPM and bazelize need to be told where it is: + +```sh +cd spm/SystemLibrary +export PKG_CONFIG_PATH="$PWD/vendor/pkgconfig" +swift test +bazelize --project . --output App +``` + +The flags are read when the workspace is generated, so only that command needs +the variable — the build does not. + `bazel test //... --config=.` is that package built with that trait selected: the selection replaces the package's defaults and carries whatever the trait enables, which is what `swift test --traits ` does. From 5e5854dcec053dd328df6f98db381916c73b092d Mon Sep 17 00:00:00 2001 From: yume190 Date: Tue, 22 Sep 2026 17:51:32 +0800 Subject: [PATCH 17/47] ci: name each matrix lane after what it builds --- .github/workflows/swift.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index bc68560..e4a7824 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -201,6 +201,7 @@ jobs: # The corpus is not in this repository: `app/` and `spm/` are ignored, so each # lane clones what it measures at the revision it was measured against. IntegrateApp: + name: IntegrateApp (${{ matrix.name }}) runs-on: macos-26 needs: [artifact] timeout-minutes: 120 @@ -311,6 +312,7 @@ jobs: # One lane per SwiftPM fixture in `spm/`: each package is one thing SwiftPM # can do, and its tests only pass if that thing was generated correctly. IntegratePackage: + name: IntegratePackage (${{ matrix.name }}) runs-on: macos-26 needs: [artifact] timeout-minutes: 60 From fad9e69a08a8150d4443adb7f3a60dfdbe1a2b9a Mon Sep 17 00:00:00 2001 From: yume190 Date: Tue, 22 Sep 2026 18:31:32 +0800 Subject: [PATCH 18/47] test(spm): pin the language standards, and run XCTest too cLanguageStandard and cxxLanguageStandard were generated as -std= and asserted by nothing; now the sources say which standard they are compiled under. Every fixture's tests were swift-testing, so the XCTest path through macos_unit_test had never been built. --- spm/Clang/Sources/CObject/CObject.m | 6 ++++++ spm/Clang/Sources/CxxLib/CxxLib.cpp | 9 +++++++++ spm/Clang/Tests/ClangTests/ClangXCTests.swift | 17 +++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 spm/Clang/Tests/ClangTests/ClangXCTests.swift diff --git a/spm/Clang/Sources/CObject/CObject.m b/spm/Clang/Sources/CObject/CObject.m index 5a482ff..85488e3 100644 --- a/spm/Clang/Sources/CObject/CObject.m +++ b/spm/Clang/Sources/CObject/CObject.m @@ -5,6 +5,12 @@ #error "C unsafe flags must reach Objective-C sources" #endif +/// `cLanguageStandard: .c11`, which is the package's to declare and nothing +/// else's: without it the compiler picks its own default. +#if __STDC_VERSION__ != 201112L +#error "The package's C language standard must reach the compiler" +#endif + @implementation CObject + (int)value { diff --git a/spm/Clang/Sources/CxxLib/CxxLib.cpp b/spm/Clang/Sources/CxxLib/CxxLib.cpp index 4dc5245..36f8a69 100644 --- a/spm/Clang/Sources/CxxLib/CxxLib.cpp +++ b/spm/Clang/Sources/CxxLib/CxxLib.cpp @@ -1,5 +1,14 @@ #include "CxxLib.hpp" +/// `cxxLanguageStandard: .gnucxx17`. +#if __cplusplus != 201703L +#error "The package's C++ language standard must reach the compiler" +#endif + +#ifndef __GNUC__ +#error "A GNU standard is what the manifest named" +#endif + namespace demo { int twice(int value) { return value * 2; diff --git a/spm/Clang/Tests/ClangTests/ClangXCTests.swift b/spm/Clang/Tests/ClangTests/ClangXCTests.swift new file mode 100644 index 0000000..acb7c1e --- /dev/null +++ b/spm/Clang/Tests/ClangTests/ClangXCTests.swift @@ -0,0 +1,17 @@ +import Consumer +import XCTest + +/// The other test framework: a package's tests are XCTest as often as they are +/// swift-testing, and the two are bundled and run differently enough that a +/// suite passing says nothing about the other kind. +final class ClangXCTests: XCTestCase { + func testDefinesReachTheCTarget() { + XCTAssertEqual(Consumer.value, 7) + XCTAssertTrue(Consumer.flag) + } + + func testAssemblyAndObjectiveCxxCompile() { + XCTAssertEqual(Consumer.assembly, 9) + XCTAssertEqual(Consumer.objectiveCxxLength, 6) + } +} From 8fc24fb9938c1ddd2c87297896de25ad358d7769 Mon Sep 17 00:00:00 2001 From: yume190 Date: Tue, 22 Sep 2026 18:31:32 +0800 Subject: [PATCH 19/47] test(spm): decide a platform while the rules are written A trait and a configuration are the build's answer, so they are selects; a platform is not, because a package rule is compiled for whatever pulls it in. A setting behind macOS or iOS survives, one behind Linux or Windows is gone before anything is written, and the package's own deployment target is what its rules are built for. --- spm/Platform/Package.swift | 33 +++++++++++++++++++ spm/Platform/Sources/Platform/Platform.swift | 30 +++++++++++++++++ .../Tests/PlatformTests/PlatformTests.swift | 7 ++++ 3 files changed, 70 insertions(+) create mode 100644 spm/Platform/Package.swift create mode 100644 spm/Platform/Sources/Platform/Platform.swift create mode 100644 spm/Platform/Tests/PlatformTests/PlatformTests.swift diff --git a/spm/Platform/Package.swift b/spm/Platform/Package.swift new file mode 100644 index 0000000..d18bde8 --- /dev/null +++ b/spm/Platform/Package.swift @@ -0,0 +1,33 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +/// What a platform decides, and when it is decided. +/// +/// A trait or a build configuration is the build's answer, so it becomes a +/// `select`. A platform is not: a package rule is compiled for whatever pulls +/// it in, so a setting conditional on a platform nothing here builds is +/// dropped while the rules are written, and one conditional on a platform an +/// Apple toolchain does build is kept. +let package = Package( + name: "Platform", + platforms: [ + .macOS(.v13), + .iOS(.v16), + ], + products: [ + .library(name: "Platform", targets: ["Platform"]), + ], + targets: [ + .target( + name: "Platform", + swiftSettings: [ + .define("APPLE_PLATFORM", .when(platforms: [.macOS, .iOS])), + .define("MACOS_PLATFORM", .when(platforms: [.macOS])), + .define("LINUX_PLATFORM", .when(platforms: [.linux])), + .define("WINDOWS_PLATFORM", .when(platforms: [.windows])), + ]), + .testTarget( + name: "PlatformTests", + dependencies: ["Platform"]), + ]) diff --git a/spm/Platform/Sources/Platform/Platform.swift b/spm/Platform/Sources/Platform/Platform.swift new file mode 100644 index 0000000..e072b69 --- /dev/null +++ b/spm/Platform/Sources/Platform/Platform.swift @@ -0,0 +1,30 @@ +import Foundation + +/// Kept: macOS is a platform an Apple toolchain builds, and the one this is +/// compiled for. +#if !APPLE_PLATFORM +#error("A setting conditional on an Apple platform must apply") +#endif + +#if !MACOS_PLATFORM +#error("A setting conditional on macOS must apply to a macOS build") +#endif + +/// Dropped while the rules are written: nothing generated here is ever +/// compiled for Linux or Windows, so a setting behind one is not a `select` — +/// it is not there at all. +#if LINUX_PLATFORM +#error("A setting conditional on Linux must not apply") +#endif + +#if WINDOWS_PLATFORM +#error("A setting conditional on Windows must not apply") +#endif + +public enum Platform { + /// What the package says it needs, which is what the rules have to compile + /// it for: a build older than this would not have the API. + public static var deploymentTargetIsDeclared: Bool { + if #available(macOS 13.0, *) { true } else { false } + } +} diff --git a/spm/Platform/Tests/PlatformTests/PlatformTests.swift b/spm/Platform/Tests/PlatformTests/PlatformTests.swift new file mode 100644 index 0000000..a4d32c1 --- /dev/null +++ b/spm/Platform/Tests/PlatformTests/PlatformTests.swift @@ -0,0 +1,7 @@ +import Platform +import Testing + +@Test +func theDeclaredDeploymentTargetIsWhatTheTargetIsBuiltFor() { + #expect(Platform.deploymentTargetIsDeclared) +} From 349ab6ce846f39a19e6be9ba9f594ada4a25068d Mon Sep 17 00:00:00 2001 From: yume190 Date: Tue, 22 Sep 2026 18:31:32 +0800 Subject: [PATCH 20/47] test(spm): run a plugin that belongs to another package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A target can name a plugin and the package it comes from, which is the only way a plugin is shared. Nothing exercised it, so the lookup that resolves the package — and the tool built out of it — was untested. --- spm/PluginDependency/Package.swift | 25 +++++++++++++++++++ .../PluginDependency/PluginDependency.swift | 4 +++ spm/PluginDependency/Stamping/Package.swift | 19 ++++++++++++++ .../Stamping/Plugins/Stamp/Plugin.swift | 18 +++++++++++++ .../Stamping/Sources/StampTool/main.swift | 13 ++++++++++ .../PluginDependencyTests.swift | 7 ++++++ 6 files changed, 86 insertions(+) create mode 100644 spm/PluginDependency/Package.swift create mode 100644 spm/PluginDependency/Sources/PluginDependency/PluginDependency.swift create mode 100644 spm/PluginDependency/Stamping/Package.swift create mode 100644 spm/PluginDependency/Stamping/Plugins/Stamp/Plugin.swift create mode 100644 spm/PluginDependency/Stamping/Sources/StampTool/main.swift create mode 100644 spm/PluginDependency/Tests/PluginDependencyTests/PluginDependencyTests.swift diff --git a/spm/PluginDependency/Package.swift b/spm/PluginDependency/Package.swift new file mode 100644 index 0000000..75d3024 --- /dev/null +++ b/spm/PluginDependency/Package.swift @@ -0,0 +1,25 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +/// A build tool plugin that belongs to another package: the target names the +/// plugin and the package it comes from, and neither the plugin nor the tool it +/// runs is anything this package builds. +let package = Package( + name: "PluginDependency", + products: [ + .library(name: "PluginDependency", targets: ["PluginDependency"]), + ], + dependencies: [ + .package(path: "Stamping"), + ], + targets: [ + .target( + name: "PluginDependency", + plugins: [ + .plugin(name: "Stamp", package: "Stamping"), + ]), + .testTarget( + name: "PluginDependencyTests", + dependencies: ["PluginDependency"]), + ]) diff --git a/spm/PluginDependency/Sources/PluginDependency/PluginDependency.swift b/spm/PluginDependency/Sources/PluginDependency/PluginDependency.swift new file mode 100644 index 0000000..317288f --- /dev/null +++ b/spm/PluginDependency/Sources/PluginDependency/PluginDependency.swift @@ -0,0 +1,4 @@ +/// `stamp` is not here: the plugin of the package next door writes it. +public func stampedValue() -> String { + stamp +} diff --git a/spm/PluginDependency/Stamping/Package.swift b/spm/PluginDependency/Stamping/Package.swift new file mode 100644 index 0000000..35001fa --- /dev/null +++ b/spm/PluginDependency/Stamping/Package.swift @@ -0,0 +1,19 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +/// The package next door, which ships a plugin as a product and the tool that +/// plugin runs. +let package = Package( + name: "Stamping", + products: [ + .plugin(name: "Stamp", targets: ["Stamp"]), + .executable(name: "StampTool", targets: ["StampTool"]), + ], + targets: [ + .plugin( + name: "Stamp", + capability: .buildTool(), + dependencies: ["StampTool"]), + .executableTarget(name: "StampTool"), + ]) diff --git a/spm/PluginDependency/Stamping/Plugins/Stamp/Plugin.swift b/spm/PluginDependency/Stamping/Plugins/Stamp/Plugin.swift new file mode 100644 index 0000000..8dc487d --- /dev/null +++ b/spm/PluginDependency/Stamping/Plugins/Stamp/Plugin.swift @@ -0,0 +1,18 @@ +import Foundation +import PackagePlugin + +@main +struct Stamp: BuildToolPlugin { + func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { + let output = context.pluginWorkDirectoryURL.appending(component: "Stamp.generated.swift") + let tool = try context.tool(named: "StampTool") + + return [ + .buildCommand( + displayName: "Stamp \(target.name)", + executable: tool.url, + arguments: [output.path(), target.name], + outputFiles: [output]), + ] + } +} diff --git a/spm/PluginDependency/Stamping/Sources/StampTool/main.swift b/spm/PluginDependency/Stamping/Sources/StampTool/main.swift new file mode 100644 index 0000000..3a84a4a --- /dev/null +++ b/spm/PluginDependency/Stamping/Sources/StampTool/main.swift @@ -0,0 +1,13 @@ +import Foundation + +let arguments = CommandLine.arguments +guard arguments.count == 3 else { + fatalError("usage: StampTool ") +} + +let source = """ +public let stamp = "stamped \(arguments[2])" + +""" + +try source.write(toFile: arguments[1], atomically: true, encoding: .utf8) diff --git a/spm/PluginDependency/Tests/PluginDependencyTests/PluginDependencyTests.swift b/spm/PluginDependency/Tests/PluginDependencyTests/PluginDependencyTests.swift new file mode 100644 index 0000000..cf3e517 --- /dev/null +++ b/spm/PluginDependency/Tests/PluginDependencyTests/PluginDependencyTests.swift @@ -0,0 +1,7 @@ +import PluginDependency +import Testing + +@Test +func aPluginFromAnotherPackageRan() { + #expect(stampedValue() == "stamped PluginDependency") +} From f8c550ea6faf4eb646264c7ebe09e378b1395570 Mon Sep 17 00:00:00 2001 From: yume190 Date: Tue, 22 Sep 2026 18:31:32 +0800 Subject: [PATCH 21/47] test(spm): copy a single file, bundle a test target, loop a link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A resource can be one file rather than a directory, a test target has resources like any other target, and a package can link a directory back into itself — which is walked forever by anything that follows links, so the sources are mirrored instead. --- spm/TargetResource/Package.swift | 10 +++++++++- .../Sources/TargetResource/TargetResource.swift | 6 ++++++ .../Sources/TargetResource/single.txt | 1 + .../Tests/TargetResourceTests/Fixtures/sample.txt | 1 + .../TargetResourceTests/TargetResourceTests.swift | 14 ++++++++++++++ spm/TargetSources/Package.swift | 4 +++- spm/TargetSources/Sources/TargetSources/Loop/self | 1 + 7 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 spm/TargetResource/Sources/TargetResource/single.txt create mode 100644 spm/TargetResource/Tests/TargetResourceTests/Fixtures/sample.txt create mode 120000 spm/TargetSources/Sources/TargetSources/Loop/self diff --git a/spm/TargetResource/Package.swift b/spm/TargetResource/Package.swift index 0fd87a7..3d52353 100644 --- a/spm/TargetResource/Package.swift +++ b/spm/TargetResource/Package.swift @@ -16,6 +16,9 @@ let package = Package( name: "TargetResource", resources: [ .copy("Copied"), + /// A single file rather than a directory: it lands at the + /// bundle's root under its own name. + .copy("single.txt"), .process("Processed"), .process("Localized", localization: .default), .embedInCode("Embedded/greeting.txt"), @@ -25,7 +28,12 @@ let package = Package( .process("Shader.metal"), .process("Catalog.xcstrings"), ]), + /// A test target has resources the same way any other target does, and + /// its own bundle to reach them through. .testTarget( name: "TargetResourceTests", - dependencies: ["TargetResource"]), + dependencies: ["TargetResource"], + resources: [ + .process("Fixtures"), + ]), ]) diff --git a/spm/TargetResource/Sources/TargetResource/TargetResource.swift b/spm/TargetResource/Sources/TargetResource/TargetResource.swift index 97b8fc3..de57bf2 100644 --- a/spm/TargetResource/Sources/TargetResource/TargetResource.swift +++ b/spm/TargetResource/Sources/TargetResource/TargetResource.swift @@ -7,6 +7,12 @@ public enum TargetResource { return try? String(contentsOf: url, encoding: .utf8).trimmed } + /// A copied file, which keeps its name and nothing above it. + public static var copiedFile: String? { + guard let url = Bundle.module.url(forResource: "single", withExtension: "txt") else { return nil } + return try? String(contentsOf: url, encoding: .utf8).trimmed + } + /// Processed: the file is in the bundle, and where it was is not part of /// how it is named. public static var processed: String? { diff --git a/spm/TargetResource/Sources/TargetResource/single.txt b/spm/TargetResource/Sources/TargetResource/single.txt new file mode 100644 index 0000000..315d84a --- /dev/null +++ b/spm/TargetResource/Sources/TargetResource/single.txt @@ -0,0 +1 @@ +single file diff --git a/spm/TargetResource/Tests/TargetResourceTests/Fixtures/sample.txt b/spm/TargetResource/Tests/TargetResourceTests/Fixtures/sample.txt new file mode 100644 index 0000000..28f6091 --- /dev/null +++ b/spm/TargetResource/Tests/TargetResourceTests/Fixtures/sample.txt @@ -0,0 +1 @@ +test fixture diff --git a/spm/TargetResource/Tests/TargetResourceTests/TargetResourceTests.swift b/spm/TargetResource/Tests/TargetResourceTests/TargetResourceTests.swift index 89a8493..a2e8ca5 100644 --- a/spm/TargetResource/Tests/TargetResourceTests/TargetResourceTests.swift +++ b/spm/TargetResource/Tests/TargetResourceTests/TargetResourceTests.swift @@ -1,3 +1,4 @@ +import Foundation import TargetResource import Testing @@ -6,6 +7,19 @@ func aCopiedResourceKeepsItsDirectory() { #expect(TargetResource.copied == "copied") } +@Test +func aCopiedFileKeepsItsName() { + #expect(TargetResource.copiedFile == "single file") +} + +@Test +func aTestTargetReachesItsOwnBundle() { + let url = Bundle.module.url(forResource: "sample", withExtension: "txt") + let contents = url.flatMap { try? String(contentsOf: $0, encoding: .utf8) } + + #expect(contents?.trimmingCharacters(in: .whitespacesAndNewlines) == "test fixture") +} + @Test func aProcessedResourceIsInTheBundle() { #expect(TargetResource.processed == "processed") diff --git a/spm/TargetSources/Package.swift b/spm/TargetSources/Package.swift index cbd8228..d54390e 100644 --- a/spm/TargetSources/Package.swift +++ b/spm/TargetSources/Package.swift @@ -3,7 +3,9 @@ import PackageDescription /// `sources:`: the target lists the files it compiles, and everything else -/// under its directory is not one of them. +/// under its directory is not one of them — including `Loop/self`, a link back +/// to the target's own directory, which a generator that follows links would +/// walk forever and a glob cannot see through at all. let package = Package( name: "TargetSources", products: [ diff --git a/spm/TargetSources/Sources/TargetSources/Loop/self b/spm/TargetSources/Sources/TargetSources/Loop/self new file mode 120000 index 0000000..c25bddb --- /dev/null +++ b/spm/TargetSources/Sources/TargetSources/Loop/self @@ -0,0 +1 @@ +../.. \ No newline at end of file From 7b421fbbd307643fd42db6c9cf39fe202b2b3238 Mon Sep 17 00:00:00 2001 From: yume190 Date: Tue, 22 Sep 2026 18:31:32 +0800 Subject: [PATCH 22/47] ci(spm): run what SwiftPM does beside what Bazel does A fixture holds bazelize to what SwiftPM does, and only one of the two sides was ever run. TargetResource is left out: the default build system does not generate what .embedInCode needs in this toolchain. --- .github/workflows/swift.yml | 17 +++++++++++++++++ spm/README.md | 8 +++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index e4a7824..ba0664b 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -338,6 +338,9 @@ jobs: config: DependencyCondition.Extras - name: DependencyShape - name: Macro + - name: Platform + - name: PluginDependency + plugins: true - name: PrebuildPlugin plugins: true - name: ProductShapes @@ -350,6 +353,10 @@ jobs: - name: TargetExclude - name: TargetPath - name: TargetResource + # `.embedInCode` is not generated by the default build system in + # this toolchain, which is SwiftPM's own bug rather than anything + # this package does: `swift test` cannot run for this one. + swiftpm: false - name: TargetSources - name: Trait traits: Fast,Slow @@ -378,6 +385,16 @@ jobs: with: name: bazelize + # What the fixture holds bazelize to is what SwiftPM does, so what + # SwiftPM does is run too: a fixture whose two sides disagree is a + # fixture that proves nothing. + - name: SwiftPM Test + if: ${{ matrix.swiftpm != false }} + working-directory: spm/${{ matrix.name }} + env: + PKG_CONFIG_PATH: ${{ github.workspace }}/spm/${{ matrix.name }}/vendor/pkgconfig + run: swift test + # A package that wraps a system library is found through `pkg-config`, # and a fixture that ships its own `.pc` is only found when that file's # directory is on the path. diff --git a/spm/README.md b/spm/README.md index dc94cbc..34a3039 100644 --- a/spm/README.md +++ b/spm/README.md @@ -10,12 +10,14 @@ would not tell us anything. | `ArtifactBundle` | a binary target that ships a program: the plugin's tool comes out of an `.artifactbundle`, and the source it writes is what the target compiles | | `BinaryTarget` | a local zipped XCFramework, which links statically | | `BuildToolPlugin` | a build tool plugin and the tool it runs: the test target only compiles through a source the plugin generates | -| `Clang` | the C-family shapes: Objective-C, Objective-C++, assembly, public headers somewhere of its own, a private header search path, defines with and without a value, a module map the package ships, C and C++ `unsafeFlags`, C++ interoperability, and the bundle a C target reaches without importing anything | +| `Clang` | the C-family shapes: Objective-C, Objective-C++, assembly, public headers somewhere of its own, a private header search path, defines with and without a value, a module map the package ships, C and C++ `unsafeFlags`, the package's language standards, C++ interoperability, the bundle a C target reaches without importing anything, and tests written in XCTest as well as swift-testing | | `CommandPlugin` | a plugin that is run on demand rather than while building, which nothing in a build may try to run | | `ConfigurationCondition` | settings conditional on debug and release, which the build decides rather than the generator | | `DependencyCondition` | dependencies conditional on a platform and on a trait: what the condition excludes must not be built | | `DependencyShape` | how a dependency is named: `.target`, by name, `.product`, a package whose identity is neither its directory nor its manifest name, and `moduleAliases` renaming a module that would otherwise clash | | `Macro` | a macro target, loaded by the compiler while the target beside it is compiled | +| `Platform` | what a platform decides and when: a setting conditional on a platform an Apple toolchain builds is kept, one conditional on Linux or Windows is gone before the rules are written, and the package's own deployment target is what its rules are built for | +| `PluginDependency` | a build tool plugin that belongs to another package, named with the package it comes from, running that package's tool | | `PrebuildPlugin` | a plugin's `.prebuildCommand`, which names a directory rather than the files it writes | | `ProductShapes` | products over several targets: `.static`, `.dynamic` and automatic libraries, a product named after one of its own targets, an executable product under another name, and a `Snippets/` program | | `RemoteXCFramework` | a remote XCFramework SwiftPM fetches, which links dynamically | @@ -23,10 +25,10 @@ would not tell us anything. | `SystemLibrary` | system-library targets: module maps whose `link` and `link framework` directives say what to link, and a library whose header only `pkg-config` knows the way to — which is why this one needs `PKG_CONFIG_PATH` (see below) | | `Trait` | the package's own traits, a default one, and a dependency whose trait is turned on by name | | `TraitGraph` | the whole trait graph: traits that enable traits, a condition naming several, and dependencies taking `.defaults`, nothing, or a named selection | -| `TargetSources` | `sources:`, where a file beside the listed ones must not be compiled | +| `TargetSources` | `sources:`, where a file beside the listed ones must not be compiled, and a link back to the target's own directory that must not be walked | | `TargetPath` | `path:`, where neither the target nor its tests are under `Sources/` | | `TargetExclude` | `exclude:`, where a named file and a named directory must not be compiled | -| `TargetResource` | every resource rule: `.copy`, `.process`, `.embedInCode`, an explicit localization, a `.lproj` directory, an asset catalogue, a xib, a shader, a string catalogue, and the `.docc` and `.xcprivacy` SwiftPM ignores | +| `TargetResource` | every resource rule: `.copy` of a directory and of a single file, `.process`, `.embedInCode`, an explicit localization, a `.lproj` directory, an asset catalogue, a xib, a shader, a string catalogue, a test target's own resources, and the `.docc` and `.xcprivacy` SwiftPM ignores | A package that must not compile a file says so in the file: it is a `#error(…)`, so a generator that globs too much fails loudly instead of From d621c326d3208f9ca3e1124e0e0d015497ec9927 Mon Sep 17 00:00:00 2001 From: yume190 Date: Tue, 22 Sep 2026 18:40:24 +0800 Subject: [PATCH 23/47] test(spm): find a target in a conventional root, and reach across a package A target that names no path is looked for in the directories SwiftPM looks in, `src/` among them, and nothing exercised any but `Sources/`. `package` access reaches the targets of one package, which is what the rules' `package_name` says: a build that leaves it out cannot compile the use of it, and now something would notice. --- spm/ProductShapes/Products/Package.swift | 4 +++- spm/ProductShapes/Products/Sources/First/First.swift | 5 +++++ .../Products/Sources/Second/Second.swift | 6 ++++++ .../Sources/ProductShapes/ProductShapes.swift | 6 ++++++ .../Tests/ProductShapesTests/ProductShapesTests.swift | 5 +++++ spm/TargetPath/Code/Tests/TargetPathTests.swift | 6 ++++++ spm/TargetPath/Package.swift | 11 ++++++++--- spm/TargetPath/src/Conventional/Conventional.swift | 3 +++ 8 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 spm/TargetPath/src/Conventional/Conventional.swift diff --git a/spm/ProductShapes/Products/Package.swift b/spm/ProductShapes/Products/Package.swift index 1fcadab..e766dd6 100644 --- a/spm/ProductShapes/Products/Package.swift +++ b/spm/ProductShapes/Products/Package.swift @@ -14,6 +14,8 @@ let package = Package( ], targets: [ .target(name: "First"), - .target(name: "Second"), + /// `package` access reaches across the targets of one package, so this + /// one links the other. + .target(name: "Second", dependencies: ["First"]), .executableTarget(name: "Tool"), ]) diff --git a/spm/ProductShapes/Products/Sources/First/First.swift b/spm/ProductShapes/Products/Sources/First/First.swift index 5e4c38a..cfe01aa 100644 --- a/spm/ProductShapes/Products/Sources/First/First.swift +++ b/spm/ProductShapes/Products/Sources/First/First.swift @@ -1 +1,6 @@ public let firstValue = 20 + +/// `package` access: visible to the targets of this package and to nothing +/// else. Which targets those are is the package's name, so a build that does +/// not say what package a target belongs to cannot compile the use of it. +package let firstPackageValue = 20 diff --git a/spm/ProductShapes/Products/Sources/Second/Second.swift b/spm/ProductShapes/Products/Sources/Second/Second.swift index cb84afd..dfdd698 100644 --- a/spm/ProductShapes/Products/Sources/Second/Second.swift +++ b/spm/ProductShapes/Products/Sources/Second/Second.swift @@ -1 +1,7 @@ +import First + public let secondValue = 22 + +/// From the target next door, reachable only because both are of the same +/// package. +public let sharedAcrossThePackage = firstPackageValue + 22 diff --git a/spm/ProductShapes/Sources/ProductShapes/ProductShapes.swift b/spm/ProductShapes/Sources/ProductShapes/ProductShapes.swift index bbca31f..6e9390a 100644 --- a/spm/ProductShapes/Sources/ProductShapes/ProductShapes.swift +++ b/spm/ProductShapes/Sources/ProductShapes/ProductShapes.swift @@ -5,4 +5,10 @@ public enum ProductShapes { public static var combinedValue: Int { firstValue + secondValue } + + /// What one target of that package could only read because it belongs to + /// the same package as the other. + public static var sharedValue: Int { + sharedAcrossThePackage + } } diff --git a/spm/ProductShapes/Tests/ProductShapesTests/ProductShapesTests.swift b/spm/ProductShapes/Tests/ProductShapesTests/ProductShapesTests.swift index 60d2ec6..9a45c81 100644 --- a/spm/ProductShapes/Tests/ProductShapesTests/ProductShapesTests.swift +++ b/spm/ProductShapes/Tests/ProductShapesTests/ProductShapesTests.swift @@ -5,3 +5,8 @@ import Testing func groupedProductsExposeEveryTarget() { #expect(ProductShapes.combinedValue == 42) } + +@Test +func packageAccessReachesTheTargetsOfThatPackage() { + #expect(ProductShapes.sharedValue == 42) +} diff --git a/spm/TargetPath/Code/Tests/TargetPathTests.swift b/spm/TargetPath/Code/Tests/TargetPathTests.swift index be62623..4a985c0 100644 --- a/spm/TargetPath/Code/Tests/TargetPathTests.swift +++ b/spm/TargetPath/Code/Tests/TargetPathTests.swift @@ -1,3 +1,4 @@ +import Conventional import TargetPath import Testing @@ -5,3 +6,8 @@ import Testing func targetLivesWhereThePathSays() { #expect(TargetPath.directory == "Code/Library") } + +@Test +func aTargetWithoutAPathIsFoundInAConventionalRoot() { + #expect(conventional == "src") +} diff --git a/spm/TargetPath/Package.swift b/spm/TargetPath/Package.swift index f0dfb59..4eee79e 100644 --- a/spm/TargetPath/Package.swift +++ b/spm/TargetPath/Package.swift @@ -2,18 +2,23 @@ import PackageDescription -/// `path:`: the target is not under `Sources/`. +/// Where a target's sources are: named by `path:`, or in one of the +/// directories SwiftPM looks in without being told — `src/` as much as +/// `Sources/`. let package = Package( name: "TargetPath", products: [ - .library(name: "TargetPath", targets: ["TargetPath"]), + .library(name: "TargetPath", targets: ["TargetPath", "Conventional"]), ], targets: [ .target( name: "TargetPath", path: "Code/Library"), + /// No `path:`: this one is found because `src/` is a place + /// SwiftPM looks. + .target(name: "Conventional"), .testTarget( name: "TargetPathTests", - dependencies: ["TargetPath"], + dependencies: ["TargetPath", "Conventional"], path: "Code/Tests"), ]) diff --git a/spm/TargetPath/src/Conventional/Conventional.swift b/spm/TargetPath/src/Conventional/Conventional.swift new file mode 100644 index 0000000..785ae6c --- /dev/null +++ b/spm/TargetPath/src/Conventional/Conventional.swift @@ -0,0 +1,3 @@ +/// Not `Sources/`: `src/` is one of the directories SwiftPM looks in for a +/// target that names no path of its own. +public let conventional = "src" From 7a41430337c03c538121b077451afdd61f52c874 Mon Sep 17 00:00:00 2001 From: yume190 Date: Tue, 22 Sep 2026 18:40:24 +0800 Subject: [PATCH 24/47] fix: answer whether a prefix was there at all MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `delete(prefix:)` returned the string unchanged when the prefix was absent, so every `?? fallback` at a call site was dead code the compiler warned about — and two of those fallbacks meant something: a plugin's file outside the directory it was given was named by its absolute path, and a plist modifier that was not `default=` was pasted in verbatim. It answers `nil` now, the same as the Xcode module's own copy, which is left where it is: that module does not depend on Util, and Util drags Yams behind it. --- Sources/BazelizeKit/Codegen/Codegen+Plist.swift | 3 +++ Sources/BazelizeKit/SwiftPM/SwiftPM+Deployment.swift | 2 +- Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift | 9 +++++++-- Sources/Util/String+Extension.swift | 11 +++++++++-- Sources/Xcode/Loader/Xcode+FileLoader.swift | 5 ++++- 5 files changed, 24 insertions(+), 6 deletions(-) diff --git a/Sources/BazelizeKit/Codegen/Codegen+Plist.swift b/Sources/BazelizeKit/Codegen/Codegen+Plist.swift index a3b8308..505334c 100644 --- a/Sources/BazelizeKit/Codegen/Codegen+Plist.swift +++ b/Sources/BazelizeKit/Codegen/Codegen+Plist.swift @@ -251,6 +251,9 @@ extension String { guard !reserved.contains(key) else { continue } let modifier = Range(match.range(at: 2), in: self).map { String(self[$0]) } + /// `$(KEY:default=value)` is the only modifier that answers what a + /// missing setting is; any other one — `:lower`, say — leaves the + /// reference as it was rather than pasting the modifier in. guard let value = settings[key] ?? modifier?.delete(prefix: "default=") else { continue } result.replaceSubrange(wholeRange, with: value) diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Deployment.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Deployment.swift index c7a8840..fd72508 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Deployment.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Deployment.swift @@ -178,6 +178,6 @@ extension SwiftPM.Deployment { output: .string(limit: 1024 * 1024)) guard result.terminationStatus.isSuccess else { return "" } - return result.standardOutput ?? "" + return result.standardOutput } } diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift index 8539a00..f2e9ccc 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift @@ -391,9 +391,14 @@ extension SwiftPM { let output = pluginOutputs.output(of: target.name, in: package) let base = output?.root.normalize().string ?? "" for file in output?.files ?? [] { - let relative = file.normalize().string - .delete(prefix: base) + let path = file.normalize().string + /// A file the plugin wrote somewhere else is not this target's + /// to name. + guard let relative = path.delete(prefix: base)? .trimmingCharacters(in: ["/"]) + else { + continue + } guard !relative.isEmpty else { continue } /// A file with no extension is the one thing a pattern cannot diff --git a/Sources/Util/String+Extension.swift b/Sources/Util/String+Extension.swift index 9cc4cb3..1742229 100644 --- a/Sources/Util/String+Extension.swift +++ b/Sources/Util/String+Extension.swift @@ -28,8 +28,15 @@ extension String { } extension String { - public func delete(prefix: String) -> String { - guard hasPrefix(prefix) else { return self } + /// What is left of the string after the prefix, or `nil` when it does not + /// start with one. + /// + /// Not the string itself when the prefix is absent: a caller that wants + /// that says so — `path.delete(prefix: root) ?? path` — and one that wants + /// something else can have it, which is what a non-optional answer took + /// away. + public func delete(prefix: String) -> String? { + guard hasPrefix(prefix) else { return nil } return String(dropFirst(prefix.count)) } } diff --git a/Sources/Xcode/Loader/Xcode+FileLoader.swift b/Sources/Xcode/Loader/Xcode+FileLoader.swift index 337ee51..5ea19be 100644 --- a/Sources/Xcode/Loader/Xcode+FileLoader.swift +++ b/Sources/Xcode/Loader/Xcode+FileLoader.swift @@ -73,7 +73,6 @@ struct FileLoader { var relativePath: String? { let root = project.workspacePath.string.realPath guard let fullPath = fullPath?.realPath else { return nil } - guard fullPath.hasPrefix(root + "/") else { return nil } return fullPath.delete(prefix: root + "/") } @@ -320,6 +319,10 @@ func unique(_ values: [T], key: (T) -> String) -> [T] { } extension String { + /// What is left after the prefix, or `nil` when the string does not start + /// with one. Its own rather than `Util`'s: this module does not depend on + /// that one, and a four-line answer is cheaper than a module that drags + /// Yams in behind it. func delete(prefix: String) -> String? { guard hasPrefix(prefix) else { return nil } return String(dropFirst(prefix.count)) From 0411d18091740b69b1a92fff90e74629ad0fe057 Mon Sep 17 00:00:00 2001 From: yume190 Date: Tue, 22 Sep 2026 18:49:26 +0800 Subject: [PATCH 25/47] test(spm): fetch the program a binary target ships A local artifact bundle is a path in the package; a fetched one is checked against its checksum and unpacked into the workspace's artifact directory, which is the only place it can be looked for. The plugin asks for the tool by the name the bundle's info.json files it under, and what the program answered is what the target compiles. The SwiftPM side runs under --build-system native: the default one will not run a program a binary target downloaded, the same way it generates nothing for .embedInCode. --- .github/workflows/swift.yml | 16 ++++++---- spm/README.md | 7 +++++ spm/RemoteArtifactBundle/Package.swift | 31 +++++++++++++++++++ .../Plugins/RecordVersion/Plugin.swift | 26 ++++++++++++++++ .../RemoteArtifactBundle.swift | 5 +++ .../RemoteArtifactBundleTests.swift | 8 +++++ 6 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 spm/RemoteArtifactBundle/Package.swift create mode 100644 spm/RemoteArtifactBundle/Plugins/RecordVersion/Plugin.swift create mode 100644 spm/RemoteArtifactBundle/Sources/RemoteArtifactBundle/RemoteArtifactBundle.swift create mode 100644 spm/RemoteArtifactBundle/Tests/RemoteArtifactBundleTests/RemoteArtifactBundleTests.swift diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index ba0664b..b8e0f9d 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -347,16 +347,21 @@ jobs: programs: | //Packages/Products:renamed-tool //Packages/Products:ProductSnippet + - name: RemoteArtifactBundle + plugins: true + # The default build system will not run a program a binary target + # downloaded, which is SwiftPM's own limitation rather than + # anything this package does. + swiftpm: --build-system native - name: RemoteXCFramework - name: SwiftSettings - name: SystemLibrary - name: TargetExclude - name: TargetPath - name: TargetResource - # `.embedInCode` is not generated by the default build system in - # this toolchain, which is SwiftPM's own bug rather than anything - # this package does: `swift test` cannot run for this one. - swiftpm: false + # `.embedInCode` generates nothing under the default build system + # in this toolchain, for the same reason. + swiftpm: --build-system native - name: TargetSources - name: Trait traits: Fast,Slow @@ -389,11 +394,10 @@ jobs: # SwiftPM does is run too: a fixture whose two sides disagree is a # fixture that proves nothing. - name: SwiftPM Test - if: ${{ matrix.swiftpm != false }} working-directory: spm/${{ matrix.name }} env: PKG_CONFIG_PATH: ${{ github.workspace }}/spm/${{ matrix.name }}/vendor/pkgconfig - run: swift test + run: swift test ${{ matrix.swiftpm }} # A package that wraps a system library is found through `pkg-config`, # and a fixture that ships its own `.pc` is only found when that file's diff --git a/spm/README.md b/spm/README.md index 34a3039..09ea7ae 100644 --- a/spm/README.md +++ b/spm/README.md @@ -21,6 +21,7 @@ would not tell us anything. | `PrebuildPlugin` | a plugin's `.prebuildCommand`, which names a directory rather than the files it writes | | `ProductShapes` | products over several targets: `.static`, `.dynamic` and automatic libraries, a product named after one of its own targets, an executable product under another name, and a `Snippets/` program | | `RemoteXCFramework` | a remote XCFramework SwiftPM fetches, which links dynamically | +| `RemoteArtifactBundle` | a binary target whose program is fetched rather than found: SwiftPM checks the archive against its checksum and unpacks it where nothing local ever sits, and the plugin's tool comes out of there | | `SwiftSettings` | every `SwiftSetting` and `LinkerSetting`, plus the package's own `swiftLanguageModes` and a target that overrides it: upcoming and experimental features, strict memory safety, default isolation, unsafe flags, a linked library, a linked framework, and linker flags — each one observable, so a setting that went missing fails the build | | `SystemLibrary` | system-library targets: module maps whose `link` and `link framework` directives say what to link, and a library whose header only `pkg-config` knows the way to — which is why this one needs `PKG_CONFIG_PATH` (see below) | | `Trait` | the package's own traits, a default one, and a dependency whose trait is turned on by name | @@ -76,3 +77,9 @@ runtime. The generated `tools/bazel` wrapper also exposes them as `bazel list config|trait`. `App/` is generated, and is not checked in. + +Two packages need `swift test --build-system native`: the default build system +in this toolchain generates nothing for `.embedInCode` (`TargetResource`) and +will not run a program a binary target downloaded (`RemoteArtifactBundle`). +Neither is anything the packages themselves ask for, and the Bazel side of both +is built the same way as every other fixture. diff --git a/spm/RemoteArtifactBundle/Package.swift b/spm/RemoteArtifactBundle/Package.swift new file mode 100644 index 0000000..5d6ba09 --- /dev/null +++ b/spm/RemoteArtifactBundle/Package.swift @@ -0,0 +1,31 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +/// A binary target that ships a program and is fetched rather than found: +/// SwiftPM downloads the archive, checks it against the checksum, and unpacks +/// it into the workspace's artifact directory — which is not where a local one +/// is, and is the only place this one can be looked for. +let package = Package( + name: "RemoteArtifactBundle", + products: [ + .library(name: "RemoteArtifactBundle", targets: ["RemoteArtifactBundle"]), + ], + targets: [ + /// Named after the artifact the bundle holds: that is what a plugin + /// asks for it by. + .binaryTarget( + name: "periphery", + url: "https://github.com/peripheryapp/periphery/releases/download/3.8.0/periphery-3.8.0.artifactbundle.zip", + checksum: "a9c7bfb1483dde1f4b660bc64183161fde3b519b7392add51ec8fe8b846cf494"), + .plugin( + name: "RecordVersion", + capability: .buildTool(), + dependencies: ["periphery"]), + .target( + name: "RemoteArtifactBundle", + plugins: ["RecordVersion"]), + .testTarget( + name: "RemoteArtifactBundleTests", + dependencies: ["RemoteArtifactBundle"]), + ]) diff --git a/spm/RemoteArtifactBundle/Plugins/RecordVersion/Plugin.swift b/spm/RemoteArtifactBundle/Plugins/RecordVersion/Plugin.swift new file mode 100644 index 0000000..2c699b4 --- /dev/null +++ b/spm/RemoteArtifactBundle/Plugins/RecordVersion/Plugin.swift @@ -0,0 +1,26 @@ +import Foundation +import PackagePlugin + +@main +struct RecordVersion: BuildToolPlugin { + func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { + let output = context.pluginWorkDirectoryURL.appending(component: "Version.generated.swift") + /// The program the bundle ships, asked for by the name the bundle's + /// `info.json` files it under. + let tool = try context.tool(named: "periphery") + + return [ + .buildCommand( + displayName: "Record what the bundled program answers", + executable: URL(fileURLWithPath: "/bin/sh"), + arguments: [ + "-c", + """ + answer="$('\(tool.url.path())' version 2>&1)" + printf 'public let toolVersion = "%s"\\n' "$answer" > '\(output.path())' + """, + ], + outputFiles: [output]), + ] + } +} diff --git a/spm/RemoteArtifactBundle/Sources/RemoteArtifactBundle/RemoteArtifactBundle.swift b/spm/RemoteArtifactBundle/Sources/RemoteArtifactBundle/RemoteArtifactBundle.swift new file mode 100644 index 0000000..9337050 --- /dev/null +++ b/spm/RemoteArtifactBundle/Sources/RemoteArtifactBundle/RemoteArtifactBundle.swift @@ -0,0 +1,5 @@ +/// `toolVersion` is not here: it is what the downloaded program answered when +/// the plugin ran it. +public func versionOfTheBundledTool() -> String { + toolVersion +} diff --git a/spm/RemoteArtifactBundle/Tests/RemoteArtifactBundleTests/RemoteArtifactBundleTests.swift b/spm/RemoteArtifactBundle/Tests/RemoteArtifactBundleTests/RemoteArtifactBundleTests.swift new file mode 100644 index 0000000..d40710d --- /dev/null +++ b/spm/RemoteArtifactBundle/Tests/RemoteArtifactBundleTests/RemoteArtifactBundleTests.swift @@ -0,0 +1,8 @@ +import RemoteArtifactBundle +import Testing + +@Test +func theFetchedProgramRan() { + /// The version the manifest pins, which only the program itself can say. + #expect(versionOfTheBundledTool() == "3.8.0") +} From d5508e424a65cc3661606e0b5678c0ab53b62490 Mon Sep 17 00:00:00 2001 From: yume190 Date: Tue, 22 Sep 2026 18:54:31 +0800 Subject: [PATCH 26/47] test(spm): name the package a product belongs to, and what a plugin may do MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two packages can ship a product of the same name — a product name is the package's, not the graph's — and the dependency that names the package is what tells them apart. Nothing exercised that, so a resolver picking the first match would have passed. A command plugin's permissions are part of its shape, and no build may grant one or run it. --- spm/CommandPlugin/Package.swift | 7 ++++++- spm/DependencyShape/Alt/Package.swift | 15 +++++++++++++++ .../Alt/Sources/AltCore/AltCore.swift | 1 + spm/DependencyShape/Package.swift | 5 +++++ .../Sources/Consumer/Consumer.swift | 5 +++++ .../DependencyShapeTests.swift | 5 +++++ spm/README.md | 4 ++-- 7 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 spm/DependencyShape/Alt/Package.swift create mode 100644 spm/DependencyShape/Alt/Sources/AltCore/AltCore.swift diff --git a/spm/CommandPlugin/Package.swift b/spm/CommandPlugin/Package.swift index ce3ee86..bef9f41 100644 --- a/spm/CommandPlugin/Package.swift +++ b/spm/CommandPlugin/Package.swift @@ -15,7 +15,12 @@ let package = Package( .plugin( name: "Hello", capability: .command( - intent: .custom(verb: "hello", description: "Prints the package's greeting."))), + intent: .custom(verb: "hello", description: "Prints the package's greeting."), + /// What such a plugin has to ask for before it may do it, and + /// what a build must never grant it: nothing here runs it. + permissions: [ + .writeToPackageDirectory(reason: "Writes the greeting it prints."), + ])), .testTarget( name: "GreetingTests", dependencies: ["Greeting"]), diff --git a/spm/DependencyShape/Alt/Package.swift b/spm/DependencyShape/Alt/Package.swift new file mode 100644 index 0000000..d4da95d --- /dev/null +++ b/spm/DependencyShape/Alt/Package.swift @@ -0,0 +1,15 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +/// Its product is called `VendorCore` too: a product name belongs to the +/// package that ships it, so two packages can name one the same and whoever +/// uses both says which package it means. +let package = Package( + name: "Alt", + products: [ + .library(name: "VendorCore", targets: ["AltCore"]), + ], + targets: [ + .target(name: "AltCore"), + ]) diff --git a/spm/DependencyShape/Alt/Sources/AltCore/AltCore.swift b/spm/DependencyShape/Alt/Sources/AltCore/AltCore.swift new file mode 100644 index 0000000..fa26e16 --- /dev/null +++ b/spm/DependencyShape/Alt/Sources/AltCore/AltCore.swift @@ -0,0 +1 @@ +public let altCore = "alt" diff --git a/spm/DependencyShape/Package.swift b/spm/DependencyShape/Package.swift index f3ae14f..56c5100 100644 --- a/spm/DependencyShape/Package.swift +++ b/spm/DependencyShape/Package.swift @@ -16,6 +16,7 @@ let package = Package( /// dependency is written as neither. .package(name: "Vendor", path: "vendor-kit"), .package(path: "Other"), + .package(path: "Alt"), ], targets: [ .target(name: "Local"), @@ -28,6 +29,10 @@ let package = Package( "Helper", /// A product of another package. .product(name: "VendorCore", package: "Vendor"), + /// A product of a third package, called what the one above is + /// called: a product name is the package's, so the dependency + /// says which package it means. + .product(name: "VendorCore", package: "Alt"), /// A product whose module is called `Core` too, so it is /// renamed here. .product( diff --git a/spm/DependencyShape/Sources/Consumer/Consumer.swift b/spm/DependencyShape/Sources/Consumer/Consumer.swift index 97918c4..88d1f9b 100644 --- a/spm/DependencyShape/Sources/Consumer/Consumer.swift +++ b/spm/DependencyShape/Sources/Consumer/Consumer.swift @@ -1,6 +1,7 @@ /// `Core` is what the other package calls its module, and what this source /// calls it too: the alias renames what is compiled — `OtherCore` — so that /// name can clash with something else in the graph without this source caring. +import AltCore import Core import Helper import Local @@ -8,4 +9,8 @@ import VendorCore public enum Consumer { public static let everything = [local, helper, vendorCore, core] + + /// Two packages ship a product called `VendorCore`; these are the modules + /// behind each one. + public static let sameNamedProducts = [vendorCore, altCore] } diff --git a/spm/DependencyShape/Tests/DependencyShapeTests/DependencyShapeTests.swift b/spm/DependencyShape/Tests/DependencyShapeTests/DependencyShapeTests.swift index 832e4f8..387eaad 100644 --- a/spm/DependencyShape/Tests/DependencyShapeTests/DependencyShapeTests.swift +++ b/spm/DependencyShape/Tests/DependencyShapeTests/DependencyShapeTests.swift @@ -5,3 +5,8 @@ import Testing func everyDependencyShapeResolves() { #expect(Consumer.everything == ["local target", "helper target", "vendor-kit", "other package"]) } + +@Test +func aProductNameBelongsToThePackageThatShipsIt() { + #expect(Consumer.sameNamedProducts == ["vendor-kit", "alt"]) +} diff --git a/spm/README.md b/spm/README.md index 09ea7ae..cfa493f 100644 --- a/spm/README.md +++ b/spm/README.md @@ -11,10 +11,10 @@ would not tell us anything. | `BinaryTarget` | a local zipped XCFramework, which links statically | | `BuildToolPlugin` | a build tool plugin and the tool it runs: the test target only compiles through a source the plugin generates | | `Clang` | the C-family shapes: Objective-C, Objective-C++, assembly, public headers somewhere of its own, a private header search path, defines with and without a value, a module map the package ships, C and C++ `unsafeFlags`, the package's language standards, C++ interoperability, the bundle a C target reaches without importing anything, and tests written in XCTest as well as swift-testing | -| `CommandPlugin` | a plugin that is run on demand rather than while building, which nothing in a build may try to run | +| `CommandPlugin` | a plugin that is run on demand rather than while building, with the permissions such a plugin asks for, which nothing in a build may grant it or try to run | | `ConfigurationCondition` | settings conditional on debug and release, which the build decides rather than the generator | | `DependencyCondition` | dependencies conditional on a platform and on a trait: what the condition excludes must not be built | -| `DependencyShape` | how a dependency is named: `.target`, by name, `.product`, a package whose identity is neither its directory nor its manifest name, and `moduleAliases` renaming a module that would otherwise clash | +| `DependencyShape` | how a dependency is named: `.target`, by name, `.product`, a package whose identity is neither its directory nor its manifest name, two packages shipping a product of the same name, and `moduleAliases` renaming a module that would otherwise clash | | `Macro` | a macro target, loaded by the compiler while the target beside it is compiled | | `Platform` | what a platform decides and when: a setting conditional on a platform an Apple toolchain builds is kept, one conditional on Linux or Windows is gone before the rules are written, and the package's own deployment target is what its rules are built for | | `PluginDependency` | a build tool plugin that belongs to another package, named with the package it comes from, running that package's tool | From 8227a681ae90ae3f55bb516695e7a7c0f436e9b7 Mon Sep 17 00:00:00 2001 From: yume190 Date: Tue, 22 Sep 2026 19:07:56 +0800 Subject: [PATCH 27/47] test(spm): compile every resource kind, and localize more than once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A storyboard, a data model, a second .lproj and a shader that includes a header: the header is the load-bearing one — a shader compiles with the headers it includes, so they belong to the resource group it is in, and nothing had ever built one that did. StoryboardCompile, MomCompile, AssetCatalogCompile and MetalCompile all run now. A dependency's resources are its own: the package using it never sees that bundle, and every resource-bearing fixture until now was the root package. --- .../Sources/Consumer/Consumer.swift | 5 +++++ .../DependencyShapeTests.swift | 5 +++++ spm/DependencyShape/vendor-kit/Package.swift | 9 ++++++++- .../Sources/VendorCore/Resources/vendored.txt | 1 + .../Sources/VendorCore/VendorCore.swift | 8 ++++++++ spm/README.md | 5 ++--- spm/TargetResource/Package.swift | 2 ++ .../Sources/TargetResource/Main.storyboard | 18 ++++++++++++++++++ .../Model.xcdatamodeld/.xccurrentversion | 8 ++++++++ .../Model.xcdatamodel/contents | 6 ++++++ .../Sources/TargetResource/Shader.metal | 4 +++- .../Sources/TargetResource/ShaderCommon.h | 6 ++++++ .../TargetResource/TargetResource.swift | 11 +++++++++++ .../ja.lproj/Localizable.strings | 1 + .../TargetResourceTests.swift | 10 ++++++++++ 15 files changed, 94 insertions(+), 5 deletions(-) create mode 100644 spm/DependencyShape/vendor-kit/Sources/VendorCore/Resources/vendored.txt create mode 100644 spm/TargetResource/Sources/TargetResource/Main.storyboard create mode 100644 spm/TargetResource/Sources/TargetResource/Model.xcdatamodeld/.xccurrentversion create mode 100644 spm/TargetResource/Sources/TargetResource/Model.xcdatamodeld/Model.xcdatamodel/contents create mode 100644 spm/TargetResource/Sources/TargetResource/ShaderCommon.h create mode 100644 spm/TargetResource/Sources/TargetResource/ja.lproj/Localizable.strings diff --git a/spm/DependencyShape/Sources/Consumer/Consumer.swift b/spm/DependencyShape/Sources/Consumer/Consumer.swift index 88d1f9b..f3e1e9b 100644 --- a/spm/DependencyShape/Sources/Consumer/Consumer.swift +++ b/spm/DependencyShape/Sources/Consumer/Consumer.swift @@ -13,4 +13,9 @@ public enum Consumer { /// Two packages ship a product called `VendorCore`; these are the modules /// behind each one. public static let sameNamedProducts = [vendorCore, altCore] + + /// A dependency's own resource, which only that package's bundle holds. + public static var vendored: String? { + vendoredResource + } } diff --git a/spm/DependencyShape/Tests/DependencyShapeTests/DependencyShapeTests.swift b/spm/DependencyShape/Tests/DependencyShapeTests/DependencyShapeTests.swift index 387eaad..eae090e 100644 --- a/spm/DependencyShape/Tests/DependencyShapeTests/DependencyShapeTests.swift +++ b/spm/DependencyShape/Tests/DependencyShapeTests/DependencyShapeTests.swift @@ -10,3 +10,8 @@ func everyDependencyShapeResolves() { func aProductNameBelongsToThePackageThatShipsIt() { #expect(Consumer.sameNamedProducts == ["vendor-kit", "alt"]) } + +@Test +func aDependencyReachesItsOwnBundle() { + #expect(Consumer.vendored == "vendored") +} diff --git a/spm/DependencyShape/vendor-kit/Package.swift b/spm/DependencyShape/vendor-kit/Package.swift index bcc2d79..bf4dadd 100644 --- a/spm/DependencyShape/vendor-kit/Package.swift +++ b/spm/DependencyShape/vendor-kit/Package.swift @@ -11,5 +11,12 @@ let package = Package( .library(name: "VendorCore", targets: ["VendorCore"]), ], targets: [ - .target(name: "VendorCore"), + /// A package this one depends on has resources like any other, and a + /// bundle of its own to reach them through — which the package using + /// it never sees. + .target( + name: "VendorCore", + resources: [ + .process("Resources"), + ]), ]) diff --git a/spm/DependencyShape/vendor-kit/Sources/VendorCore/Resources/vendored.txt b/spm/DependencyShape/vendor-kit/Sources/VendorCore/Resources/vendored.txt new file mode 100644 index 0000000..297360c --- /dev/null +++ b/spm/DependencyShape/vendor-kit/Sources/VendorCore/Resources/vendored.txt @@ -0,0 +1 @@ +vendored diff --git a/spm/DependencyShape/vendor-kit/Sources/VendorCore/VendorCore.swift b/spm/DependencyShape/vendor-kit/Sources/VendorCore/VendorCore.swift index 867112d..e41ad7a 100644 --- a/spm/DependencyShape/vendor-kit/Sources/VendorCore/VendorCore.swift +++ b/spm/DependencyShape/vendor-kit/Sources/VendorCore/VendorCore.swift @@ -1 +1,9 @@ +import Foundation + public let vendorCore = "vendor-kit" + +/// Read out of this package's own bundle, not the one using it. +public var vendoredResource: String? { + guard let url = Bundle.module.url(forResource: "vendored", withExtension: "txt") else { return nil } + return try? String(contentsOf: url, encoding: .utf8).trimmingCharacters(in: .whitespacesAndNewlines) +} diff --git a/spm/README.md b/spm/README.md index cfa493f..909ef32 100644 --- a/spm/README.md +++ b/spm/README.md @@ -14,7 +14,7 @@ would not tell us anything. | `CommandPlugin` | a plugin that is run on demand rather than while building, with the permissions such a plugin asks for, which nothing in a build may grant it or try to run | | `ConfigurationCondition` | settings conditional on debug and release, which the build decides rather than the generator | | `DependencyCondition` | dependencies conditional on a platform and on a trait: what the condition excludes must not be built | -| `DependencyShape` | how a dependency is named: `.target`, by name, `.product`, a package whose identity is neither its directory nor its manifest name, two packages shipping a product of the same name, and `moduleAliases` renaming a module that would otherwise clash | +| `DependencyShape` | how a dependency is named: `.target`, by name, `.product`, a package whose identity is neither its directory nor its manifest name, two packages shipping a product of the same name, `moduleAliases` renaming a module that would otherwise clash, and a dependency's own resource bundle | | `Macro` | a macro target, loaded by the compiler while the target beside it is compiled | | `Platform` | what a platform decides and when: a setting conditional on a platform an Apple toolchain builds is kept, one conditional on Linux or Windows is gone before the rules are written, and the package's own deployment target is what its rules are built for | | `PluginDependency` | a build tool plugin that belongs to another package, named with the package it comes from, running that package's tool | @@ -28,8 +28,7 @@ would not tell us anything. | `TraitGraph` | the whole trait graph: traits that enable traits, a condition naming several, and dependencies taking `.defaults`, nothing, or a named selection | | `TargetSources` | `sources:`, where a file beside the listed ones must not be compiled, and a link back to the target's own directory that must not be walked | | `TargetPath` | `path:`, where neither the target nor its tests are under `Sources/` | -| `TargetExclude` | `exclude:`, where a named file and a named directory must not be compiled | -| `TargetResource` | every resource rule: `.copy` of a directory and of a single file, `.process`, `.embedInCode`, an explicit localization, a `.lproj` directory, an asset catalogue, a xib, a shader, a string catalogue, a test target's own resources, and the `.docc` and `.xcprivacy` SwiftPM ignores | +| `TargetResource` | every resource rule: `.copy` of a directory and of a single file, `.process`, `.embedInCode`, an explicit localization, two `.lproj` directories, an asset catalogue, a xib, a storyboard, a data model, a shader that includes a header, a string catalogue, a test target's own resources, and the `.docc` and `.xcprivacy` SwiftPM ignores | A package that must not compile a file says so in the file: it is a `#error(…)`, so a generator that globs too much fails loudly instead of diff --git a/spm/TargetResource/Package.swift b/spm/TargetResource/Package.swift index 3d52353..9e7fc7c 100644 --- a/spm/TargetResource/Package.swift +++ b/spm/TargetResource/Package.swift @@ -27,6 +27,8 @@ let package = Package( .process("Panel.xib"), .process("Shader.metal"), .process("Catalog.xcstrings"), + .process("Main.storyboard"), + .process("Model.xcdatamodeld"), ]), /// A test target has resources the same way any other target does, and /// its own bundle to reach them through. diff --git a/spm/TargetResource/Sources/TargetResource/Main.storyboard b/spm/TargetResource/Sources/TargetResource/Main.storyboard new file mode 100644 index 0000000..f3c2701 --- /dev/null +++ b/spm/TargetResource/Sources/TargetResource/Main.storyboard @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/spm/TargetResource/Sources/TargetResource/Model.xcdatamodeld/.xccurrentversion b/spm/TargetResource/Sources/TargetResource/Model.xcdatamodeld/.xccurrentversion new file mode 100644 index 0000000..6e25a42 --- /dev/null +++ b/spm/TargetResource/Sources/TargetResource/Model.xcdatamodeld/.xccurrentversion @@ -0,0 +1,8 @@ + + + + + _XCCurrentVersionName + Model.xcdatamodel + + diff --git a/spm/TargetResource/Sources/TargetResource/Model.xcdatamodeld/Model.xcdatamodel/contents b/spm/TargetResource/Sources/TargetResource/Model.xcdatamodeld/Model.xcdatamodel/contents new file mode 100644 index 0000000..ef300ac --- /dev/null +++ b/spm/TargetResource/Sources/TargetResource/Model.xcdatamodeld/Model.xcdatamodel/contents @@ -0,0 +1,6 @@ + + + + + + diff --git a/spm/TargetResource/Sources/TargetResource/Shader.metal b/spm/TargetResource/Sources/TargetResource/Shader.metal index ab863d3..78ab760 100644 --- a/spm/TargetResource/Sources/TargetResource/Shader.metal +++ b/spm/TargetResource/Sources/TargetResource/Shader.metal @@ -1,8 +1,10 @@ #include +#include "ShaderCommon.h" + using namespace metal; kernel void doubleValues(device float *values [[buffer(0)]], uint index [[thread_position_in_grid]]) { - values[index] = values[index] * 2.0; + values[index] = values[index] * SHADER_FACTOR; } diff --git a/spm/TargetResource/Sources/TargetResource/ShaderCommon.h b/spm/TargetResource/Sources/TargetResource/ShaderCommon.h new file mode 100644 index 0000000..628468b --- /dev/null +++ b/spm/TargetResource/Sources/TargetResource/ShaderCommon.h @@ -0,0 +1,6 @@ +#pragma once + +/// Included by the shader beside it. A header is not a resource, but a shader +/// that includes one has to be compiled with it there, so it joins the group +/// the shader is in. +#define SHADER_FACTOR 2.0 diff --git a/spm/TargetResource/Sources/TargetResource/TargetResource.swift b/spm/TargetResource/Sources/TargetResource/TargetResource.swift index de57bf2..b06be33 100644 --- a/spm/TargetResource/Sources/TargetResource/TargetResource.swift +++ b/spm/TargetResource/Sources/TargetResource/TargetResource.swift @@ -37,6 +37,17 @@ public enum TargetResource { NSLocalizedString("lproj", bundle: .module, comment: "") } + /// The same key in whichever localization is asked for: a bundle with two + /// of them answers each, which one with only a fallback cannot. + public static func lprojLocalization(_ language: String) -> String? { + guard let path = Bundle.module.path(forResource: language, ofType: "lproj"), + let bundle = Bundle(path: path) + else { + return nil + } + return bundle.localizedString(forKey: "lproj", value: nil, table: nil) + } + /// What is in the bundle, by name. A platform resource is compiled by /// whoever builds it — `Assets.car`, `Panel.nib`, `default.metallib` — and /// copied as it is by whoever cannot, so both names are the same resource diff --git a/spm/TargetResource/Sources/TargetResource/ja.lproj/Localizable.strings b/spm/TargetResource/Sources/TargetResource/ja.lproj/Localizable.strings new file mode 100644 index 0000000..9656d23 --- /dev/null +++ b/spm/TargetResource/Sources/TargetResource/ja.lproj/Localizable.strings @@ -0,0 +1 @@ +"lproj" = "lproj から"; diff --git a/spm/TargetResource/Tests/TargetResourceTests/TargetResourceTests.swift b/spm/TargetResource/Tests/TargetResourceTests/TargetResourceTests.swift index a2e8ca5..0872283 100644 --- a/spm/TargetResource/Tests/TargetResourceTests/TargetResourceTests.swift +++ b/spm/TargetResource/Tests/TargetResourceTests/TargetResourceTests.swift @@ -36,6 +36,14 @@ func localizedResourcesAreFiledUnderTheirLocalization() { #expect(TargetResource.lprojLocalization == "from lproj") } +@Test +func everyLocalizationIsItsOwn() { + /// Not one answering for both: each `.lproj` is in the bundle, and each + /// says what it says. + #expect(TargetResource.lprojLocalization("en") == "from lproj") + #expect(TargetResource.lprojLocalization("ja") == "lproj から") +} + @Test func platformResourcesReachTheBundle() { let bundled = TargetResource.bundled @@ -46,6 +54,8 @@ func platformResourcesReachTheBundle() { #expect(has("Assets.car", "Assets.xcassets")) #expect(has("Panel.nib", "Panel.xib")) + #expect(has("Main.storyboardc", "Main.storyboard")) + #expect(has("Model.momd", "Model.xcdatamodeld")) #expect(has("default.metallib", "Shader.metal")) #expect(has("Catalog.xcstrings", "Catalog.strings")) } From c2166aa8ca9bc65aa7ca3dddda8a67b41a1b37b0 Mon Sep 17 00:00:00 2001 From: yume190 Date: Tue, 22 Sep 2026 19:07:56 +0800 Subject: [PATCH 28/47] fix(spm): say that a program's resource bundle is not built MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A resource bundle is built by whatever bundles it — an app or a test — and a program is neither, so an executable target's bundle rule produces something nothing puts anywhere and `Bundle.module` fatalErrors at run time. SwiftPM writes that bundle beside the program. Until the bundle is written where such a program can find it, the difference is said out loud instead of discovered by a crash. --- .../BazelizeKit/SwiftPM/SwiftPM+Resources.swift | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Resources.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Resources.swift index c8a551e..189152f 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Resources.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Resources.swift @@ -149,7 +149,19 @@ extension SwiftPM.Generator { tags: Self.manual)) switch kind { - case .swift, .executable, .test: + case .executable: + /// A resource bundle is built by whatever bundles it — an app or a + /// test — and a program is neither: the rule produces the bundle + /// for nothing to put anywhere, so `Bundle.module` finds nothing at + /// run time. SwiftPM writes the bundle beside the program, so the + /// difference is said out loud rather than discovered by a crash. + note(""" + \(package.directory)/\(target.name) is a program with resources, which \ + Bazel has nothing to bundle into: it is built, but `Bundle.module` finds \ + nothing when it runs. + """) + fallthrough + case .swift, .test: let accessor = "Generated/\(target.name)ResourceBundleAccessor.swift" try (root + accessor).write(Self.swiftAccessor(bundle: bundle)) return ResourceBundle( From 2760769408214a141d81f521990795817117f42a Mon Sep 17 00:00:00 2001 From: yume190 Date: Tue, 22 Sep 2026 21:19:29 +0800 Subject: [PATCH 29/47] ci(spm): build and test every package of a fixture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only the package bazelize was pointed at was built: the ones beside it are dependencies with sources of their own, and one that stopped building would have been found in whatever it broke instead of where it broke. LinuxOnly could not be built at all — its source was an #error, so the fixture proved its point by being unbuildable. What must not be linked now says so where it would have been linked, which is the target that would have imported it, so the package itself builds anywhere and the fixture still fails if the dependency ever came along. A package with no tests is built and not run; there is nothing to run. --- .github/workflows/swift.yml | 25 +++++++++++++++++-- .../Sources/LinuxOnly/LinuxOnly.swift | 6 ++--- .../Sources/Conditional/Conditional.swift | 7 ++++++ spm/README.md | 8 +++++- 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index b8e0f9d..355e6ff 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -393,11 +393,32 @@ jobs: # What the fixture holds bazelize to is what SwiftPM does, so what # SwiftPM does is run too: a fixture whose two sides disagree is a # fixture that proves nothing. - - name: SwiftPM Test + # + # Every package of the fixture, not only the one bazelize is pointed at: + # the packages beside it are dependencies with sources of their own, and + # one that stopped building would be found here rather than in whatever + # it broke. A package with no tests is built and not run — there is + # nothing to run — and `App/` is generated, not a package of the fixture. + - name: SwiftPM Build And Test working-directory: spm/${{ matrix.name }} env: PKG_CONFIG_PATH: ${{ github.workspace }}/spm/${{ matrix.name }}/vendor/pkgconfig - run: swift test ${{ matrix.swiftpm }} + ARGS: ${{ matrix.swiftpm }} + run: | + status=0 + while read -r manifest; do + package=$(dirname "$manifest") + echo "::group::$package" + if ! (cd "$package" && swift build $ARGS); then + echo "::error::swift build failed for $package" + status=1 + elif [ -d "$package/Tests" ] && ! (cd "$package" && swift test $ARGS); then + echo "::error::swift test failed for $package" + status=1 + fi + echo "::endgroup::" + done < <(find . -name Package.swift -not -path "*/App/*" -not -path "*/.build/*" | sort) + exit $status # A package that wraps a system library is found through `pkg-config`, # and a fixture that ships its own `.pc` is only found when that file's diff --git a/spm/DependencyCondition/LinuxOnly/Sources/LinuxOnly/LinuxOnly.swift b/spm/DependencyCondition/LinuxOnly/Sources/LinuxOnly/LinuxOnly.swift index ef8305a..8dc2029 100644 --- a/spm/DependencyCondition/LinuxOnly/Sources/LinuxOnly/LinuxOnly.swift +++ b/spm/DependencyCondition/LinuxOnly/Sources/LinuxOnly/LinuxOnly.swift @@ -1,7 +1,5 @@ -#if !os(Linux) -#error("This package is a Linux-only dependency, so building it here is the bug this package is here to catch.") -#endif - +/// A package nothing here builds: what proves it is the target that would have +/// linked it, which fails to compile if this module ever became importable. public enum LinuxOnly { public static let value = 2 } diff --git a/spm/DependencyCondition/Sources/Conditional/Conditional.swift b/spm/DependencyCondition/Sources/Conditional/Conditional.swift index 16517a0..ff1ff32 100644 --- a/spm/DependencyCondition/Sources/Conditional/Conditional.swift +++ b/spm/DependencyCondition/Sources/Conditional/Conditional.swift @@ -1,4 +1,11 @@ import Always + +/// The dependency behind `.when(platforms: [.linux])`: this is a macOS build, +/// so that package is not part of it and its module cannot be imported. A +/// build that pulled it in anyway would fail right here. +#if !os(Linux) && canImport(LinuxOnly) +#error("A dependency conditional on Linux must not be linked into a macOS build") +#endif #if canImport(Extras) import Extras #endif diff --git a/spm/README.md b/spm/README.md index 909ef32..0cbe885 100644 --- a/spm/README.md +++ b/spm/README.md @@ -30,9 +30,15 @@ would not tell us anything. | `TargetPath` | `path:`, where neither the target nor its tests are under `Sources/` | | `TargetResource` | every resource rule: `.copy` of a directory and of a single file, `.process`, `.embedInCode`, an explicit localization, two `.lproj` directories, an asset catalogue, a xib, a storyboard, a data model, a shader that includes a header, a string catalogue, a test target's own resources, and the `.docc` and `.xcprivacy` SwiftPM ignores | +Every package of a fixture builds and — where it has tests — passes them, the +package beside the one bazelize is pointed at included: those are dependencies +with sources of their own, and one that stopped building should be found here +rather than in whatever it broke. + A package that must not compile a file says so in the file: it is a `#error(…)`, so a generator that globs too much fails loudly instead of -quietly passing. +quietly passing. What must not be *linked* says so where it would have been +linked, so the package holding it still builds anywhere. ## Running one From a95a7a8a542dab4a4383e2551db32e13c842895a Mon Sep 17 00:00:00 2001 From: yume190 Date: Tue, 22 Sep 2026 21:45:48 +0800 Subject: [PATCH 30/47] test(spm): pass swift test the way swift test is run Two fixtures only passed with --build-system native, which is a flag no one uses and a deprecation warning besides. RemoteArtifactBundle's plugin asked for a build command, and the default build system will not run a program a binary target downloaded from one; a prebuild command runs it, and that is what the plugin asks for now. .embedInCode is generated by neither build system but the old one, so it is a package of its own: TargetEmbed is the one fixture that still needs the flag, and TargetResource is run the way every other fixture is. That put TargetResource on the default build system for the first time, where a privacy manifest turns out to be bundled rather than ignored. It is declared now, which is what a package shipping one does, and what the test pins. --- .github/workflows/swift.yml | 13 +++++----- spm/README.md | 13 +++++----- .../Plugins/RecordVersion/Plugin.swift | 8 +++--- spm/TargetEmbed/Package.swift | 25 +++++++++++++++++++ .../TargetEmbed}/Embedded/greeting.txt | 0 .../Sources/TargetEmbed/TargetEmbed.swift | 10 ++++++++ .../TargetEmbedTests/TargetEmbedTests.swift | 7 ++++++ spm/TargetResource/Package.swift | 3 ++- .../TargetResource/TargetResource.swift | 5 ---- .../TargetResourceTests.swift | 11 +++----- 10 files changed, 66 insertions(+), 29 deletions(-) create mode 100644 spm/TargetEmbed/Package.swift rename spm/{TargetResource/Sources/TargetResource => TargetEmbed/Sources/TargetEmbed}/Embedded/greeting.txt (100%) create mode 100644 spm/TargetEmbed/Sources/TargetEmbed/TargetEmbed.swift create mode 100644 spm/TargetEmbed/Tests/TargetEmbedTests/TargetEmbedTests.swift diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 355e6ff..380ebb6 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -349,19 +349,18 @@ jobs: //Packages/Products:ProductSnippet - name: RemoteArtifactBundle plugins: true - # The default build system will not run a program a binary target - # downloaded, which is SwiftPM's own limitation rather than - # anything this package does. - swiftpm: --build-system native - name: RemoteXCFramework - name: SwiftSettings - name: SystemLibrary + - name: TargetEmbed + # `.embedInCode` generates nothing under the default build system + # in this toolchain, which is SwiftPM's own gap rather than + # anything this package does — and the only reason this rule has a + # package of its own. + swiftpm: --build-system native - name: TargetExclude - name: TargetPath - name: TargetResource - # `.embedInCode` generates nothing under the default build system - # in this toolchain, for the same reason. - swiftpm: --build-system native - name: TargetSources - name: Trait traits: Fast,Slow diff --git a/spm/README.md b/spm/README.md index 0cbe885..2c5a855 100644 --- a/spm/README.md +++ b/spm/README.md @@ -28,7 +28,8 @@ would not tell us anything. | `TraitGraph` | the whole trait graph: traits that enable traits, a condition naming several, and dependencies taking `.defaults`, nothing, or a named selection | | `TargetSources` | `sources:`, where a file beside the listed ones must not be compiled, and a link back to the target's own directory that must not be walked | | `TargetPath` | `path:`, where neither the target nor its tests are under `Sources/` | -| `TargetResource` | every resource rule: `.copy` of a directory and of a single file, `.process`, `.embedInCode`, an explicit localization, two `.lproj` directories, an asset catalogue, a xib, a storyboard, a data model, a shader that includes a header, a string catalogue, a test target's own resources, and the `.docc` and `.xcprivacy` SwiftPM ignores | +| `TargetEmbed` | `.embedInCode`, the one rule with no bundle at all: the file's bytes are a generated source | +| `TargetResource` | every other resource rule: `.copy` of a directory and of a single file, `.process`, an explicit localization, two `.lproj` directories, an asset catalogue, a xib, a storyboard, a data model, a shader that includes a header, a string catalogue, a privacy manifest, a test target's own resources, and the `.docc` SwiftPM ignores | Every package of a fixture builds and — where it has tests — passes them, the package beside the one bazelize is pointed at included: those are dependencies @@ -83,8 +84,8 @@ runtime. The generated `tools/bazel` wrapper also exposes them as `App/` is generated, and is not checked in. -Two packages need `swift test --build-system native`: the default build system -in this toolchain generates nothing for `.embedInCode` (`TargetResource`) and -will not run a program a binary target downloaded (`RemoteArtifactBundle`). -Neither is anything the packages themselves ask for, and the Bazel side of both -is built the same way as every other fixture. +`TargetEmbed` needs `swift test --build-system native`: the default build +system in this toolchain generates nothing for `.embedInCode`, which is +SwiftPM's own gap rather than anything the package asks for — and the only +reason that rule has a package of its own. Its Bazel side is built the same +way as every other fixture. diff --git a/spm/RemoteArtifactBundle/Plugins/RecordVersion/Plugin.swift b/spm/RemoteArtifactBundle/Plugins/RecordVersion/Plugin.swift index 2c699b4..f5ec7af 100644 --- a/spm/RemoteArtifactBundle/Plugins/RecordVersion/Plugin.swift +++ b/spm/RemoteArtifactBundle/Plugins/RecordVersion/Plugin.swift @@ -4,13 +4,15 @@ import PackagePlugin @main struct RecordVersion: BuildToolPlugin { func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { - let output = context.pluginWorkDirectoryURL.appending(component: "Version.generated.swift") + let directory = context.pluginWorkDirectoryURL.appending(component: "Generated") + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + let output = directory.appending(component: "Version.generated.swift") /// The program the bundle ships, asked for by the name the bundle's /// `info.json` files it under. let tool = try context.tool(named: "periphery") return [ - .buildCommand( + .prebuildCommand( displayName: "Record what the bundled program answers", executable: URL(fileURLWithPath: "/bin/sh"), arguments: [ @@ -20,7 +22,7 @@ struct RecordVersion: BuildToolPlugin { printf 'public let toolVersion = "%s"\\n' "$answer" > '\(output.path())' """, ], - outputFiles: [output]), + outputFilesDirectory: directory), ] } } diff --git a/spm/TargetEmbed/Package.swift b/spm/TargetEmbed/Package.swift new file mode 100644 index 0000000..dc3ad6a --- /dev/null +++ b/spm/TargetEmbed/Package.swift @@ -0,0 +1,25 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +/// `.embedInCode`: the one resource rule that produces no bundle at all — the +/// file's bytes become a generated source the target compiles. +/// +/// Its own package because the default build system in this toolchain +/// generates nothing for it, so this is the only fixture whose SwiftPM side +/// needs `--build-system native`. +let package = Package( + name: "TargetEmbed", + products: [ + .library(name: "TargetEmbed", targets: ["TargetEmbed"]), + ], + targets: [ + .target( + name: "TargetEmbed", + resources: [ + .embedInCode("Embedded/greeting.txt"), + ]), + .testTarget( + name: "TargetEmbedTests", + dependencies: ["TargetEmbed"]), + ]) diff --git a/spm/TargetResource/Sources/TargetResource/Embedded/greeting.txt b/spm/TargetEmbed/Sources/TargetEmbed/Embedded/greeting.txt similarity index 100% rename from spm/TargetResource/Sources/TargetResource/Embedded/greeting.txt rename to spm/TargetEmbed/Sources/TargetEmbed/Embedded/greeting.txt diff --git a/spm/TargetEmbed/Sources/TargetEmbed/TargetEmbed.swift b/spm/TargetEmbed/Sources/TargetEmbed/TargetEmbed.swift new file mode 100644 index 0000000..9d9cc15 --- /dev/null +++ b/spm/TargetEmbed/Sources/TargetEmbed/TargetEmbed.swift @@ -0,0 +1,10 @@ +import Foundation + +/// No bundle is involved: `PackageResources` is generated from the file, and +/// the bytes are in the binary. +public enum TargetEmbed { + public static var embedded: String { + String(decoding: PackageResources.greeting_txt, as: UTF8.self) + .trimmingCharacters(in: .whitespacesAndNewlines) + } +} diff --git a/spm/TargetEmbed/Tests/TargetEmbedTests/TargetEmbedTests.swift b/spm/TargetEmbed/Tests/TargetEmbedTests/TargetEmbedTests.swift new file mode 100644 index 0000000..bcfeea5 --- /dev/null +++ b/spm/TargetEmbed/Tests/TargetEmbedTests/TargetEmbedTests.swift @@ -0,0 +1,7 @@ +import TargetEmbed +import Testing + +@Test +func anEmbeddedResourceIsInTheBinary() { + #expect(TargetEmbed.embedded == "embedded") +} diff --git a/spm/TargetResource/Package.swift b/spm/TargetResource/Package.swift index 9e7fc7c..e520485 100644 --- a/spm/TargetResource/Package.swift +++ b/spm/TargetResource/Package.swift @@ -21,7 +21,8 @@ let package = Package( .copy("single.txt"), .process("Processed"), .process("Localized", localization: .default), - .embedInCode("Embedded/greeting.txt"), + /// A privacy manifest is a file a package ships as it is. + .copy("PrivacyInfo.xcprivacy"), /// Kinds the platform compiles rather than copies. .process("Assets.xcassets"), .process("Panel.xib"), diff --git a/spm/TargetResource/Sources/TargetResource/TargetResource.swift b/spm/TargetResource/Sources/TargetResource/TargetResource.swift index b06be33..c5b72c0 100644 --- a/spm/TargetResource/Sources/TargetResource/TargetResource.swift +++ b/spm/TargetResource/Sources/TargetResource/TargetResource.swift @@ -20,11 +20,6 @@ public enum TargetResource { return try? String(contentsOf: url, encoding: .utf8).trimmed } - /// Embedded in code: no bundle at all, the bytes are a generated source. - public static var embedded: String { - String(decoding: PackageResources.greeting_txt, as: UTF8.self).trimmed - } - /// The explicitly localized resource: declared with `localization:`, so it /// is filed under the package's default localization. public static var explicitLocalization: String? { diff --git a/spm/TargetResource/Tests/TargetResourceTests/TargetResourceTests.swift b/spm/TargetResource/Tests/TargetResourceTests/TargetResourceTests.swift index 0872283..029339a 100644 --- a/spm/TargetResource/Tests/TargetResourceTests/TargetResourceTests.swift +++ b/spm/TargetResource/Tests/TargetResourceTests/TargetResourceTests.swift @@ -25,11 +25,6 @@ func aProcessedResourceIsInTheBundle() { #expect(TargetResource.processed == "processed") } -@Test -func anEmbeddedResourceIsInTheBinary() { - #expect(TargetResource.embedded == "embedded") -} - @Test func localizedResourcesAreFiledUnderTheirLocalization() { #expect(TargetResource.explicitLocalization == #""explicit" = "explicit localization";"#) @@ -58,12 +53,14 @@ func platformResourcesReachTheBundle() { #expect(has("Model.momd", "Model.xcdatamodeld")) #expect(has("default.metallib", "Shader.metal")) #expect(has("Catalog.xcstrings", "Catalog.strings")) + /// A privacy manifest is shipped, not compiled: it is in the bundle under + /// its own name because the manifest declares it. + #expect(has("PrivacyInfo.xcprivacy")) } @Test -func documentationAndPrivacyAreNotResources() { +func aDocumentationCatalogueIsNotAResource() { let bundled = TargetResource.bundled #expect(!bundled.contains { $0.hasSuffix(".docc") || $0.hasSuffix(".md") }) - #expect(!bundled.contains { $0.hasSuffix(".xcprivacy") }) } From 362237d1ea0f5b27aac2ceee574b9848053c91d1 Mon Sep 17 00:00:00 2001 From: yume190 Date: Tue, 22 Sep 2026 22:23:16 +0800 Subject: [PATCH 31/47] test(spm): localize with a base and a copied language `Base.lproj` is the localization a resource falls back to, and a `.lproj` copied rather than processed is in the bundle with nothing compiled: one fixture had a single language, so neither the fallback nor a second language being its own was ever built. --- spm/TargetResource/Package.swift | 6 ++++++ .../Sources/TargetResource/Base.lproj/Localizable.strings | 1 + .../Sources/TargetResource/de.lproj/Localizable.strings | 1 + .../Tests/TargetResourceTests/TargetResourceTests.swift | 4 ++++ 4 files changed, 12 insertions(+) create mode 100644 spm/TargetResource/Sources/TargetResource/Base.lproj/Localizable.strings create mode 100644 spm/TargetResource/Sources/TargetResource/de.lproj/Localizable.strings diff --git a/spm/TargetResource/Package.swift b/spm/TargetResource/Package.swift index e520485..f2ca305 100644 --- a/spm/TargetResource/Package.swift +++ b/spm/TargetResource/Package.swift @@ -21,6 +21,12 @@ let package = Package( .copy("single.txt"), .process("Processed"), .process("Localized", localization: .default), + /// The localization a resource falls back to, which is a + /// directory rather than a language. + .process("Base.lproj"), + /// A localization copied as it is: what is inside is not + /// compiled, and the directory keeps its name. + .copy("de.lproj"), /// A privacy manifest is a file a package ships as it is. .copy("PrivacyInfo.xcprivacy"), /// Kinds the platform compiles rather than copies. diff --git a/spm/TargetResource/Sources/TargetResource/Base.lproj/Localizable.strings b/spm/TargetResource/Sources/TargetResource/Base.lproj/Localizable.strings new file mode 100644 index 0000000..6570ece --- /dev/null +++ b/spm/TargetResource/Sources/TargetResource/Base.lproj/Localizable.strings @@ -0,0 +1 @@ +"lproj" = "from base"; diff --git a/spm/TargetResource/Sources/TargetResource/de.lproj/Localizable.strings b/spm/TargetResource/Sources/TargetResource/de.lproj/Localizable.strings new file mode 100644 index 0000000..a582dc2 --- /dev/null +++ b/spm/TargetResource/Sources/TargetResource/de.lproj/Localizable.strings @@ -0,0 +1 @@ +"lproj" = "aus de"; diff --git a/spm/TargetResource/Tests/TargetResourceTests/TargetResourceTests.swift b/spm/TargetResource/Tests/TargetResourceTests/TargetResourceTests.swift index 029339a..8c9a695 100644 --- a/spm/TargetResource/Tests/TargetResourceTests/TargetResourceTests.swift +++ b/spm/TargetResource/Tests/TargetResourceTests/TargetResourceTests.swift @@ -37,6 +37,10 @@ func everyLocalizationIsItsOwn() { /// says what it says. #expect(TargetResource.lprojLocalization("en") == "from lproj") #expect(TargetResource.lprojLocalization("ja") == "lproj から") + /// `Base` is a localization of its own — what a resource falls back to — + /// and a copied one is in the bundle whether or not anything compiled it. + #expect(TargetResource.lprojLocalization("Base") == "from base") + #expect(TargetResource.lprojLocalization("de") == "aus de") } @Test From 13fd6c9c5b12754a1c7006602036c74c5d06371e Mon Sep 17 00:00:00 2001 From: yume190 Date: Tue, 22 Sep 2026 22:23:16 +0800 Subject: [PATCH 32/47] feat(spm): pick the localizations a build bundles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A package's languages were read by nothing: every localization it ships went into every build, and there was no way to ask for one. `bazel list language` says which localizations the workspace's packages ship and which package calls each its default, read from the `.lproj` directories and each manifest's defaultLocalization. `--config=lang.` bundles that one — `Base` comes along, because that is what a missing localization falls back to — and a build that names none bundles them all, which is what SwiftPM does. --- .github/workflows/swift.yml | 17 ++- Sources/BazelizeKit/Bazel/Bazel+BazelRC.swift | 4 +- Sources/BazelizeKit/List/Listing.swift | 30 +++++ .../SwiftPM/SwiftPM+Generator.swift | 1 + .../SwiftPM/SwiftPM+Language.swift | 110 ++++++++++++++++++ .../SwiftPM/SwiftPM+Manifest.swift | 4 + .../SwiftPM/SwiftPM+PluginRule.swift | 9 +- spm/README.md | 9 +- 8 files changed, 177 insertions(+), 7 deletions(-) create mode 100644 Sources/BazelizeKit/SwiftPM/SwiftPM+Language.swift diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 380ebb6..4629243 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -438,12 +438,27 @@ jobs: run: PATH="$GITHUB_WORKSPACE:$PATH" bazel run //:plugins # The workspace's own commands, which answer whatever the package is: - # a package with no trait and no configuration answers that. + # a package with no trait, no configuration and no localization answers + # that. - name: List Workspace working-directory: spm/${{ matrix.name }}/App run: | PATH="$GITHUB_WORKSPACE:$PATH" bazel list config PATH="$GITHUB_WORKSPACE:$PATH" bazel list trait + PATH="$GITHUB_WORKSPACE:$PATH" bazel list language + + # A build that names a localization bundles that one and `Base`, and + # nothing else. What is in the bundle is the only proof of that. + - name: Filter Localizations + if: matrix.name == 'TargetResource' + working-directory: spm/TargetResource/App + run: | + bazel build //Packages/TargetResource:TargetResourceTests --config=lang.en + bundle=$(find -L bazel-bin/Packages/TargetResource -name TargetResourceTests.zip | head -1) + unzip -Z1 "$bundle" | grep -q "en.lproj" + unzip -Z1 "$bundle" | grep -q "Base.lproj" + ! unzip -Z1 "$bundle" | grep -q "ja.lproj" + ! unzip -Z1 "$bundle" | grep -q "de.lproj" - name: Test Package working-directory: spm/${{ matrix.name }}/App diff --git a/Sources/BazelizeKit/Bazel/Bazel+BazelRC.swift b/Sources/BazelizeKit/Bazel/Bazel+BazelRC.swift index deab28a..fd5d8ee 100644 --- a/Sources/BazelizeKit/Bazel/Bazel+BazelRC.swift +++ b/Sources/BazelizeKit/Bazel/Bazel+BazelRC.swift @@ -84,10 +84,12 @@ extension Bazel { /// the generated flags are inert without this file. struct RootRC { /// What the root file has to import for the generated flags to be - /// read: the project's configurations, and the traits of its packages. + /// read: the project's configurations, the traits of its packages, and + /// the localizations they ship. static let importLines = [ "import %workspace%/config.bazelrc", "import %workspace%/traits.bazelrc", + "import %workspace%/languages.bazelrc", ] let path: Path diff --git a/Sources/BazelizeKit/List/Listing.swift b/Sources/BazelizeKit/List/Listing.swift index 442b774..0698379 100644 --- a/Sources/BazelizeKit/List/Listing.swift +++ b/Sources/BazelizeKit/List/Listing.swift @@ -90,6 +90,36 @@ enum Listing { return lines.joined(separator: "\n") } + /// The localizations the workspace's packages ship, and what to say to + /// build only one of them. + static func languages(_ localizations: [SwiftPM.Generator.Localization]) -> String { + guard !localizations.isEmpty else { + return "No package in this workspace ships a localization." + } + + var lines = ["Localizations of this workspace's packages:", ""] + for localization in localizations { + let shipped = localization.packages.joined(separator: ", ") + let fallback = localization.defaultOf.isEmpty + ? "" + : " (the default of \(localization.defaultOf.joined(separator: ", ")))" + + lines.append(" \(localization.code)\(fallback)") + lines.append(" shipped by \(shipped)") + lines.append(" --config=\(localization.config)") + } + + lines.append("") + lines.append(""" + A build that names no localization bundles every one of them, which is \ + what SwiftPM does. Naming one is how a build ships a single language: \ + `Base` comes along whatever is asked for, because it is what a missing \ + localization falls back to. Several at once is the flag underneath, \ + `--@build_bazel_rules_apple//apple/build_settings:locales_to_include=en,ja`. + """) + return lines.joined(separator: "\n") + } + /// What the workspace's `.bazelrc` says, and what the files it imports say: /// the `build:` lines by name, and the ones that name no /// configuration. diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift index f2e9ccc..e426732 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift @@ -106,6 +106,7 @@ extension SwiftPM { /// `.bazelrc` imports it, and an import of a file that is not /// there is a workspace that does not load. try writeTraitConfigs() + try writeLanguageConfigs() try writeListingCommands() } diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Language.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Language.swift new file mode 100644 index 0000000..57c1845 --- /dev/null +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Language.swift @@ -0,0 +1,110 @@ +// +// SwiftPM+Language.swift +// +// +// The localizations a workspace's packages ship, as something a build picks. +// + +import Foundation +@preconcurrency import PathKit +import Util + +extension SwiftPM.Generator { + /// One localization, and the packages that ship it. + struct Localization { + /// `en`, `ja`, `Base` — the name of the `.lproj` directory, without it. + let code: String + /// The packages whose resources are in that language. + let packages: [String] + /// The packages that call it their default: what a resource declared + /// without a language of its own is filed under. + let defaultOf: [String] + + /// `lang.`, the name a `--config` goes by. + var config: String { + "lang.\(code)" + } + } + + /// Every localization every package in the workspace ships. + /// + /// A `.lproj` directory is what makes a resource localized — SwiftPM needs + /// nothing declared for it — so the directories are what is read, plus each + /// package's `defaultLocalization`, which it may declare without shipping + /// anything under that name yet. + var localizations: [Localization] { + var packagesByCode: [String: [String]] = [:] + var defaultsByCode: [String: [String]] = [:] + + for package in workspace.packages.sorted(by: { $0.directory < $1.directory }) { + var codes: Set = [] + + for target in package.manifest.targets { + guard let directory = sourceDirectory(of: target, in: package) else { continue } + + for file in Self.walk(directory) { + for component in file.components where component.hasSuffix(".lproj") { + codes.insert(String(component.dropLast(".lproj".count))) + } + } + } + + if let fallback = package.manifest.defaultLocalization { + codes.insert(fallback) + defaultsByCode[fallback, default: []].append(package.directory) + } + + for code in codes { + packagesByCode[code, default: []].append(package.directory) + } + } + + return packagesByCode.keys.sorted().map { code in + Localization( + code: code, + packages: (packagesByCode[code] ?? []).sorted(), + defaultOf: (defaultsByCode[code] ?? []).sorted()) + } + } + + /// `--config=lang.` for every localization the workspace's packages + /// ship. + /// + /// A build that names none bundles them all, which is what SwiftPM does. + /// Naming one is how an app ships a single language; `Base` comes along + /// whatever is asked for, because it is what a missing localization falls + /// back to. + /// + /// The file is always written, because a `.bazelrc` that imports a file + /// that is not there does not load. + func writeLanguageConfigs() throws { + let localizations = self.localizations + var lines = [ + "# Generated by Bazelize: one --config per localization the packages of", + "# this workspace ship. A build that names none bundles every one of", + "# them, which is what SwiftPM does; `Base` is kept whatever is asked", + "# for.", + ] + + if localizations.isEmpty { + lines.append("#") + lines.append("# No package in this workspace ships a localization.") + } + + for localization in localizations { + let shipped = localization.packages.joined(separator: ", ") + let fallback = localization.defaultOf.isEmpty + ? "" + : ", the default of \(localization.defaultOf.joined(separator: ", "))" + + lines.append("") + lines.append("# \(localization.code): shipped by \(shipped)\(fallback)") + lines.append( + "build:\(localization.config) " + + "--@build_bazel_rules_apple//apple/build_settings:locales_to_include=" + + localization.code) + } + + try (output + "languages.bazelrc").write(lines.joined(separator: "\n") + "\n") + } +} diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Manifest.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Manifest.swift index 715fd16..fd7ecc3 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Manifest.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Manifest.swift @@ -32,6 +32,9 @@ extension SwiftPM { /// the language modes the package's targets compile in unless one of /// them says otherwise. let swiftLanguageModes: [String] + /// The localization a package's resources fall back to, which is the + /// one a resource declared without a language is filed under. + let defaultLocalization: String? /// `{"_version": "6.0.0"}`: which `PackageDescription` the manifest was /// written against, which a plugin has to be compiled against too. let toolsVersion: String @@ -47,6 +50,7 @@ extension SwiftPM { cLanguageStandard = container.value(String.self, "cLanguageStandard") cxxLanguageStandard = container.value(String.self, "cxxLanguageStandard") swiftLanguageModes = container.list(String.self, "swiftLanguageVersions") + defaultLocalization = container.value(String.self, "defaultLocalization") toolsVersion = container.value([String: String].self, "toolsVersion")?["_version"] ?? "5.9.0" } diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginRule.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginRule.swift index dc065c6..00f3433 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginRule.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginRule.swift @@ -106,7 +106,7 @@ extension SwiftPM.Generator { /// The answers are embedded in executable targets, so using them never /// depends on whichever `bazelize` executable happens to be on `PATH`. /// Bazel itself has no extension point for custom commands; `tools/bazel` - /// keeps `bazel list config|trait` as aliases for the two `bazel run` + /// keeps `bazel list config|trait|language` as aliases for the `bazel run` /// targets and forwards every other command unchanged. func writeListingCommands() throws { let directory = output + "tools" @@ -114,7 +114,8 @@ extension SwiftPM.Generator { let listings = [ ("config", try Listing.config(output: output)), - ("trait", Listing.traits(workspace: workspace)) + ("trait", Listing.traits(workspace: workspace)), + ("language", Listing.languages(localizations)), ] let builder = CodeBuilder() builder.load(loadableRule: Rules.Shell.sh_binary) @@ -156,11 +157,11 @@ extension SwiftPM.Generator { if [[ "${1:-}" == "list" ]]; then case "${2:-}" in - config|trait) + config|trait|language) exec "$BAZEL_REAL" run "//tools:list-${2}" ;; *) - echo "Usage: bazel list config|trait" >&2 + echo "Usage: bazel list config|trait|language" >&2 exit 2 ;; esac diff --git a/spm/README.md b/spm/README.md index 2c5a855..31aa784 100644 --- a/spm/README.md +++ b/spm/README.md @@ -51,7 +51,9 @@ bazel run //:plugins # only the packages with a build tool plugin need this bazel test //... bazel run //tools:list-config # what `--config=` the workspace defines bazel run //tools:list-trait # which traits its packages declare, and which are on +bazel run //tools:list-language # which localizations they ship bazel test //... --config=. # …with one of them turned on +bazel build //... --config=lang. # …bundling that localization only ``` A package that wraps a system library is found through `pkg-config`, and the @@ -78,9 +80,14 @@ they generate into `Packages//Generated/`. It is a separate step because a plugin is a program: Bazel builds it, and bazelize runs it as SwiftPM would. +`bazel build --config=lang.` bundles that localization and `Base`, and +nothing else; a build that names none bundles every one of them, which is what +SwiftPM does. Several at once is the flag underneath, +`--@build_bazel_rules_apple//apple/build_settings:locales_to_include=en,ja`. + The listing commands are generated Bazel targets and do not need `bazelize` at runtime. The generated `tools/bazel` wrapper also exposes them as -`bazel list config|trait`. +`bazel list config|trait|language`. `App/` is generated, and is not checked in. From 4bf2e099335ba11af327deb25840395344419aac Mon Sep 17 00:00:00 2001 From: yume190 Date: Tue, 22 Sep 2026 22:33:30 +0800 Subject: [PATCH 33/47] test(spm): version the data model, and say why mapping models are not here MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A versioned .xcdatamodeld is what Core Data migration is built on: both versions have to reach the bundle, and a mapping between them has to be derivable from what momc compiled. The test loads Model.mom and Model2.mom out of Model.momd and infers one. .xcmappingmodel stays uncovered, and the README says why: its source is a Core Data XML persistent store that only Xcode's modeler writes — mapc rejects a hand-written one — so there is nothing to check in. It is globbed and grouped exactly as .xcdatamodeld is, and what would compile it is rules_apple's action rather than anything this generates. --- spm/README.md | 10 ++++++++- .../Model.xcdatamodeld/.xccurrentversion | 2 +- .../Model2.xcdatamodel/contents | 7 ++++++ .../TargetResource/TargetResource.swift | 22 +++++++++++++++++++ .../TargetResourceTests.swift | 10 +++++++++ 5 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 spm/TargetResource/Sources/TargetResource/Model.xcdatamodeld/Model2.xcdatamodel/contents diff --git a/spm/README.md b/spm/README.md index 31aa784..78cabe8 100644 --- a/spm/README.md +++ b/spm/README.md @@ -29,13 +29,21 @@ would not tell us anything. | `TargetSources` | `sources:`, where a file beside the listed ones must not be compiled, and a link back to the target's own directory that must not be walked | | `TargetPath` | `path:`, where neither the target nor its tests are under `Sources/` | | `TargetEmbed` | `.embedInCode`, the one rule with no bundle at all: the file's bytes are a generated source | -| `TargetResource` | every other resource rule: `.copy` of a directory and of a single file, `.process`, an explicit localization, two `.lproj` directories, an asset catalogue, a xib, a storyboard, a data model, a shader that includes a header, a string catalogue, a privacy manifest, a test target's own resources, and the `.docc` SwiftPM ignores | +| `TargetResource` | every other resource rule: `.copy` of a directory and of a single file, `.process`, an explicit localization, two `.lproj` directories, an asset catalogue, a xib, a storyboard, a data model with two versions, a shader that includes a header, a string catalogue, a privacy manifest, a test target's own resources, and the `.docc` SwiftPM ignores | Every package of a fixture builds and — where it has tests — passes them, the package beside the one bazelize is pointed at included: those are dependencies with sources of their own, and one that stopped building should be found here rather than in whatever it broke. +`.xcmappingmodel` is the one resource kind with no fixture: its source is a +Core Data XML persistent store that only Xcode's modeler writes — a hand-written +one is rejected by `mapc` — so there is nothing to check in. Nothing about it is +particular to bazelize either: it is globbed and grouped exactly as +`.xcdatamodeld` is, which is built and asserted, and what would compile it is +rules_apple's own action. The versioned model covers what migration is built +on: both versions in the bundle, with a mapping derivable between them. + A package that must not compile a file says so in the file: it is a `#error(…)`, so a generator that globs too much fails loudly instead of quietly passing. What must not be *linked* says so where it would have been diff --git a/spm/TargetResource/Sources/TargetResource/Model.xcdatamodeld/.xccurrentversion b/spm/TargetResource/Sources/TargetResource/Model.xcdatamodeld/.xccurrentversion index 6e25a42..1af70d5 100644 --- a/spm/TargetResource/Sources/TargetResource/Model.xcdatamodeld/.xccurrentversion +++ b/spm/TargetResource/Sources/TargetResource/Model.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - Model.xcdatamodel + Model2.xcdatamodel diff --git a/spm/TargetResource/Sources/TargetResource/Model.xcdatamodeld/Model2.xcdatamodel/contents b/spm/TargetResource/Sources/TargetResource/Model.xcdatamodeld/Model2.xcdatamodel/contents new file mode 100644 index 0000000..8e37524 --- /dev/null +++ b/spm/TargetResource/Sources/TargetResource/Model.xcdatamodeld/Model2.xcdatamodel/contents @@ -0,0 +1,7 @@ + + + + + + + diff --git a/spm/TargetResource/Sources/TargetResource/TargetResource.swift b/spm/TargetResource/Sources/TargetResource/TargetResource.swift index c5b72c0..cc9dafb 100644 --- a/spm/TargetResource/Sources/TargetResource/TargetResource.swift +++ b/spm/TargetResource/Sources/TargetResource/TargetResource.swift @@ -1,3 +1,4 @@ +import CoreData import Foundation public enum TargetResource { @@ -43,6 +44,27 @@ public enum TargetResource { return bundle.localizedString(forKey: "lproj", value: nil, table: nil) } + /// The two versions of the data model, loaded out of what `momc` compiled. + /// + /// A versioned `.xcdatamodeld` is what migration is built on: both + /// versions have to be in the bundle, and a mapping between them has to be + /// derivable from them — which is what `nil` here would deny. + public static var modelMigration: (from: Int, to: Int)? { + guard let momd = Bundle.module.url(forResource: "Model", withExtension: "momd"), + let source = NSManagedObjectModel(contentsOf: momd.appendingPathComponent("Model.mom")), + let destination = NSManagedObjectModel(contentsOf: momd.appendingPathComponent("Model2.mom")), + (try? NSMappingModel.inferredMappingModel( + forSourceModel: source, + destinationModel: destination)) != nil + else { + return nil + } + + return ( + source.entitiesByName["Item"]?.properties.count ?? 0, + destination.entitiesByName["Item"]?.properties.count ?? 0) + } + /// What is in the bundle, by name. A platform resource is compiled by /// whoever builds it — `Assets.car`, `Panel.nib`, `default.metallib` — and /// copied as it is by whoever cannot, so both names are the same resource diff --git a/spm/TargetResource/Tests/TargetResourceTests/TargetResourceTests.swift b/spm/TargetResource/Tests/TargetResourceTests/TargetResourceTests.swift index 8c9a695..1fe08e6 100644 --- a/spm/TargetResource/Tests/TargetResourceTests/TargetResourceTests.swift +++ b/spm/TargetResource/Tests/TargetResourceTests/TargetResourceTests.swift @@ -68,3 +68,13 @@ func aDocumentationCatalogueIsNotAResource() { #expect(!bundled.contains { $0.hasSuffix(".docc") || $0.hasSuffix(".md") }) } + +@Test +func bothVersionsOfTheDataModelAreCompiled() { + /// The one thing a versioned model is for: an older version and a newer + /// one in the same bundle, with a migration derivable between them. + let migration = TargetResource.modelMigration + + #expect(migration?.from == 1) + #expect(migration?.to == 2) +} From cdec2f4f95747e839a488fd5a435942a2f9ff929 Mon Sep 17 00:00:00 2001 From: yume190 Date: Tue, 22 Sep 2026 22:43:16 +0800 Subject: [PATCH 34/47] docs: record what the SwiftPM side does not do yet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three generator gaps, seven uncovered shapes and three process notes, each with what it would take and what it would cost — including the two decided against, so they are not rediscovered. --- TODO.md | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 TODO.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..0286db8 --- /dev/null +++ b/TODO.md @@ -0,0 +1,124 @@ +# TODO + +What the SwiftPM side of bazelize does not do yet, and why. Written after the +`spm/` fixture corpus reached 24 packages, all of which build and test both +ways (`swift build`/`swift test` and `bazelize` + `bazel test //...`). + +Ordered by what a user would hit first. + +## A. Generator behaviour + +### A1. An executable target's resources are not bundled + +A program built by `swift_binary` gets the generated `Bundle.module` accessor +and no bundle: `apple_resource_bundle` hands its resources to whatever bundles +them — an app or a test — and a program is neither, so the rule produces +something nothing puts anywhere. The binary compiles and `fatalError`s when it +runs. + +`cquery --output=files` on such a bundle is empty, and its `OutputGroupInfo` is +an empty depset, so `data = [":XResources"]` on the binary brings nothing into +runfiles either. + +Today the generator says so out loud while generating (`SwiftPM+Resources.swift`, +the `.executable` case) rather than leaving it to be found by a crash. + +A fix means writing the bundle directory into the generated workspace — +`Generated/_.bundle` with the Info.plist and one link per +resource — carrying it as `data`, and teaching the accessor the runfiles +candidates. The cost is that `.process` no longer compiles: an asset catalogue, +a xib or a shader in a program's resources would be copied rather than built, +which is a divergence from SwiftPM worth reporting where it happens. + +SwiftPM writes that bundle beside the program, so a package shipping a CLI tool +with resources works there and not here. + +### A2. An undeclared privacy manifest is not bundled + +`PrivacyInfo.xcprivacy` sitting beside a target's sources is bundled by +SwiftPM's default build system and by nothing here — the generator's discovered +resource types do not include it. A package that declares it (`.copy`) works +either way, which is what `spm/TargetResource` does. + +Adding `xcprivacy` to the discovered types would match the default build +system, and diverge from `--build-system native`, which ignores it. Decide +which one is the contract before changing it. + +### A3. A resource bundle is flat, not wrapped — not planned + +On macOS SwiftPM produces `Bundle.bundle/Contents/Resources/…`; rules_apple +produces a flat `Bundle.bundle/…` on every platform by design, which is the iOS +shape. Measured: `Bundle.module.infoDictionary` and every `url(forResource:)` +lookup answer the same on both, and only code that builds `Contents/Resources` +paths by hand would notice. Aligning means not using `apple_resource_bundle` +and assembling the bundle ourselves, which is not worth it. + +## B. Coverage + +### B1. Nothing in `spm/` is built for iOS + +Every fixture is macOS. `spm/Platform` declares `.iOS(.v16)` but its tests run +on macOS, so the iOS bundle shape, `minimum_os_version` on an iOS rule and the +platform transition a package rule is built through are covered only by +`fixture/iOS`, which is the Xcode side. This is the largest hole. + +### B2. `.xcmappingmodel` — not planned + +Its source is a Core Data XML persistent store that only Xcode's modeler +writes; a hand-written `xcmapping.xml` is rejected by `mapc` (`Unknown store +type, format, or version`), and there is no sample on a machine with Xcode +installed to copy the format from. Nothing about it is particular to bazelize +either: it is globbed and grouped exactly as `.xcdatamodeld` is, which is built +and asserted, and what would compile it is rules_apple's own action. + +What migration is actually built on — a versioned `.xcdatamodeld` with both +versions in the bundle and a mapping derivable between them — is covered. + +### B3. A dependency pinned by branch, revision or exact version + +Every fixture uses `from:`. For the generator these are the same path: SwiftPM +resolves them and the generator reads the checkout. Low value. + +### B4. A package with no products, and a plugin-only package + +Both are real shapes; neither is exercised. + +### B5. A target whose sources sit at the package root + +`sourceDirectory(of:in:)` falls back to `package.root + target.name`, but +SwiftPM only looks in `Sources`, `Source`, `src` and `srcs` unless the target +names a `path:`. Check whether that fallback is reachable at all: if it is not, +delete it rather than write a fixture for it. + +### B6. A macro used from another package + +A build tool plugin from another package is covered (`spm/PluginDependency`); +a macro target is not. + +### B7. Asset catalogue variants + +Only a colour set is built. An app icon set, a symbol set and the generated +asset symbols are not. + +## C. Process + +### C1. Sub-packages have no tests — by design + +Eleven packages under the fixtures (`vendor-kit`, `Alt`, `Other`, `Stamping`, +`Products`, `Trait/Dependency`, `TraitGraph/*Dependency`, +`DependencyCondition/Extras`, `DependencyCondition/LinuxOnly`) are dependencies +with nothing of their own to assert. CI builds every one of them and runs +`swift test` only where a `Tests` directory exists, rather than padding them +with tests that prove nothing. + +### C2. A fixture lane does not fail on a generator note + +`IntegrateIOS` greps the generation log for `did not run the`; the package +lanes check nothing. A note like A1's would not turn CI red. Cheap to add: +generate into a log and fail the lane on the notes that matter. + +### C3. `TargetEmbed` needs `--build-system native` + +`.embedInCode` generates no `PackageResources` under the default build system +in this toolchain, which is SwiftPM's own gap. It is the only fixture that +needs the flag, which is why that rule has a package of its own. From 869ba38aa1a2246563051b2cd0d9c43d4d145a1f Mon Sep 17 00:00:00 2001 From: yume190 Date: Tue, 22 Sep 2026 22:43:59 +0800 Subject: [PATCH 35/47] docs: keep the same record in Chinese --- TODO.md | 2 + TODO.zh-Hant.md | 118 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 TODO.zh-Hant.md diff --git a/TODO.md b/TODO.md index 0286db8..e590ad9 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,7 @@ # TODO +中文版:[TODO.zh-Hant.md](TODO.zh-Hant.md) + What the SwiftPM side of bazelize does not do yet, and why. Written after the `spm/` fixture corpus reached 24 packages, all of which build and test both ways (`swift build`/`swift test` and `bazelize` + `bazel test //...`). diff --git a/TODO.zh-Hant.md b/TODO.zh-Hant.md new file mode 100644 index 0000000..0cf01ae --- /dev/null +++ b/TODO.zh-Hant.md @@ -0,0 +1,118 @@ +# TODO(中文版) + +英文版:[TODO.md](TODO.md)。兩份內容相同。 + +bazelize 的 SwiftPM 這一側還沒做的事,以及為什麼。寫於 `spm/` fixture 增到 24 +個套件之後 —— 這 24 個在兩邊都建得起來也測得過(`swift build`/`swift test`, +以及 `bazelize` + `bazel test //...`)。 + +依「使用者最先踩到什麼」排序。 + +## A. 產生器行為 + +### A1. executable target 的 resources 不會被打包 + +`swift_binary` 建出來的程式拿得到產生的 `Bundle.module` accessor,卻拿不到 +bundle:`apple_resource_bundle` 是把資源交給「組 bundle 的人」—— app 或 +test —— 而程式兩者都不是,所以那條規則產出的東西沒有人會放到任何地方。二進位 +檔編得過,跑起來 `fatalError`。 + +證據:對那個 bundle 下 `cquery --output=files` 是空的,`OutputGroupInfo` 是空 +depset,所以就算在執行檔上掛 `data = [":XResources"]`,runfiles 裡也不會多出 +任何東西。 + +目前的做法是在產生階段就明講(`SwiftPM+Resources.swift` 的 `.executable` +分支),而不是留給使用者用 crash 去發現。 + +要修的話,得把 bundle 目錄寫進產生的 workspace —— +`Generated/_.bundle`,裡面放 Info.plist 與每個資源的 +symlink —— 用 `data` 帶進 runfiles,再教 accessor 認得 runfiles 的候選路徑。 +代價是 `.process` 不再編譯:程式的資源裡若有 asset catalog、xib 或 shader, +會變成原樣複製而不是編譯,這與 SwiftPM 有落差,該在發生的地方回報。 + +SwiftPM 會把那個 bundle 寫在程式旁邊,所以「CLI 工具帶資源」這種套件在 +SwiftPM 能跑、在這裡不能。 + +### A2. 未宣告的 privacy manifest 不會進 bundle + +`PrivacyInfo.xcprivacy` 放在 target 原始碼旁邊時,SwiftPM 的預設 build system +會把它打包,這裡不會 —— 產生器的「自動辨識資源類型」清單裡沒有它。有明確宣告 +(`.copy`)的套件兩邊都正常,`spm/TargetResource` 就是這樣做的。 + +把 `xcprivacy` 加進自動辨識清單會對齊預設 build system,但會與 +`--build-system native` 不一致(後者忽略它)。改之前要先決定哪一個才是我們的 +契約。 + +### A3. resource bundle 是扁平而非包裝式 —— 不打算做 + +macOS 上 SwiftPM 產的是 `Bundle.bundle/Contents/Resources/…`;rules_apple 在 +所有平台都刻意產扁平的 `Bundle.bundle/…`,那是 iOS 的形狀。實測過: +`Bundle.module.infoDictionary` 與各種 `url(forResource:)` 查找在兩邊答案相同, +只有自己手拼 `Contents/Resources` 路徑的程式碼會看見差異。要對齊就得放棄 +`apple_resource_bundle` 自己組 bundle,不划算。 + +## B. 覆蓋率 + +### B1. `spm/` 裡沒有任何東西是為 iOS 建的 + +所有 fixture 都是 macOS。`spm/Platform` 有宣告 `.iOS(.v16)`,但測試仍在 macOS +上跑,所以 iOS 的 bundle 形狀、iOS 規則上的 `minimum_os_version`、以及 package +規則被建立時所經過的 platform transition,都只有 `fixture/iOS`(Xcode 那側) +間接覆蓋。**這是最大的一個洞。** + +### B2. `.xcmappingmodel` —— 不打算做 + +它的原始檔是 Core Data 的 XML persistent store,只有 Xcode 的 modeler 寫得 +出來;手寫的 `xcmapping.xml` 會被 `mapc` 拒絕(`Unknown store type, format, +or version`),而且在一台裝了 Xcode 的機器上也找不到任何範本可以照抄格式。 +它對 bazelize 來說也沒有特別之處:glob 與分組的路徑與 `.xcdatamodeld` 完全 +相同,而後者有 fixture 也有斷言;真正會編譯它的是 rules_apple 自己的 action。 + +migration 真正立足的東西 —— 有版本的 `.xcdatamodeld`、兩個版本都在 bundle 裡、 +兩版之間能推導出 mapping —— 已經覆蓋了。 + +### B3. 用 branch、revision 或精確版本釘住的依賴 + +所有 fixture 都用 `from:`。對產生器而言這幾種是同一條路:SwiftPM 解析完,產生 +器讀 checkout。價值低。 + +### B4. 沒有 product 的套件,以及只有 plugin 的套件 + +兩種都是真實形狀,目前都沒被踩過。 + +### B5. 原始碼直接放在套件根目錄的 target + +`sourceDirectory(of:in:)` 有一個 fallback 會找 `package.root + target.name`, +但 SwiftPM 只會在 `Sources`、`Source`、`src`、`srcs` 裡找(除非 target 自己寫了 +`path:`)。要查的是那個 fallback 到底走不走得到:如果走不到,**刪掉**比補 +fixture 好。 + +### B6. 跨套件使用 macro + +跨套件的 build tool plugin 已覆蓋(`spm/PluginDependency`),macro target 沒有。 + +### B7. asset catalog 的各種變體 + +只建過 colorset。app icon set、symbol set 與產生的 asset symbols 都沒有。 + +## C. 流程 + +### C1. 子套件沒有測試 —— 刻意如此 + +fixture 底下有 11 個套件(`vendor-kit`、`Alt`、`Other`、`Stamping`、 +`Products`、`Trait/Dependency`、`TraitGraph/*Dependency`、 +`DependencyCondition/Extras`、`DependencyCondition/LinuxOnly`)是給人依賴用的, +本身沒有東西好斷言。CI 對每一個都跑 `swift build`,只有存在 `Tests` 目錄的才跑 +`swift test` —— 不會為了讓指令回 0 而塞一堆證明不了任何事的測試。 + +### C2. fixture lane 不會因為產生器的提醒而失敗 + +`IntegrateIOS` 會在產生的 log 裡 grep `did not run the`;套件的 lane 什麼都沒 +檢查。像 A1 那種提醒不會讓 CI 變紅。補起來很便宜:把產生輸出寫進 log,對在意的 +提醒讓 lane 失敗。 + +### C3. `TargetEmbed` 需要 `--build-system native` + +在這個工具鏈上,預設 build system 完全不產 `.embedInCode` 需要的 +`PackageResources`,那是 SwiftPM 自己的缺口。它是唯一需要這個 flag 的 fixture +—— 這也正是那條規則被拆成獨立套件的原因。 From 92baa2f815cf67b401ca49cc5dee09939b3ddd55 Mon Sep 17 00:00:00 2001 From: yume190 Date: Wed, 23 Sep 2026 09:55:05 +0800 Subject: [PATCH 36/47] fix(xcode): ask the resolved graph which package a product belongs to MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An Xcode target can name a product without saying which package it came from, and the answer was guessed: the repository whose name matched the product's. That covers one package with one product named after its repository and nothing else — a product named unlike its repository resolved to no package, and a package whose checkout directory is not its repository's last path component resolved to a label that does not exist. The resolved graph knows: every manifest is dumped, so which package declares a product is a fact by then. A name two packages both ship answers for neither, because nothing in an Xcode target says which one it meant. Target rules are written after the packages are resolved for that reason; they needed nothing else from that step. --- Sources/BazelizeKit/Kit.swift | 34 ++++++++++++++++++- .../BazelizeKit/Plugin/Plugin+SwiftPM.swift | 14 ++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/Sources/BazelizeKit/Kit.swift b/Sources/BazelizeKit/Kit.swift index 7c11fc3..22622fc 100644 --- a/Sources/BazelizeKit/Kit.swift +++ b/Sources/BazelizeKit/Kit.swift @@ -64,7 +64,12 @@ public final class Kit { // try await loadPlugins(mainfest) try generate() + /// Which package a product belongs to is the resolved graph's answer, + /// so the rules that name one are written once the packages have been + /// resolved — everything those rules need besides that is already in + /// the project. try await generateSwiftPackages() + try generateTargetBuild() } public final func dump() throws { @@ -124,11 +129,39 @@ extension Kit { deployment: deployment) try await generator.generate(locals: locals) packageTips = generator.notes + packageDirectoryByProduct(of: workspace) let count = workspace.packages.count Log.codeGenerate.info("Generate \(count, privacy: .public) Swift packages") } + /// Which package directory declares each product, for the target rules + /// that have to name one. + /// + /// A product name is the package's own, so two packages can ship one of + /// the same name; such a name answers for neither, because nothing in an + /// Xcode target says which package it meant. + private final func packageDirectoryByProduct(of workspace: SwiftPM.Workspace) { + var directories: [String: String] = [:] + var ambiguous: Set = [] + + for package in workspace.packages { + for product in package.manifest.products { + if let existing = directories[product.name], existing != package.directory { + ambiguous.insert(product.name) + continue + } + directories[product.name] = package.directory + } + } + + for name in ambiguous { + directories[name] = nil + } + + pluginSPM.packageDirectoryByProduct = directories + } + /// The versions a package's targets end up compiled at: the lowest deployment /// target of the project's own targets, per platform, because that is the one /// a package has to be buildable against. @@ -183,7 +216,6 @@ extension Kit { try generateBuild() try generateConfig() try generatePrebuiltBuild() - try generateTargetBuild() try generatePluginExtraFile() } diff --git a/Sources/BazelizeKit/Plugin/Plugin+SwiftPM.swift b/Sources/BazelizeKit/Plugin/Plugin+SwiftPM.swift index 5eda8be..e0712b2 100644 --- a/Sources/BazelizeKit/Plugin/Plugin+SwiftPM.swift +++ b/Sources/BazelizeKit/Plugin/Plugin+SwiftPM.swift @@ -20,6 +20,13 @@ final class PluginSwiftPM: PluginBuiltin { let locals: [LocalPackage] private var projectPath: Path? + /// Which package directory declares a product, answered by the resolved + /// graph rather than guessed from a repository's name. + /// + /// Empty until the packages have been resolved, which is why the target + /// rules that use it are written after that. + var packageDirectoryByProduct: [String: String] = [:] + func loadPackageNames(projPath: Path) async throws { projectPath = projPath } @@ -39,6 +46,13 @@ final class PluginSwiftPM: PluginBuiltin { /// NIO, from a remote package. private func remoteProduct(_ product: PackageProductDependency) -> FacadeProduct? { let name = product.productName + + /// What the resolved graph says: a product belongs to the package that + /// declares it, whatever that package's repository is called. + if let directory = packageDirectoryByProduct[name] { + return .init(package: directory, product: name) + } + guard let url = product.package ?? remoteURL(forProduct: name) else { return nil } return .init(package: Self.packageDirectoryName(url: url), product: name) From 07e50de27fd14c5ae65d01d92b4b9d8114a4f4a0 Mon Sep 17 00:00:00 2001 From: yume190 Date: Wed, 23 Sep 2026 10:04:57 +0800 Subject: [PATCH 37/47] test(spm): a package with no products, and one that is only a plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both fold into fixtures that already have a lane rather than paying for two more. TargetExclude drops its product — a package can be nothing but targets, which is what an app's own package often is — and PluginDependency gains Marking, a package whose only target is a plugin and whose only product shares it. Marking also shows where plugins are built from: a plugin belonging to another package is compiled by bazelize, not by Bazel, so its own BUILD comes out empty — deliberately, because an empty BUILD still keeps the parent's globs out. --- TODO.md | 16 ++++++++++--- TODO.zh-Hant.md | 11 +++++++-- spm/PluginDependency/Marking/Package.swift | 17 ++++++++++++++ .../Marking/Plugins/Mark/Plugin.swift | 23 +++++++++++++++++++ spm/PluginDependency/Package.swift | 3 +++ .../PluginDependency/PluginDependency.swift | 7 +++++- .../PluginDependencyTests.swift | 5 ++++ spm/TargetExclude/Package.swift | 7 +++--- 8 files changed, 80 insertions(+), 9 deletions(-) create mode 100644 spm/PluginDependency/Marking/Package.swift create mode 100644 spm/PluginDependency/Marking/Plugins/Mark/Plugin.swift diff --git a/TODO.md b/TODO.md index e590ad9..e2689e5 100644 --- a/TODO.md +++ b/TODO.md @@ -81,9 +81,19 @@ versions in the bundle and a mapping derivable between them — is covered. Every fixture uses `from:`. For the generator these are the same path: SwiftPM resolves them and the generator reads the checkout. Low value. -### B4. A package with no products, and a plugin-only package - -Both are real shapes; neither is exercised. +### B4. A package with no products, and a plugin-only package — done + +Neither needed a lane of its own. `spm/TargetExclude` declares no products at +all — a package that is nothing but targets, built and tested and depended on +by nothing — and `spm/PluginDependency/Marking` is nothing but a plugin and the +plugin product that shares it, used by the package next door. + +Worth knowing about the second: a plugin belonging to another package is +compiled by bazelize rather than built by Bazel. `Packages/Marking/BUILD` comes +out empty, because the rules that build a plugin are written for the package +that *uses* it, and Marking uses nothing. The empty file is deliberate: it +keeps the directory a Bazel package of its own, so the parent's globs do not +reach into it. ### B5. A target whose sources sit at the package root diff --git a/TODO.zh-Hant.md b/TODO.zh-Hant.md index 0cf01ae..b050c07 100644 --- a/TODO.zh-Hant.md +++ b/TODO.zh-Hant.md @@ -76,9 +76,16 @@ migration 真正立足的東西 —— 有版本的 `.xcdatamodeld`、兩個版 所有 fixture 都用 `from:`。對產生器而言這幾種是同一條路:SwiftPM 解析完,產生 器讀 checkout。價值低。 -### B4. 沒有 product 的套件,以及只有 plugin 的套件 +### B4. 沒有 product 的套件,以及只有 plugin 的套件 —— 已完成 -兩種都是真實形狀,目前都沒被踩過。 +兩個都不需要自己的 lane。`spm/TargetExclude` 完全不宣告 product —— 一個只有 +target 的套件,建得起來、測得過、沒有人依賴它;`spm/PluginDependency/Marking` +則是只有一個 plugin 與對應的 plugin product,由隔壁的套件使用。 + +第二點有件事值得記住:**屬於別的套件的 plugin 是由 bazelize 自己編的,不是 +Bazel 建的**。`Packages/Marking/BUILD` 產出來是空檔,因為建 plugin 的規則是寫給 +「使用它的套件」,而 Marking 誰也沒用。那個空檔是刻意的:它讓那個目錄成為獨立的 +Bazel package,父層的 glob 就抓不進去。 ### B5. 原始碼直接放在套件根目錄的 target diff --git a/spm/PluginDependency/Marking/Package.swift b/spm/PluginDependency/Marking/Package.swift new file mode 100644 index 0000000..1cf63df --- /dev/null +++ b/spm/PluginDependency/Marking/Package.swift @@ -0,0 +1,17 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +/// A package that is nothing but a plugin: no library, no executable, nothing +/// this workspace builds a rule for. What it ships is the plugin itself, and +/// the command that plugin asks for runs a program every machine already has. +let package = Package( + name: "Marking", + products: [ + .plugin(name: "Mark", targets: ["Mark"]), + ], + targets: [ + .plugin( + name: "Mark", + capability: .buildTool()), + ]) diff --git a/spm/PluginDependency/Marking/Plugins/Mark/Plugin.swift b/spm/PluginDependency/Marking/Plugins/Mark/Plugin.swift new file mode 100644 index 0000000..c398bb6 --- /dev/null +++ b/spm/PluginDependency/Marking/Plugins/Mark/Plugin.swift @@ -0,0 +1,23 @@ +import Foundation +import PackagePlugin + +@main +struct Mark: BuildToolPlugin { + func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { + let directory = context.pluginWorkDirectoryURL.appending(component: "Marked") + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + + let output = directory.appending(component: "Mark.generated.swift") + + return [ + .prebuildCommand( + displayName: "Mark \(target.name)", + executable: URL(fileURLWithPath: "/bin/sh"), + arguments: [ + "-c", + "printf 'public let mark = \"marked %s\"\\n' '\(target.name)' > '\(output.path())'", + ], + outputFilesDirectory: directory), + ] + } +} diff --git a/spm/PluginDependency/Package.swift b/spm/PluginDependency/Package.swift index 75d3024..beacbdb 100644 --- a/spm/PluginDependency/Package.swift +++ b/spm/PluginDependency/Package.swift @@ -12,12 +12,15 @@ let package = Package( ], dependencies: [ .package(path: "Stamping"), + .package(path: "Marking"), ], targets: [ .target( name: "PluginDependency", plugins: [ .plugin(name: "Stamp", package: "Stamping"), + /// From a package that is nothing but this plugin. + .plugin(name: "Mark", package: "Marking"), ]), .testTarget( name: "PluginDependencyTests", diff --git a/spm/PluginDependency/Sources/PluginDependency/PluginDependency.swift b/spm/PluginDependency/Sources/PluginDependency/PluginDependency.swift index 317288f..d4e4ef0 100644 --- a/spm/PluginDependency/Sources/PluginDependency/PluginDependency.swift +++ b/spm/PluginDependency/Sources/PluginDependency/PluginDependency.swift @@ -1,4 +1,9 @@ -/// `stamp` is not here: the plugin of the package next door writes it. +/// Neither `stamp` nor `mark` is here: one comes from the plugin of the package +/// next door, the other from a package that is nothing but a plugin. public func stampedValue() -> String { stamp } + +public func markedValue() -> String { + mark +} diff --git a/spm/PluginDependency/Tests/PluginDependencyTests/PluginDependencyTests.swift b/spm/PluginDependency/Tests/PluginDependencyTests/PluginDependencyTests.swift index cf3e517..af24b39 100644 --- a/spm/PluginDependency/Tests/PluginDependencyTests/PluginDependencyTests.swift +++ b/spm/PluginDependency/Tests/PluginDependencyTests/PluginDependencyTests.swift @@ -5,3 +5,8 @@ import Testing func aPluginFromAnotherPackageRan() { #expect(stampedValue() == "stamped PluginDependency") } + +@Test +func aPackageThatIsNothingButAPluginRan() { + #expect(markedValue() == "marked PluginDependency") +} diff --git a/spm/TargetExclude/Package.swift b/spm/TargetExclude/Package.swift index 4429341..1301180 100644 --- a/spm/TargetExclude/Package.swift +++ b/spm/TargetExclude/Package.swift @@ -4,11 +4,12 @@ import PackageDescription /// `exclude:`: everything under the target's directory is compiled except what /// the manifest names. +/// +/// No products, either: a package can be nothing but targets — an app's own, +/// built and tested and depended on by nothing — and a generator that assumes +/// a library to hang the targets off would have nothing to write. let package = Package( name: "TargetExclude", - products: [ - .library(name: "TargetExclude", targets: ["TargetExclude"]), - ], targets: [ .target( name: "TargetExclude", From d07c4f857c74a9886bf9b670926359f235e56fe9 Mon Sep 17 00:00:00 2001 From: yume190 Date: Wed, 23 Sep 2026 10:19:12 +0800 Subject: [PATCH 38/47] test(spm): expand a macro that belongs to another package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The macro fixture had one package doing everything. A macro a package away is the shape a package that ships macros actually has, and it went through a product, which is the part nothing had exercised. It needed no generator change: the consumer's rule carries only its own package's plugin, and the one next door arrives because the library declaring the macro carries it and rules_swift propagates a compiler plugin to whoever depends on that library — the product facade included. Now something would notice if that stopped being true. --- TODO.md | 13 ++++++-- TODO.zh-Hant.md | 10 +++++-- spm/Macro/Package.swift | 9 ++++-- spm/Macro/Provider/Package.resolved | 15 ++++++++++ spm/Macro/Provider/Package.swift | 30 +++++++++++++++++++ .../Provider/Sources/Provider/Provider.swift | 13 ++++++++ .../ProviderMacros/ProviderMacro.swift | 25 ++++++++++++++++ spm/Macro/Sources/Stringify/Stringify.swift | 14 +++++++++ .../Tests/StringifyTests/StringifyTests.swift | 6 ++++ 9 files changed, 128 insertions(+), 7 deletions(-) create mode 100644 spm/Macro/Provider/Package.resolved create mode 100644 spm/Macro/Provider/Package.swift create mode 100644 spm/Macro/Provider/Sources/Provider/Provider.swift create mode 100644 spm/Macro/Provider/Sources/ProviderMacros/ProviderMacro.swift diff --git a/TODO.md b/TODO.md index e2689e5..714d5f6 100644 --- a/TODO.md +++ b/TODO.md @@ -102,10 +102,17 @@ SwiftPM only looks in `Sources`, `Source`, `src` and `srcs` unless the target names a `path:`. Check whether that fallback is reachable at all: if it is not, delete it rather than write a fixture for it. -### B6. A macro used from another package +### B6. A macro used from another package — done -A build tool plugin from another package is covered (`spm/PluginDependency`); -a macro target is not. +`spm/Macro/Provider` ships a macro and the library that declares it; the root +package uses that macro through the product, beside the macro of its own it +already used. + +Nothing in the generator needed changing, which is the thing worth knowing: the +consumer's rule lists only its own package's plugin, and the one a package away +arrives because the library that declares the macro carries +`plugins = [":ProviderMacros"]` and rules_swift propagates a compiler plugin to +whoever depends on that library — through the product facade included. ### B7. Asset catalogue variants diff --git a/TODO.zh-Hant.md b/TODO.zh-Hant.md index b050c07..7bf1c9c 100644 --- a/TODO.zh-Hant.md +++ b/TODO.zh-Hant.md @@ -94,9 +94,15 @@ Bazel package,父層的 glob 就抓不進去。 `path:`)。要查的是那個 fallback 到底走不走得到:如果走不到,**刪掉**比補 fixture 好。 -### B6. 跨套件使用 macro +### B6. 跨套件使用 macro —— 已完成 -跨套件的 build tool plugin 已覆蓋(`spm/PluginDependency`),macro target 沒有。 +`spm/Macro/Provider` 提供一個 macro 與宣告它的 library;root 套件透過那個 +product 使用它,與它自己原本就有的同套件 macro 並存。 + +產生器**一行都不用改**,而這正是值得記住的地方:consumer 的規則裡只列自己套件的 +plugin,隔壁套件那個之所以生效,是因為宣告該 macro 的 library 帶著 +`plugins = [":ProviderMacros"]`,而 rules_swift 會把 compiler plugin 傳播給 +依賴那個 library 的人 —— 經過 product facade 也一樣。 ### B7. asset catalog 的各種變體 diff --git a/spm/Macro/Package.swift b/spm/Macro/Package.swift index 12bbd2c..810f3d8 100644 --- a/spm/Macro/Package.swift +++ b/spm/Macro/Package.swift @@ -4,7 +4,7 @@ import CompilerPluginSupport import PackageDescription /// A macro: a target the compiler loads as a plugin while it compiles another -/// target of the same package. +/// target — one of the same package, and one a package away. let package = Package( name: "Macro", /// The host the macro is built for. Without it SwiftPM builds it for the @@ -18,6 +18,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/swiftlang/swift-syntax", from: "600.0.0"), + .package(path: "Provider"), ], targets: [ .macro( @@ -28,7 +29,11 @@ let package = Package( ]), .target( name: "Stringify", - dependencies: ["StringifyMacros"]), + dependencies: [ + "StringifyMacros", + /// The macro of another package, reached through its product. + .product(name: "Provider", package: "Provider"), + ]), .testTarget( name: "StringifyTests", dependencies: ["Stringify"]), diff --git a/spm/Macro/Provider/Package.resolved b/spm/Macro/Provider/Package.resolved new file mode 100644 index 0000000..fd002bf --- /dev/null +++ b/spm/Macro/Provider/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "defd792d3ee0a468aea1b4ab28593524fe409fcc0ec51d7d7b419e4ee560721c", + "pins" : [ + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax", + "state" : { + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" + } + } + ], + "version" : 3 +} diff --git a/spm/Macro/Provider/Package.swift b/spm/Macro/Provider/Package.swift new file mode 100644 index 0000000..f64ece6 --- /dev/null +++ b/spm/Macro/Provider/Package.swift @@ -0,0 +1,30 @@ +// swift-tools-version: 6.0 + +import CompilerPluginSupport +import PackageDescription + +/// The package next door, which ships a macro: the compiler has to load its +/// plugin while it compiles whoever uses the macro, and that is a package away +/// from where the plugin is built. +let package = Package( + name: "Provider", + platforms: [ + .macOS(.v10_15), + ], + products: [ + .library(name: "Provider", targets: ["Provider"]), + ], + dependencies: [ + .package(url: "https://github.com/swiftlang/swift-syntax", from: "600.0.0"), + ], + targets: [ + .macro( + name: "ProviderMacros", + dependencies: [ + .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), + .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), + ]), + .target( + name: "Provider", + dependencies: ["ProviderMacros"]), + ]) diff --git a/spm/Macro/Provider/Sources/Provider/Provider.swift b/spm/Macro/Provider/Sources/Provider/Provider.swift new file mode 100644 index 0000000..8c17063 --- /dev/null +++ b/spm/Macro/Provider/Sources/Provider/Provider.swift @@ -0,0 +1,13 @@ +/// Declared here and implemented in this package's macro target: whoever +/// imports this module expands the macro with a plugin built a package away. +@freestanding(expression) +public macro shout(_ value: String) -> String = #externalMacro( + module: "ProviderMacros", + type: "ShoutMacro") + +public enum Provider { + /// The same expansion, done inside the package that ships the macro. + public static var here: String { + #shout("here") + } +} diff --git a/spm/Macro/Provider/Sources/ProviderMacros/ProviderMacro.swift b/spm/Macro/Provider/Sources/ProviderMacros/ProviderMacro.swift new file mode 100644 index 0000000..9511736 --- /dev/null +++ b/spm/Macro/Provider/Sources/ProviderMacros/ProviderMacro.swift @@ -0,0 +1,25 @@ +import SwiftCompilerPlugin +import SwiftSyntax +import SwiftSyntaxMacros + +/// `#shout("hi")` becomes `"HI"`, which nothing but an expansion produces. +struct ShoutMacro: ExpressionMacro { + static func expansion( + of node: some FreestandingMacroExpansionSyntax, + in _: some MacroExpansionContext) throws -> ExprSyntax + { + guard + let argument = node.arguments.first?.expression, + let literal = argument.as(StringLiteralExprSyntax.self)?.representedLiteralValue + else { + fatalError("#shout takes one string literal") + } + + return "\(literal: literal.uppercased())" + } +} + +@main +struct ProviderPlugin: CompilerPlugin { + let providingMacros: [any Macro.Type] = [ShoutMacro.self] +} diff --git a/spm/Macro/Sources/Stringify/Stringify.swift b/spm/Macro/Sources/Stringify/Stringify.swift index 1b01995..5d70ede 100644 --- a/spm/Macro/Sources/Stringify/Stringify.swift +++ b/spm/Macro/Sources/Stringify/Stringify.swift @@ -1,3 +1,5 @@ +import Provider + /// Expanded by this package's own macro target. @freestanding(expression) public macro stringify(_ value: T) -> (T, String) = #externalMacro( @@ -9,4 +11,16 @@ public enum Stringify { public static var onePlusOne: (Int, String) { #stringify(1 + 1) } + + /// Expanded by the macro of another package: the plugin the compiler loads + /// for this is built a package away, and reached through that package's + /// product. + public static var shouted: String { + #shout("across") + } + + /// The same macro, expanded inside the package that ships it. + public static var shoutedThere: String { + Provider.here + } } diff --git a/spm/Macro/Tests/StringifyTests/StringifyTests.swift b/spm/Macro/Tests/StringifyTests/StringifyTests.swift index 003e3eb..5b8ea46 100644 --- a/spm/Macro/Tests/StringifyTests/StringifyTests.swift +++ b/spm/Macro/Tests/StringifyTests/StringifyTests.swift @@ -6,3 +6,9 @@ func macroExpands() { #expect(Stringify.onePlusOne.0 == 2) #expect(Stringify.onePlusOne.1 == "1 + 1") } + +@Test +func aMacroOfAnotherPackageExpands() { + #expect(Stringify.shouted == "ACROSS") + #expect(Stringify.shoutedThere == "HERE") +} From 477c242908d3b1a4e6ffa067c90d7a1b10a90a71 Mon Sep 17 00:00:00 2001 From: yume190 Date: Wed, 23 Sep 2026 10:31:24 +0800 Subject: [PATCH 39/47] ci(spm): fail a lane on a note no fixture expects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A note is the generator saying the build differs from what the package asked for — a plugin that did not run, a program whose resources nothing can bundle, a library pkg-config has never heard of. All of those build and test green and are wrong when something runs, and the package lanes threw that output away; only the iOS lane read any of it. The lanes keep it now and fail on anything said, unless the lane names the note it exists for. No fixture says anything today, so the gate starts shut: the next note is a red lane rather than a line nobody reads. --- .github/workflows/swift.yml | 20 +++++++++++++++++++- TODO.md | 17 ++++++++++++----- TODO.zh-Hant.md | 12 ++++++++---- 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 4629243..9fcf6f0 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -422,12 +422,30 @@ jobs: # A package that wraps a system library is found through `pkg-config`, # and a fixture that ships its own `.pc` is only found when that file's # directory is on the path. + # + # A note is the generator saying the build differs from what the package + # asked for — a plugin that did not run, a program whose resources + # nothing can bundle, a library pkg-config has never heard of. Every one + # of those builds and tests green and is wrong at run time, so no fixture + # may produce one silently: a lane expecting a note names it in `notes`, + # and anything else said is a failure. - name: Bazel Generation env: PKG_CONFIG_PATH: ${{ github.workspace }}/spm/${{ matrix.name }}/vendor/pkgconfig + NOTES: ${{ matrix.notes }} run: | chmod +x bazelize - ./bazelize --project "spm/${{ matrix.name }}" --output "spm/${{ matrix.name }}/App" + ./bazelize --project "spm/${{ matrix.name }}" --output "spm/${{ matrix.name }}/App" | tee bazelize.log + + if [ -n "$NOTES" ]; then + if ! grep -qE "$NOTES" bazelize.log; then + echo "::error::the note this fixture is here for was not said: $NOTES" + exit 1 + fi + elif [ -s bazelize.log ]; then + echo "::error::the generator said something no fixture expects" + exit 1 + fi # A build tool plugin is a program: Bazel builds it and bazelize runs it, # which is what puts the sources it generates where the rules glob for diff --git a/TODO.md b/TODO.md index 714d5f6..20ff670 100644 --- a/TODO.md +++ b/TODO.md @@ -130,11 +130,18 @@ with nothing of their own to assert. CI builds every one of them and runs `swift test` only where a `Tests` directory exists, rather than padding them with tests that prove nothing. -### C2. A fixture lane does not fail on a generator note - -`IntegrateIOS` greps the generation log for `did not run the`; the package -lanes check nothing. A note like A1's would not turn CI red. Cheap to add: -generate into a log and fail the lane on the notes that matter. +### C2. A lane fails on a note no fixture expects — done + +A note is the generator saying the build differs from what the package asked +for: a plugin that did not run, a program whose resources nothing can bundle, a +library `pkg-config` has never heard of. Every one of those builds and tests +green and is wrong at run time, and the package lanes threw the generator's +output away. + +They now keep it and fail on anything said, unless the lane names the note it +is there for in `notes`. No fixture says anything today, so the gate starts +shut: a note that appears is a lane turning red, rather than a line nobody +reads. ### C3. `TargetEmbed` needs `--build-system native` diff --git a/TODO.zh-Hant.md b/TODO.zh-Hant.md index 7bf1c9c..b0b3cd7 100644 --- a/TODO.zh-Hant.md +++ b/TODO.zh-Hant.md @@ -118,11 +118,15 @@ fixture 底下有 11 個套件(`vendor-kit`、`Alt`、`Other`、`Stamping`、 本身沒有東西好斷言。CI 對每一個都跑 `swift build`,只有存在 `Tests` 目錄的才跑 `swift test` —— 不會為了讓指令回 0 而塞一堆證明不了任何事的測試。 -### C2. fixture lane 不會因為產生器的提醒而失敗 +### C2. lane 會因為沒人預期的 note 而失敗 —— 已完成 -`IntegrateIOS` 會在產生的 log 裡 grep `did not run the`;套件的 lane 什麼都沒 -檢查。像 A1 那種提醒不會讓 CI 變紅。補起來很便宜:把產生輸出寫進 log,對在意的 -提醒讓 lane 失敗。 +note 是產生器在說「這個 build 與套件要求的不一樣」:plugin 沒跑起來、程式的 +resources 沒有東西可以打包、`pkg-config` 不認識那個函式庫。這些情況 build 與 +test 都是綠的、錯在執行期,而 package lane 以前直接把產生器的輸出丟掉。 + +現在 lane 會留下輸出,**只要有話說就失敗**,除非該 lane 在 `notes` 裡指名它本來 +就是為那句話存在的。目前沒有任何 fixture 會說話,所以這道閘門是預設關著的:出現 +note 等於 lane 變紅,而不是多一行沒人看的字。 ### C3. `TargetEmbed` 需要 `--build-system native` From 55b82e0534f85581b0af40e465eb16681d3d9225 Mon Sep 17 00:00:00 2001 From: yume190 Date: Wed, 23 Sep 2026 10:44:29 +0800 Subject: [PATCH 40/47] fix(spm): find the sources of a package that has no directory for them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fallback looked for /, a layout SwiftPM warns about and builds an empty module from, so nothing valid ever reached it. What it missed is the source directory itself: files in Sources with no directory of the target's own, which SwiftPM allows when no other target could claim them. Such a package was dropped without a word — an empty BUILD and no rule for anything in it. The lookup now ends there instead, guarded by the condition SwiftPM applies, and ConfigurationCondition is laid out that way so it stays found. --- .../SwiftPM/SwiftPM+Generator.swift | 32 +++++++++++++++++-- TODO.md | 18 ++++++++--- TODO.zh-Hant.md | 15 ++++++--- spm/ConfigurationCondition/Package.swift | 4 +++ .../ConfigurationCondition.swift | 0 5 files changed, 57 insertions(+), 12 deletions(-) rename spm/ConfigurationCondition/Sources/{ConfigurationCondition => }/ConfigurationCondition.swift (100%) diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift index e426732..1810004 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift @@ -753,8 +753,36 @@ extension SwiftPM { if directory.exists { return directory } } - let flat = package.root + target.name - return flat.exists ? flat : nil + /// `Sources` itself, with the files in it and no directory of the + /// target's own: SwiftPM allows that when nothing else could claim + /// them, which is a package with one target of that kind. + guard Self.isOnlyTarget(target, in: package) else { return nil } + + for candidate in candidates { + let directory = package.root + candidate + if directory.isDirectory { return directory } + } + + return nil + } + + /// Whether the package has no other target that a bare source + /// directory could belong to: a test target does not take `Tests` from + /// another test target, and a library does not take `Sources` from + /// another library. + private static func isOnlyTarget(_ target: PackageTarget, in package: Package) -> Bool { + let sameKind = package.manifest.targets.filter { other in + switch (other.type, target.type) { + case ("test", "test"), ("plugin", "plugin"): + return true + case ("test", _), (_, "test"), ("plugin", _), (_, "plugin"): + return false + default: + return true + } + } + + return sameKind.count == 1 } private func build( diff --git a/TODO.md b/TODO.md index 20ff670..f907aa1 100644 --- a/TODO.md +++ b/TODO.md @@ -95,12 +95,20 @@ that *uses* it, and Marking uses nothing. The empty file is deliberate: it keeps the directory a Bazel package of its own, so the parent's globs do not reach into it. -### B5. A target whose sources sit at the package root +### B5. A bare source directory — done, and it was a gap rather than dead code -`sourceDirectory(of:in:)` falls back to `package.root + target.name`, but -SwiftPM only looks in `Sources`, `Source`, `src` and `srcs` unless the target -names a `path:`. Check whether that fallback is reachable at all: if it is not, -delete it rather than write a fixture for it. +The fallback looked for `/`. SwiftPM warns about +that layout and builds an empty module from it — a target laid out that way +cannot be imported — so nothing valid ever reached the fallback. + +What SwiftPM does allow, and what the fallback missed, is the source directory +*itself*: files in `Sources` with no directory of the target's own, which is +legal when no other target could claim them. Such a package was dropped +silently — an empty `BUILD` and no rule. + +`sourceDirectory` looks there now, guarded by the same condition SwiftPM +applies: the package has one target of that kind. `spm/ConfigurationCondition` +is laid out that way, so something would notice. ### B6. A macro used from another package — done diff --git a/TODO.zh-Hant.md b/TODO.zh-Hant.md index b0b3cd7..1bb6e23 100644 --- a/TODO.zh-Hant.md +++ b/TODO.zh-Hant.md @@ -87,12 +87,17 @@ Bazel 建的**。`Packages/Marking/BUILD` 產出來是空檔,因為建 plugin 「使用它的套件」,而 Marking 誰也沒用。那個空檔是刻意的:它讓那個目錄成為獨立的 Bazel package,父層的 glob 就抓不進去。 -### B5. 原始碼直接放在套件根目錄的 target +### B5. 裸的來源目錄 —— 已完成,而且它不是死碼、是缺口 -`sourceDirectory(of:in:)` 有一個 fallback 會找 `package.root + target.name`, -但 SwiftPM 只會在 `Sources`、`Source`、`src`、`srcs` 裡找(除非 target 自己寫了 -`path:`)。要查的是那個 fallback 到底走不走得到:如果走不到,**刪掉**比補 -fixture 好。 +原本的 fallback 找的是 `<套件根目錄>/`。SwiftPM 對那種佈局會發警告, +而且編出來是**空模組**(import 不到)—— 所以任何合法的套件都走不到那個 fallback。 + +SwiftPM 真正允許、而那個 fallback 漏掉的,是**來源目錄本身**:檔案直接放在 +`Sources`、沒有屬於 target 的子目錄,在沒有其他 target 會來搶的情況下是合法的。 +這種套件以前會被**靜默丟掉** —— 產出一個空的 `BUILD`、沒有任何規則。 + +`sourceDirectory` 現在會找那裡,條件與 SwiftPM 相同:該套件只有一個同類型的 +target。`spm/ConfigurationCondition` 就是這樣的佈局,所以之後有東西會叫。 ### B6. 跨套件使用 macro —— 已完成 diff --git a/spm/ConfigurationCondition/Package.swift b/spm/ConfigurationCondition/Package.swift index e022069..d779459 100644 --- a/spm/ConfigurationCondition/Package.swift +++ b/spm/ConfigurationCondition/Package.swift @@ -3,6 +3,10 @@ import PackageDescription /// Settings conditional on SwiftPM's debug and release build configurations. +/// +/// Its sources are in `Sources` itself rather than a directory of the target's +/// own, which SwiftPM allows when no other target could claim them — a package +/// with one target of that kind. let configurationSettings: [SwiftSetting] = [ .define("DEBUG_ONLY", .when(configuration: .debug)), .define("RELEASE_ONLY", .when(configuration: .release)), diff --git a/spm/ConfigurationCondition/Sources/ConfigurationCondition/ConfigurationCondition.swift b/spm/ConfigurationCondition/Sources/ConfigurationCondition.swift similarity index 100% rename from spm/ConfigurationCondition/Sources/ConfigurationCondition/ConfigurationCondition.swift rename to spm/ConfigurationCondition/Sources/ConfigurationCondition.swift From 9190a631b034637b3091a0cad22935c75e8225bd Mon Sep 17 00:00:00 2001 From: yume190 Date: Wed, 23 Sep 2026 10:55:27 +0800 Subject: [PATCH 41/47] feat: one //:tool for everything a workspace answers or is told MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The workspace had a runnable per thing to say: //:plugins at the root and a //tools:list- for each listing, with a wrapper mapping half of them. They are one program now — `bazel run //:tool -- plugin` and `… -- list config|trait|language`, with the wrapper taking `bazel plugin` and `bazel list …` as before. What `list` answers is still embedded when the workspace is generated, so asking never needs bazelize on PATH; `plugin` still does, because running a plugin is the part Bazel cannot do, and the programs it runs are still `data` so running it builds them first. A project with no Swift packages gets the tool too: the command says there are no packages rather than naming a filegroup that does not exist. --- .github/workflows/swift.yml | 8 +- .../BazelizeKit/Plugin/Plugin+SwiftPM.swift | 23 ++- .../SwiftPM/SwiftPM+Generator.swift | 5 +- .../SwiftPM/SwiftPM+PluginRule.swift | 178 ++++++++++-------- spm/README.md | 22 ++- 5 files changed, 125 insertions(+), 111 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 9fcf6f0..7638fc5 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -453,7 +453,7 @@ jobs: - name: Run Plugins if: matrix.plugins working-directory: spm/${{ matrix.name }}/App - run: PATH="$GITHUB_WORKSPACE:$PATH" bazel run //:plugins + run: PATH="$GITHUB_WORKSPACE:$PATH" bazel run //:tool -- plugin # The workspace's own commands, which answer whatever the package is: # a package with no trait, no configuration and no localization answers @@ -461,9 +461,9 @@ jobs: - name: List Workspace working-directory: spm/${{ matrix.name }}/App run: | - PATH="$GITHUB_WORKSPACE:$PATH" bazel list config - PATH="$GITHUB_WORKSPACE:$PATH" bazel list trait - PATH="$GITHUB_WORKSPACE:$PATH" bazel list language + bazel run //:tool -- list config + bazel run //:tool -- list trait + bazel run //:tool -- list language # A build that names a localization bundles that one and `Base`, and # nothing else. What is in the bundle is the only proof of that. diff --git a/Sources/BazelizeKit/Plugin/Plugin+SwiftPM.swift b/Sources/BazelizeKit/Plugin/Plugin+SwiftPM.swift index e0712b2..2e2adc8 100644 --- a/Sources/BazelizeKit/Plugin/Plugin+SwiftPM.swift +++ b/Sources/BazelizeKit/Plugin/Plugin+SwiftPM.swift @@ -183,28 +183,27 @@ final class PluginSwiftPM: PluginBuiltin { pinnedRevisions[Self.repositoryModuleName(url: url).lowercased()] } - /// `bazel run //:plugins`: what brings the files a build tool plugin writes - /// up to date, without generating the workspace again. + /// `bazel run //:tool`: the workspace's own commands — what it can be + /// asked about itself, and the one thing that writes back into it. /// - /// A plugin decides what it writes, so changing the plugin changes those - /// files while nothing else about the project moves — and the rules glob the - /// directory rather than name the files, so they need no regenerating. This - /// is the workspace's own way to run them, the way `bazel mod tidy` is the - /// workspace's way to fix its module file. + /// `plugin` brings the files a build tool plugin writes up to date without + /// generating the workspace again: a plugin decides what it writes, so + /// changing the plugin changes those files while nothing else about the + /// project moves, and the rules glob the directory rather than name the + /// files. That is the workspace's way to run them, as `bazel mod tidy` is + /// its way to fix its module file. /// /// The plugins and the tools they run are `data`, so running this builds /// them: the script speaks to programs Bazel made, not to SwiftPM. The /// script itself is written by the package generator, which is what knows /// which programs those are. override func build(_ builder: CodeBuilder) { - guard hasPackages else { return } - builder.load(loadableRule: Rules.Shell.sh_binary) builder.call( Rules.Shell.Call.sh_binary( - name: "plugins", - srcs: ["plugins.sh"], - data: ["//\(Self.packagesDirectory):plugins"])) + name: "tool", + srcs: ["tool.sh"], + data: hasPackages ? ["//\(Self.packagesDirectory):plugins"] : [])) } override var custom: [PluginBuiltin.Custom]? { diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift index 1810004..f4da002 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift @@ -101,13 +101,14 @@ extension SwiftPM { try generate(package) } - try writePluginRunner(locals: locals) /// Written whether or not there is a trait to switch: the root /// `.bazelrc` imports it, and an import of a file that is not /// there is a workspace that does not load. try writeTraitConfigs() try writeLanguageConfigs() - try writeListingCommands() + /// Last, because what it answers about this workspace includes the + /// files above. + try writeWorkspaceTool(locals: locals) } /// A package that declares a platform version the project does not reach is diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginRule.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginRule.swift index 00f3433..336b063 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginRule.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginRule.swift @@ -48,19 +48,96 @@ extension SwiftPM.Generator { visibility: .public)) } - /// `bazel run //:plugins`, and everything it needs built first. + /// `bazel run //:tool`: everything the generated workspace can be asked or + /// told, in one program. /// - /// The plugins and the tools they run are `data` of the script, so running - /// it builds them: a script that called `bazel build` itself would be a - /// second Bazel inside the first one's lock. - func writePluginRunner(locals: [Path]) throws { - /// The root `BUILD` declares `//:plugins` for any project with packages, - /// because whether one of them has a plugin is not known when that file - /// is written. So both of the things it names are written for any such - /// project: a package with nothing to run is a command that does - /// nothing, and a label that does not resolve is a workspace that does - /// not load. - guard (output + "Package.swift").exists else { return } + /// `list` answers out of text embedded when the workspace was generated, + /// so asking never depends on whichever `bazelize` happens to be on + /// `PATH`. `plugin` does depend on it, because running a plugin is what + /// bazelize does that Bazel cannot: the plugins and the tools they run are + /// `data` of this script, so running it builds them first — a script that + /// called `bazel build` itself would be a second Bazel inside the first + /// one's lock. + /// + /// Bazel has no extension point for custom commands, so `tools/bazel` + /// keeps `bazel plugin` and `bazel list …` as aliases for it and forwards + /// every other command unchanged. + func writeWorkspaceTool(locals: [Path]) throws { + let listings = [ + ("config", try Listing.config(output: output)), + ("trait", Listing.traits(workspace: workspace)), + ("language", Listing.languages(localizations)), + ] + + let answers = listings.map { topic, contents in + """ + \(topic)) + cat <<'BAZELIZE_LIST' + \(contents) + BAZELIZE_LIST + ;; + """ + }.joined(separator: "\n") + + let script = output + "tool.sh" + try script.write(""" + #!/bin/bash + # What this workspace can be asked about itself, and the one thing that + # writes back into it. + set -euo pipefail + runfiles="${RUNFILES_DIR:-$0.runfiles}/_main" + cd "${BUILD_WORKSPACE_DIRECTORY:-$(dirname "$0")}" + + usage() { + cat >&2 <<'BAZELIZE_USAGE' + Usage: bazel run //:tool -- + + plugin run this workspace's build tool plugins, + writing what they generate back into + Packages/*/Generated/*Plugin + list config the --config this workspace defines + list trait the traits its packages declare + list language the localizations its packages ship + BAZELIZE_USAGE + exit 2 + } + + case "${1:-}" in + plugin) + \(pluginCommand(locals: locals)) + ;; + list) + case "${2:-}" in + \(answers) + *) + usage + ;; + esac + ;; + *) + usage + ;; + esac + + """) + + /// `sh_binary` refuses a script that is not executable. + try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: script.string) + + let directory = output + "tools" + try directory.mkpath() + try writeBazelWrapper(to: directory + "bazel") + } + + /// What `plugin` runs, and the filegroup of programs it needs built. + /// + /// A project with no packages has no plugin to run and no filegroup to + /// name: the command says so rather than naming a label that does not + /// resolve, which is a workspace that does not load. + private func pluginCommand(locals: [Path]) throws -> String { + guard (output + "Package.swift").exists else { + return #"echo "This workspace has no Swift packages." >&2"# + } let binaries = pluginBinaries @@ -82,66 +159,7 @@ extension SwiftPM.Generator { [binary.isPlugin ? "--plugin" : "--tool", "\(binary.name)=$runfiles/\(binary.path)"] } - let script = output + "plugins.sh" - try script.write(""" - #!/bin/bash - # Runs this workspace's build tool plugins, writing what they generate - # back into `Packages/*/Generated/*Plugin`. - # - # The plugins and their tools are built by Bazel: they are `data` of this - # script, so they are in its runfiles by the time it runs. - set -euo pipefail - runfiles="${RUNFILES_DIR:-$0.runfiles}/_main" - cd "${BUILD_WORKSPACE_DIRECTORY:-$(dirname "$0")}" - exec bazelize plugins \(arguments.joined(separator: " ")) - - """) - - /// `sh_binary` refuses a script that is not executable. - try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: script.string) - } - - /// Bazel-native commands that describe the generated workspace. - /// - /// The answers are embedded in executable targets, so using them never - /// depends on whichever `bazelize` executable happens to be on `PATH`. - /// Bazel itself has no extension point for custom commands; `tools/bazel` - /// keeps `bazel list config|trait|language` as aliases for the `bazel run` - /// targets and forwards every other command unchanged. - func writeListingCommands() throws { - let directory = output + "tools" - try directory.mkpath() - - let listings = [ - ("config", try Listing.config(output: output)), - ("trait", Listing.traits(workspace: workspace)), - ("language", Listing.languages(localizations)), - ] - let builder = CodeBuilder() - builder.load(loadableRule: Rules.Shell.sh_binary) - - for (topic, contents) in listings { - let name = "list-\(topic)" - let script = directory + "\(name).sh" - try script.write(""" - #!/bin/bash - cat <<'BAZELIZE_LIST' - \(contents) - BAZELIZE_LIST - - """) - try FileManager.default.setAttributes( - [.posixPermissions: 0o755], - ofItemAtPath: script.string) - builder.call( - Rules.Shell.Call.sh_binary( - name: name, - srcs: ["\(name).sh"])) - } - - try (directory + "BUILD").write(builder.build()) - - try writeBazelWrapper(to: directory + "bazel") + return "exec bazelize plugins \(arguments.joined(separator: " "))" } private func writeBazelWrapper(to wrapper: Path) throws { @@ -155,17 +173,11 @@ extension SwiftPM.Generator { exit 1 fi - if [[ "${1:-}" == "list" ]]; then - case "${2:-}" in - config|trait|language) - exec "$BAZEL_REAL" run "//tools:list-${2}" - ;; - *) - echo "Usage: bazel list config|trait|language" >&2 - exit 2 - ;; - esac - fi + case "${1:-}" in + plugin|list) + exec "$BAZEL_REAL" run //:tool -- "$@" + ;; + esac exec "$BAZEL_REAL" "$@" diff --git a/spm/README.md b/spm/README.md index 78cabe8..50a8ff9 100644 --- a/spm/README.md +++ b/spm/README.md @@ -55,11 +55,11 @@ linked, so the package holding it still builds anywhere. cd spm/ bazelize --project . --output App cd App -bazel run //:plugins # only the packages with a build tool plugin need this +bazel run //:tool -- plugin # only the packages with a build tool plugin need this bazel test //... -bazel run //tools:list-config # what `--config=` the workspace defines -bazel run //tools:list-trait # which traits its packages declare, and which are on -bazel run //tools:list-language # which localizations they ship +bazel run //:tool -- list config # what `--config=` the workspace defines +bazel run //:tool -- list trait # which traits its packages declare, and which are on +bazel run //:tool -- list language # which localizations they ship bazel test //... --config=. # …with one of them turned on bazel build //... --config=lang. # …bundling that localization only ``` @@ -83,18 +83,20 @@ trait selected: the selection replaces the package's defaults and carries whatever the trait enables, which is what `swift test --traits ` does. The flags underneath are there for a build that wants some other combination. -`bazel run //:plugins` runs this workspace's build tool plugins and writes what -they generate into `Packages//Generated/`. It is a separate step -because a plugin is a program: Bazel builds it, and bazelize runs it as SwiftPM -would. +`bazel run //:tool -- plugin` runs this workspace's build tool plugins and +writes what they generate into `Packages//Generated/`. It is a +separate step because a plugin is a program: Bazel builds it, and bazelize runs +it as SwiftPM would. `bazel build --config=lang.` bundles that localization and `Base`, and nothing else; a build that names none bundles every one of them, which is what SwiftPM does. Several at once is the flag underneath, `--@build_bazel_rules_apple//apple/build_settings:locales_to_include=en,ja`. -The listing commands are generated Bazel targets and do not need `bazelize` at -runtime. The generated `tools/bazel` wrapper also exposes them as +`//:tool` is a generated Bazel target: what `list` answers is embedded when the +workspace is generated, so asking never needs `bazelize` at run time. Only +`plugin` does, because running a plugin is the part Bazel cannot do. The +generated `tools/bazel` wrapper also takes them directly, as `bazel plugin` and `bazel list config|trait|language`. `App/` is generated, and is not checked in. From 3901877eda7c58ed2ed4448fe415a03993ea8725 Mon Sep 17 00:00:00 2001 From: yume190 Date: Wed, 23 Sep 2026 11:21:00 +0800 Subject: [PATCH 42/47] fix: restore generated workspace command labels --- .github/workflows/swift.yml | 8 +- .../BazelizeKit/Plugin/Plugin+SwiftPM.swift | 23 +-- .../SwiftPM/SwiftPM+Generator.swift | 5 +- .../SwiftPM/SwiftPM+PluginRule.swift | 178 ++++++++---------- spm/README.md | 22 +-- 5 files changed, 111 insertions(+), 125 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 7638fc5..9fcf6f0 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -453,7 +453,7 @@ jobs: - name: Run Plugins if: matrix.plugins working-directory: spm/${{ matrix.name }}/App - run: PATH="$GITHUB_WORKSPACE:$PATH" bazel run //:tool -- plugin + run: PATH="$GITHUB_WORKSPACE:$PATH" bazel run //:plugins # The workspace's own commands, which answer whatever the package is: # a package with no trait, no configuration and no localization answers @@ -461,9 +461,9 @@ jobs: - name: List Workspace working-directory: spm/${{ matrix.name }}/App run: | - bazel run //:tool -- list config - bazel run //:tool -- list trait - bazel run //:tool -- list language + PATH="$GITHUB_WORKSPACE:$PATH" bazel list config + PATH="$GITHUB_WORKSPACE:$PATH" bazel list trait + PATH="$GITHUB_WORKSPACE:$PATH" bazel list language # A build that names a localization bundles that one and `Base`, and # nothing else. What is in the bundle is the only proof of that. diff --git a/Sources/BazelizeKit/Plugin/Plugin+SwiftPM.swift b/Sources/BazelizeKit/Plugin/Plugin+SwiftPM.swift index 2e2adc8..e0712b2 100644 --- a/Sources/BazelizeKit/Plugin/Plugin+SwiftPM.swift +++ b/Sources/BazelizeKit/Plugin/Plugin+SwiftPM.swift @@ -183,27 +183,28 @@ final class PluginSwiftPM: PluginBuiltin { pinnedRevisions[Self.repositoryModuleName(url: url).lowercased()] } - /// `bazel run //:tool`: the workspace's own commands — what it can be - /// asked about itself, and the one thing that writes back into it. + /// `bazel run //:plugins`: what brings the files a build tool plugin writes + /// up to date, without generating the workspace again. /// - /// `plugin` brings the files a build tool plugin writes up to date without - /// generating the workspace again: a plugin decides what it writes, so - /// changing the plugin changes those files while nothing else about the - /// project moves, and the rules glob the directory rather than name the - /// files. That is the workspace's way to run them, as `bazel mod tidy` is - /// its way to fix its module file. + /// A plugin decides what it writes, so changing the plugin changes those + /// files while nothing else about the project moves — and the rules glob the + /// directory rather than name the files, so they need no regenerating. This + /// is the workspace's own way to run them, the way `bazel mod tidy` is the + /// workspace's way to fix its module file. /// /// The plugins and the tools they run are `data`, so running this builds /// them: the script speaks to programs Bazel made, not to SwiftPM. The /// script itself is written by the package generator, which is what knows /// which programs those are. override func build(_ builder: CodeBuilder) { + guard hasPackages else { return } + builder.load(loadableRule: Rules.Shell.sh_binary) builder.call( Rules.Shell.Call.sh_binary( - name: "tool", - srcs: ["tool.sh"], - data: hasPackages ? ["//\(Self.packagesDirectory):plugins"] : [])) + name: "plugins", + srcs: ["plugins.sh"], + data: ["//\(Self.packagesDirectory):plugins"])) } override var custom: [PluginBuiltin.Custom]? { diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift index f4da002..1810004 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift @@ -101,14 +101,13 @@ extension SwiftPM { try generate(package) } + try writePluginRunner(locals: locals) /// Written whether or not there is a trait to switch: the root /// `.bazelrc` imports it, and an import of a file that is not /// there is a workspace that does not load. try writeTraitConfigs() try writeLanguageConfigs() - /// Last, because what it answers about this workspace includes the - /// files above. - try writeWorkspaceTool(locals: locals) + try writeListingCommands() } /// A package that declares a platform version the project does not reach is diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginRule.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginRule.swift index 336b063..00f3433 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginRule.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginRule.swift @@ -48,96 +48,19 @@ extension SwiftPM.Generator { visibility: .public)) } - /// `bazel run //:tool`: everything the generated workspace can be asked or - /// told, in one program. + /// `bazel run //:plugins`, and everything it needs built first. /// - /// `list` answers out of text embedded when the workspace was generated, - /// so asking never depends on whichever `bazelize` happens to be on - /// `PATH`. `plugin` does depend on it, because running a plugin is what - /// bazelize does that Bazel cannot: the plugins and the tools they run are - /// `data` of this script, so running it builds them first — a script that - /// called `bazel build` itself would be a second Bazel inside the first - /// one's lock. - /// - /// Bazel has no extension point for custom commands, so `tools/bazel` - /// keeps `bazel plugin` and `bazel list …` as aliases for it and forwards - /// every other command unchanged. - func writeWorkspaceTool(locals: [Path]) throws { - let listings = [ - ("config", try Listing.config(output: output)), - ("trait", Listing.traits(workspace: workspace)), - ("language", Listing.languages(localizations)), - ] - - let answers = listings.map { topic, contents in - """ - \(topic)) - cat <<'BAZELIZE_LIST' - \(contents) - BAZELIZE_LIST - ;; - """ - }.joined(separator: "\n") - - let script = output + "tool.sh" - try script.write(""" - #!/bin/bash - # What this workspace can be asked about itself, and the one thing that - # writes back into it. - set -euo pipefail - runfiles="${RUNFILES_DIR:-$0.runfiles}/_main" - cd "${BUILD_WORKSPACE_DIRECTORY:-$(dirname "$0")}" - - usage() { - cat >&2 <<'BAZELIZE_USAGE' - Usage: bazel run //:tool -- - - plugin run this workspace's build tool plugins, - writing what they generate back into - Packages/*/Generated/*Plugin - list config the --config this workspace defines - list trait the traits its packages declare - list language the localizations its packages ship - BAZELIZE_USAGE - exit 2 - } - - case "${1:-}" in - plugin) - \(pluginCommand(locals: locals)) - ;; - list) - case "${2:-}" in - \(answers) - *) - usage - ;; - esac - ;; - *) - usage - ;; - esac - - """) - - /// `sh_binary` refuses a script that is not executable. - try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: script.string) - - let directory = output + "tools" - try directory.mkpath() - try writeBazelWrapper(to: directory + "bazel") - } - - /// What `plugin` runs, and the filegroup of programs it needs built. - /// - /// A project with no packages has no plugin to run and no filegroup to - /// name: the command says so rather than naming a label that does not - /// resolve, which is a workspace that does not load. - private func pluginCommand(locals: [Path]) throws -> String { - guard (output + "Package.swift").exists else { - return #"echo "This workspace has no Swift packages." >&2"# - } + /// The plugins and the tools they run are `data` of the script, so running + /// it builds them: a script that called `bazel build` itself would be a + /// second Bazel inside the first one's lock. + func writePluginRunner(locals: [Path]) throws { + /// The root `BUILD` declares `//:plugins` for any project with packages, + /// because whether one of them has a plugin is not known when that file + /// is written. So both of the things it names are written for any such + /// project: a package with nothing to run is a command that does + /// nothing, and a label that does not resolve is a workspace that does + /// not load. + guard (output + "Package.swift").exists else { return } let binaries = pluginBinaries @@ -159,7 +82,66 @@ extension SwiftPM.Generator { [binary.isPlugin ? "--plugin" : "--tool", "\(binary.name)=$runfiles/\(binary.path)"] } - return "exec bazelize plugins \(arguments.joined(separator: " "))" + let script = output + "plugins.sh" + try script.write(""" + #!/bin/bash + # Runs this workspace's build tool plugins, writing what they generate + # back into `Packages/*/Generated/*Plugin`. + # + # The plugins and their tools are built by Bazel: they are `data` of this + # script, so they are in its runfiles by the time it runs. + set -euo pipefail + runfiles="${RUNFILES_DIR:-$0.runfiles}/_main" + cd "${BUILD_WORKSPACE_DIRECTORY:-$(dirname "$0")}" + exec bazelize plugins \(arguments.joined(separator: " ")) + + """) + + /// `sh_binary` refuses a script that is not executable. + try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: script.string) + } + + /// Bazel-native commands that describe the generated workspace. + /// + /// The answers are embedded in executable targets, so using them never + /// depends on whichever `bazelize` executable happens to be on `PATH`. + /// Bazel itself has no extension point for custom commands; `tools/bazel` + /// keeps `bazel list config|trait|language` as aliases for the `bazel run` + /// targets and forwards every other command unchanged. + func writeListingCommands() throws { + let directory = output + "tools" + try directory.mkpath() + + let listings = [ + ("config", try Listing.config(output: output)), + ("trait", Listing.traits(workspace: workspace)), + ("language", Listing.languages(localizations)), + ] + let builder = CodeBuilder() + builder.load(loadableRule: Rules.Shell.sh_binary) + + for (topic, contents) in listings { + let name = "list-\(topic)" + let script = directory + "\(name).sh" + try script.write(""" + #!/bin/bash + cat <<'BAZELIZE_LIST' + \(contents) + BAZELIZE_LIST + + """) + try FileManager.default.setAttributes( + [.posixPermissions: 0o755], + ofItemAtPath: script.string) + builder.call( + Rules.Shell.Call.sh_binary( + name: name, + srcs: ["\(name).sh"])) + } + + try (directory + "BUILD").write(builder.build()) + + try writeBazelWrapper(to: directory + "bazel") } private func writeBazelWrapper(to wrapper: Path) throws { @@ -173,11 +155,17 @@ extension SwiftPM.Generator { exit 1 fi - case "${1:-}" in - plugin|list) - exec "$BAZEL_REAL" run //:tool -- "$@" - ;; - esac + if [[ "${1:-}" == "list" ]]; then + case "${2:-}" in + config|trait|language) + exec "$BAZEL_REAL" run "//tools:list-${2}" + ;; + *) + echo "Usage: bazel list config|trait|language" >&2 + exit 2 + ;; + esac + fi exec "$BAZEL_REAL" "$@" diff --git a/spm/README.md b/spm/README.md index 50a8ff9..78cabe8 100644 --- a/spm/README.md +++ b/spm/README.md @@ -55,11 +55,11 @@ linked, so the package holding it still builds anywhere. cd spm/ bazelize --project . --output App cd App -bazel run //:tool -- plugin # only the packages with a build tool plugin need this +bazel run //:plugins # only the packages with a build tool plugin need this bazel test //... -bazel run //:tool -- list config # what `--config=` the workspace defines -bazel run //:tool -- list trait # which traits its packages declare, and which are on -bazel run //:tool -- list language # which localizations they ship +bazel run //tools:list-config # what `--config=` the workspace defines +bazel run //tools:list-trait # which traits its packages declare, and which are on +bazel run //tools:list-language # which localizations they ship bazel test //... --config=. # …with one of them turned on bazel build //... --config=lang. # …bundling that localization only ``` @@ -83,20 +83,18 @@ trait selected: the selection replaces the package's defaults and carries whatever the trait enables, which is what `swift test --traits ` does. The flags underneath are there for a build that wants some other combination. -`bazel run //:tool -- plugin` runs this workspace's build tool plugins and -writes what they generate into `Packages//Generated/`. It is a -separate step because a plugin is a program: Bazel builds it, and bazelize runs -it as SwiftPM would. +`bazel run //:plugins` runs this workspace's build tool plugins and writes what +they generate into `Packages//Generated/`. It is a separate step +because a plugin is a program: Bazel builds it, and bazelize runs it as SwiftPM +would. `bazel build --config=lang.` bundles that localization and `Base`, and nothing else; a build that names none bundles every one of them, which is what SwiftPM does. Several at once is the flag underneath, `--@build_bazel_rules_apple//apple/build_settings:locales_to_include=en,ja`. -`//:tool` is a generated Bazel target: what `list` answers is embedded when the -workspace is generated, so asking never needs `bazelize` at run time. Only -`plugin` does, because running a plugin is the part Bazel cannot do. The -generated `tools/bazel` wrapper also takes them directly, as `bazel plugin` and +The listing commands are generated Bazel targets and do not need `bazelize` at +runtime. The generated `tools/bazel` wrapper also exposes them as `bazel list config|trait|language`. `App/` is generated, and is not checked in. From 6edc24efc0266110be1ff7978451e2a328249670 Mon Sep 17 00:00:00 2001 From: yume190 Date: Wed, 23 Sep 2026 11:45:41 +0800 Subject: [PATCH 43/47] feat(spm): make plugin runner self-contained --- .github/workflows/swift.yml | 8 +- Sources/Bazelize/Command.swift | 13 +- .../BazelizeKit/Plugin/Plugin+SwiftPM.swift | 19 +- .../SwiftPM/SwiftPM+Generator.swift | 10 +- .../SwiftPM/SwiftPM+PluginHost.swift | 13 +- .../SwiftPM/SwiftPM+PluginRule.swift | 203 ++++++++--- .../SwiftPM/SwiftPM+PluginRunnerSource.swift | 341 ++++++++++++++++++ docs/SPM.md | 17 +- docs/SPM_ZH.md | 15 +- spm/README.md | 7 +- 10 files changed, 547 insertions(+), 99 deletions(-) create mode 100644 Sources/BazelizeKit/SwiftPM/SwiftPM+PluginRunnerSource.swift diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 9fcf6f0..ade0804 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -447,13 +447,13 @@ jobs: exit 1 fi - # A build tool plugin is a program: Bazel builds it and bazelize runs it, - # which is what puts the sources it generates where the rules glob for - # them. Only the packages that have one need the step. + # Bazel builds the plugin host, plugins and tools, then runs them from + # runfiles. No separately installed bazelize executable is involved. + # Only the packages that have a build tool plugin need the step. - name: Run Plugins if: matrix.plugins working-directory: spm/${{ matrix.name }}/App - run: PATH="$GITHUB_WORKSPACE:$PATH" bazel run //:plugins + run: bazel run //:plugins # The workspace's own commands, which answer whatever the package is: # a package with no trait, no configuration and no localization answers diff --git a/Sources/Bazelize/Command.swift b/Sources/Bazelize/Command.swift index d6d8d19..4dd250f 100644 --- a/Sources/Bazelize/Command.swift +++ b/Sources/Bazelize/Command.swift @@ -30,13 +30,11 @@ struct Command: AsyncParsableCommand { // MARK: - PluginsCommand -/// Runs the build tool plugins of a generated workspace, and nothing else. +/// Runs build tool plugins directly through bazelize. /// -/// What a plugin writes is decided by the plugin, so a change to its own source -/// changes the files a target compiles without anything else about the project -/// moving. This is the command that brings those files up to date — the -/// generated workspace exposes it as `bazel run //:plugins`, the way a Bazel -/// workspace exposes every other thing that writes back into it. +/// Generated workspaces use their Bazel-built host through +/// `bazel run //:plugins`; this command remains the direct form for callers +/// that supply already-built plugin and tool programs. struct PluginsCommand: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "plugins", @@ -48,8 +46,7 @@ struct PluginsCommand: AsyncParsableCommand { @Option(name: [.customLong("local", withSingleDash: false)], help: "PATH/TO/LOCAL/PACKAGE") var locals: [String] = [] - /// `NAME=PATH`, for the programs Bazel built: `//:plugins` has them as - /// `data`, so a plugin runs without SwiftPM building anything. + /// `NAME=PATH`, for plugin programs the caller already built. @Option(name: [.customLong("plugin", withSingleDash: false)], help: "NAME=PATH/TO/PLUGIN") var plugins: [String] = [] diff --git a/Sources/BazelizeKit/Plugin/Plugin+SwiftPM.swift b/Sources/BazelizeKit/Plugin/Plugin+SwiftPM.swift index e0712b2..181965f 100644 --- a/Sources/BazelizeKit/Plugin/Plugin+SwiftPM.swift +++ b/Sources/BazelizeKit/Plugin/Plugin+SwiftPM.swift @@ -192,19 +192,28 @@ final class PluginSwiftPM: PluginBuiltin { /// is the workspace's own way to run them, the way `bazel mod tidy` is the /// workspace's way to fix its module file. /// - /// The plugins and the tools they run are `data`, so running this builds - /// them: the script speaks to programs Bazel made, not to SwiftPM. The - /// script itself is written by the package generator, which is what knows - /// which programs those are. + /// The plugin host, plugins and tools are all Bazel-built `data` of the + /// runner. Nothing is looked up through `PATH`; the generated workspace is + /// sufficient to run its plugins. override func build(_ builder: CodeBuilder) { guard hasPackages else { return } + builder.load(loadableRule: Rules.Swift.swift_binary) builder.load(loadableRule: Rules.Shell.sh_binary) + builder.call( + Rules.Swift.Call.swift_binary( + name: "_plugin_host", + srcs: ["plugin-host.swift"], + tags: ["manual"])) builder.call( Rules.Shell.Call.sh_binary( name: "plugins", srcs: ["plugins.sh"], - data: ["//\(Self.packagesDirectory):plugins"])) + data: [ + ":_plugin_host", + "plugin-plan.json", + "//\(Self.packagesDirectory):plugins", + ])) } override var custom: [PluginBuiltin.Custom]? { diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift index 1810004..7359088 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift @@ -58,9 +58,9 @@ extension SwiftPM { notes.append(message) } - /// The plugins and tools Bazel already built, by target name. Empty - /// while a workspace is being generated — nothing has been built yet — - /// and filled by `//:plugins`, which has Bazel build them first. + /// Plugins and tools an API caller already built, by target name. + /// Workspace generation leaves these empty; the generated `//:plugins` + /// target has its own Bazel-built host and runfiles. let built: BuiltPrograms init( @@ -101,7 +101,7 @@ extension SwiftPM { try generate(package) } - try writePluginRunner(locals: locals) + try writePluginRunner() /// Written whether or not there is a trait to switch: the root /// `.bazelrc` imports it, and an import of a file that is not /// there is a workspace that does not load. @@ -172,7 +172,7 @@ extension SwiftPM { /// `//:plugins` can run it without SwiftPM having to load — let /// alone build — the package it lives in. if package.isRoot || package.isLocal { - let used = Set(package.manifest.targets.flatMap(\.pluginUsages).map(\.name)) + let used = usedPluginNames(in: package) for target in package.manifest.targets where target.type == "plugin" && used.contains(target.name) { diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginHost.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginHost.swift index c9fd585..f758349 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginHost.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginHost.swift @@ -18,9 +18,8 @@ extension SwiftPM { /// when a plugin writes a different set of files, because they glob the /// directory the plugin writes into. What comes back is what to tell the /// user about. - /// `plugins` and `tools` are what Bazel built, by target name: a plugin is - /// a program and so is the tool it runs, and building them is Bazel's job - /// wherever `//:plugins` is what started this. + /// `plugins` and `tools` are programs a direct API caller already built. + /// Generated workspaces use the separate host embedded in `//:plugins`. public static func runPlugins( output: Path, locals: [Path], @@ -170,8 +169,7 @@ extension SwiftPM.Generator { /// so compiling it needs no package graph — which is the whole reason the /// host can run one without building anything else. private func compile(plugin target: SwiftPM.PackageTarget, in package: SwiftPM.Package) async throws -> Path { - /// Bazel built it: `//:plugins` has the plugin as `data`, so it is in - /// the runfiles by the time the host runs. + /// A direct caller may supply a plugin it already built. if let prebuilt = built.plugins[target.name], prebuilt.exists { return prebuilt } let built = output + ".bazelize/plugins" + package.directory + target.name @@ -260,9 +258,8 @@ extension SwiftPM.Generator { guard target.type == "executable" else { return nil } - /// Bazel built it, so SwiftPM never has to load the package the tool - /// lives in — which some toolchains cannot do when a plugin names its - /// tool by target. + /// A direct caller may supply a tool it already built, avoiding a + /// SwiftPM build of the package that owns it. if let prebuilt = built.tools[name], prebuilt.exists { return prebuilt } let product = package.manifest.products.first { product in diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginRule.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginRule.swift index 00f3433..0c05d1d 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginRule.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginRule.swift @@ -50,50 +50,45 @@ extension SwiftPM.Generator { /// `bazel run //:plugins`, and everything it needs built first. /// - /// The plugins and the tools they run are `data` of the script, so running - /// it builds them: a script that called `bazel build` itself would be a - /// second Bazel inside the first one's lock. - func writePluginRunner(locals: [Path]) throws { + /// Bazel builds the host, plugins and tools. The generated plan contains + /// each SwiftPM plugin request, so running this target never needs an + /// installed `bazelize` executable. + func writePluginRunner() throws { /// The root `BUILD` declares `//:plugins` for any project with packages, /// because whether one of them has a plugin is not known when that file - /// is written. So both of the things it names are written for any such - /// project: a package with nothing to run is a command that does - /// nothing, and a label that does not resolve is a workspace that does - /// not load. + /// is written. So all of its inputs exist even when the plan is empty. guard (output + "Package.swift").exists else { return } - let binaries = pluginBinaries + let executions = pluginExecutions + let binaries = executions + .flatMap(binaries) + .reduce(into: [String: PluginBinary]()) { result, binary in + result[binary.label] = binary + } + .values + .sorted { $0.label < $1.label } try packagesRoot.mkpath() let group = CodeBuilder() group.call( Rules.Builtin.Call.filegroup( name: "plugins", - srcs: .build { binaries.map(\.label).sorted().map { Starlark.Label.named($0) } }, + srcs: .build { binaries.map { Starlark.Label.named($0.label) } }, visibility: .public)) /// The flags every trait is switched with, in the package the /// generator owns. buildTraitRules(group) try (packagesRoot + "BUILD").write(group.build()) - let arguments = ["--output", "."] - + locals.flatMap { local in ["--local", local.absolute().string.quoted] } - + binaries.flatMap { binary in - [binary.isPlugin ? "--plugin" : "--tool", "\(binary.name)=$runfiles/\(binary.path)"] - } - + try writePluginPlan(executions) + try (output + "plugin-host.swift").write(Self.pluginRunnerSource) let script = output + "plugins.sh" try script.write(""" #!/bin/bash - # Runs this workspace's build tool plugins, writing what they generate - # back into `Packages/*/Generated/*Plugin`. - # - # The plugins and their tools are built by Bazel: they are `data` of this - # script, so they are in its runfiles by the time it runs. + # Bazel supplies this host, its plan, every plugin and every tool. set -euo pipefail runfiles="${RUNFILES_DIR:-$0.runfiles}/_main" - cd "${BUILD_WORKSPACE_DIRECTORY:-$(dirname "$0")}" - exec bazelize plugins \(arguments.joined(separator: " ")) + exec "$runfiles/_plugin_host" "$runfiles/plugin-plan.json" "$runfiles" """) @@ -101,6 +96,34 @@ extension SwiftPM.Generator { try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: script.string) } + private func writePluginPlan(_ executions: [PluginExecution]) throws { + let plan = try executions.enumerated().map { index, execution in + let workDirectory = pluginWorkDirectory(of: execution.target, in: execution.package) + let request = try pluginRequest(for: execution, workDirectory: workDirectory) + let payload = try JSONEncoder().encode(request) + let firstForTarget = index == 0 + || executions[index - 1].package.directory != execution.package.directory + || executions[index - 1].target.name != execution.target.name + + return [ + "package": execution.package.directory, + "target": execution.target.name, + "plugin": execution.usage.name, + "executable": binary( + of: execution.plugin, + in: execution.pluginPackage).path, + "output": workDirectory.absolute().string, + "resetOutput": firstForTarget, + "request": payload.base64EncodedString() + ] as [String: Any] + } + var data = try JSONSerialization.data( + withJSONObject: plan, + options: [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]) + data.append(0x0A) + try data.write(to: (output + "plugin-plan.json").url) + } + /// Bazel-native commands that describe the generated workspace. /// /// The answers are embedded in executable targets, so using them never @@ -175,67 +198,141 @@ extension SwiftPM.Generator { ofItemAtPath: wrapper.string) } - /// What a plugin needs built: the plugin itself, and the tools it runs. - private var pluginBinaries: [PluginBinary] { - var binaries: [PluginBinary] = [] - - for package in workspace.packages where package.isRoot || package.isLocal { - let used = Set(package.manifest.targets.flatMap(\.pluginUsages).map(\.name)) - guard !used.isEmpty else { continue } + /// One plugin attached to one target. + private struct PluginExecution { + let package: SwiftPM.Package + let target: SwiftPM.PackageTarget + let usage: SwiftPM.PluginUsage + let plugin: SwiftPM.PackageTarget + let pluginPackage: SwiftPM.Package + } - for target in package.manifest.targets where used.contains(target.name) { - guard target.type == "plugin" else { continue } - binaries.append(binary(of: target, in: package, isPlugin: true)) + /// Plugins of this package that a project-owned target actually uses. + func usedPluginNames(in package: SwiftPM.Package) -> Set { + Set(pluginExecutions.lazy + .filter { $0.pluginPackage.directory == package.directory } + .map(\.plugin.name)) + } - for dependency in target.dependencies { - guard case .target(let name) = dependency.kind else { - guard case .byName(let name) = dependency.kind else { continue } - if let tool = tool(named: name, in: package) { binaries.append(tool) } - continue + private var pluginExecutions: [PluginExecution] { + workspace.packages + .filter { $0.isRoot || $0.isLocal } + .flatMap { package in + package.manifest.targets.flatMap { target in + target.pluginUsages.compactMap { usage in + guard let (plugin, pluginPackage) = resolvedPlugin(usage, from: package) else { + return nil + } + return .init( + package: package, + target: target, + usage: usage, + plugin: plugin, + pluginPackage: pluginPackage) } - if let tool = tool(named: name, in: package) { binaries.append(tool) } } } + } + + private func resolvedPlugin( + _ usage: SwiftPM.PluginUsage, + from package: SwiftPM.Package) -> (SwiftPM.PackageTarget, SwiftPM.Package)? + { + let packages: [SwiftPM.Package] + if let name = usage.package { + let directory = workspace.directoryByIdentity[name.lowercased()] + packages = workspace.packages.filter { $0.directory == directory } + } else { + packages = [package] + workspace.packages.filter { $0.directory != package.directory } + } + + for candidate in packages { + if let target = candidate.manifest.targets.first(where: { + $0.name == usage.name && $0.type == "plugin" + }) { + return (target, candidate) + } + } + return nil + } + + private func binaries(_ execution: PluginExecution) -> [PluginBinary] { + [binary(of: execution.plugin, in: execution.pluginPackage)] + + execution.plugin.dependencies.compactMap { dependency in + switch dependency.kind { + case .target(let name), .byName(let name): + return toolBinary(named: name, in: execution.pluginPackage) + case .product: + return nil + } + } + } + + private func pluginRequest( + for execution: PluginExecution, + workDirectory: Path) throws -> SwiftPM.PluginWire.Request + { + var builder = SwiftPM.PluginContextBuilder(package: execution.package, generator: self) + let targetId = try builder.add(package: execution.package, asking: execution.target) + let workDirectoryId = builder.add(path: workDirectory.absolute().string) + + var tools: [String: SwiftPM.PluginWire.Tool] = [:] + for dependency in execution.plugin.dependencies { + let name: String + switch dependency.kind { + case .target(let value), .byName(let value): + name = value + case .product: + continue + } + guard let binary = toolBinary(named: name, in: execution.pluginPackage) else { continue } + tools[name] = .init(path: builder.add(path: "$RUNFILES/\(binary.path)"), triples: nil) } - return binaries + return .init( + context: builder.context(workDirectoryId: workDirectoryId, tools: tools), + rootPackageId: 0, + targetId: targetId, + pluginGeneratedSources: [], + pluginGeneratedResources: []) } - private func tool(named name: String, in package: SwiftPM.Package) -> PluginBinary? { + private func toolBinary(named name: String, in package: SwiftPM.Package) -> PluginBinary? { + guard let target = package.manifest.targets.first(where: { $0.name == name }) else { + return nil + } + if target.type == "executable" { + return binary(of: target, in: package) + } guard - let target = package.manifest.targets.first(where: { - $0.name == name && $0.type == "executable" - }) + target.type == "binary", + let artifact = artifact(of: target, in: package), + case .artifactBundle = artifact.kind, + Self.executable(inArtifactBundle: artifact.path) != nil else { return nil } - - return binary(of: target, in: package, isPlugin: false) + return binary(of: target, in: package) } private func binary( of target: SwiftPM.PackageTarget, - in package: SwiftPM.Package, - isPlugin: Bool) -> PluginBinary + in package: SwiftPM.Package) -> PluginBinary { let rule = ruleName(of: target.name, in: package) let directory = "\(PluginSwiftPM.packagesDirectory)/\(package.directory)" return .init( - name: target.name, label: "//\(directory):\(rule)", - path: "\(directory)/\(rule)", - isPlugin: isPlugin) + path: "\(directory)/\(rule)") } } extension SwiftPM.Generator { /// A program `//:plugins` has Bazel build before it runs. struct PluginBinary { - let name: String let label: String /// Where it sits in the runner's runfiles. let path: String - let isPlugin: Bool } } diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginRunnerSource.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginRunnerSource.swift new file mode 100644 index 0000000..35d2f3b --- /dev/null +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginRunnerSource.swift @@ -0,0 +1,341 @@ +// +// SwiftPM+PluginRunnerSource.swift +// +// +// The self-contained host built by a generated workspace. +// + +import Foundation + +extension SwiftPM.Generator { + /// A small SwiftPM build-tool-plugin host compiled by Bazel in the generated + /// workspace. The invocation plan is generated separately; this program only + /// speaks the plugin wire protocol and runs the commands returned over it. + static let pluginRunnerSource = #""" + import Foundation + + private struct Invocation: Decodable { + let package: String + let target: String + let plugin: String + let executable: String + let output: String + let resetOutput: Bool + let request: String + } + + private struct ProcessResult { + let status: Int32 + let output: Data + let error: String + } + + private struct RunnerError: Error, CustomStringConvertible { + let description: String + } + + @main + private enum PluginHost { + static func main() { + do { + try run() + } catch { + FileHandle.standardError.write(Data("plugin host: \(error)\n".utf8)) + exit(1) + } + } + + private static func run() throws { + guard CommandLine.arguments.count == 3 else { + throw RunnerError(description: "expected PLAN RUNFILES") + } + + let plan = URL(fileURLWithPath: CommandLine.arguments[1]) + let runfiles = CommandLine.arguments[2] + let invocations = try JSONDecoder().decode([Invocation].self, from: Data(contentsOf: plan)) + + for invocation in invocations { + do { + try run(invocation, runfiles: runfiles) + } catch { + let subject = "\(invocation.package)/\(invocation.target)" + let reason = "did not run the \(invocation.plugin) plugin: \(error)." + print("\(subject) \(reason) Whatever that plugin generates is missing from the target.") + } + } + } + + private static func run(_ invocation: Invocation, runfiles: String) throws { + let files = FileManager.default + if invocation.resetOutput, files.fileExists(atPath: invocation.output) { + try files.removeItem(atPath: invocation.output) + } + try files.createDirectory(atPath: invocation.output, withIntermediateDirectories: true) + + guard let encoded = Data(base64Encoded: invocation.request) else { + throw RunnerError(description: "the generated request is not base64") + } + let decoded = try JSONSerialization.jsonObject(with: encoded) + let replaced = replacingRunfiles(in: decoded, with: runfiles) + let refreshed = try refreshingSources(in: replaced) + let payload = try JSONSerialization.data(withJSONObject: refreshed) + + var length = UInt64(payload.count).littleEndian + var input = withUnsafeBytes(of: &length) { Data($0) } + input.append(payload) + + let executable = URL(fileURLWithPath: runfiles).appendingPathComponent(invocation.executable).path + let result = try process(executable: executable, input: input) + let responses = try messages(in: result.output) + let commands = try handle(responses) + + if result.status != 0, commands.isEmpty { + throw RunnerError(description: errors(result.error)) + } + + for command in commands { + try execute(command) + } + } + + private static func replacingRunfiles(in value: Any, with runfiles: String) -> Any { + if let string = value as? String { + return string.replacingOccurrences(of: "$RUNFILES", with: runfiles) + } + if let array = value as? [Any] { + return array.map { replacingRunfiles(in: $0, with: runfiles) } + } + if let dictionary = value as? [String: Any] { + return dictionary.mapValues { replacingRunfiles(in: $0, with: runfiles) } + } + return value + } + + /// Source additions do not require regenerating the workspace. Refresh the + /// one target whose files a build-tool plugin is allowed to inspect. + private static func refreshingSources(in value: Any) throws -> Any { + guard + var request = value as? [String: Any], + var body = request["createBuildToolCommands"] as? [String: Any], + var context = body["context"] as? [String: Any], + var targets = context["targets"] as? [[String: Any]], + let targetId = body["targetId"] as? Int, + targets.indices.contains(targetId), + var info = targets[targetId]["info"] as? [String: Any], + let directoryId = targets[targetId]["directoryId"] as? Int, + let paths = context["paths"] as? [[String: Any]], + let directory = path(directoryId, in: paths) + else { + return value + } + + let key: String + if info["swiftSourceModuleInfo"] != nil { + key = "swiftSourceModuleInfo" + } else if info["clangSourceModuleInfo"] != nil { + key = "clangSourceModuleInfo" + } else { + return value + } + + guard var module = info[key] as? [String: Any] else { return value } + let root = URL(fileURLWithPath: directory, isDirectory: true) + let prefix = root.path.hasSuffix("/") ? root.path : root.path + "/" + module["sourceFiles"] = try walk(root).map { file -> [String: Any] in + let path = file.path + let name = path.hasPrefix(prefix) ? String(path.dropFirst(prefix.count)) : file.lastPathComponent + return ["basePathId": directoryId, "name": name, "type": fileType(file)] + } + info[key] = module + targets[targetId]["info"] = info + context["targets"] = targets + body["context"] = context + request["createBuildToolCommands"] = body + return request + } + + private static func path(_ id: Int, in paths: [[String: Any]]) -> String? { + guard paths.indices.contains(id), let subpath = paths[id]["subpath"] as? String else { + return nil + } + guard let base = paths[id]["baseURLId"] as? Int, let root = path(base, in: paths) else { + return subpath + } + return URL(fileURLWithPath: root, isDirectory: true).appendingPathComponent(subpath).path + } + + private static func walk(_ root: URL) throws -> [URL] { + var files: [URL] = [] + var visited: Set = [] + + func visit(_ directory: URL) throws { + let resolved = directory.resolvingSymlinksInPath().standardizedFileURL.path + guard visited.insert(resolved).inserted else { return } + + let children = try FileManager.default.contentsOfDirectory( + at: directory, + includingPropertiesForKeys: [.isDirectoryKey], + options: []) + .sorted { $0.path < $1.path } + for child in children { + if try child.resourceValues(forKeys: [.isDirectoryKey]).isDirectory == true { + try visit(child) + } else { + files.append(child) + } + } + } + + try visit(root) + return files + } + + private static func fileType(_ file: URL) -> String { + let headers: Set = ["h", "hh", "hpp", "hxx", "inc"] + let sources: Set = ["swift", "c", "cc", "cpp", "cxx", "m", "mm", "S", "s"] + if headers.contains(file.pathExtension) { return "header" } + if sources.contains(file.pathExtension) { return "source" } + return "resource" + } + + private static func messages(in data: Data) throws -> [[String: Any]] { + var messages: [[String: Any]] = [] + var offset = 0 + + while offset + 8 <= data.count { + var count: UInt64 = 0 + for index in 0 ..< 8 { + count |= UInt64(data[offset + index]) << UInt64(index * 8) + } + let start = offset + 8 + guard count > 0, count <= UInt64(Int.max), start + Int(count) <= data.count else { + throw RunnerError(description: "a plugin response claims \(count) bytes and the stream has fewer") + } + + let payload = data[start ..< start + Int(count)] + guard let message = try JSONSerialization.jsonObject(with: payload) as? [String: Any] else { + throw RunnerError(description: "a plugin response is not a JSON object") + } + messages.append(message) + offset = start + Int(count) + } + + return messages + } + + private static func handle(_ messages: [[String: Any]]) throws -> [[String: Any]] { + var commands: [[String: Any]] = [] + + for message in messages { + if let diagnostic = message["emitDiagnostic"] as? [String: Any] { + let severity = diagnostic["severity"] as? String ?? "warning" + let text = diagnostic["message"] as? String ?? "" + FileHandle.standardError.write(Data("plugin \(severity): \(text)\n".utf8)) + } else if let build = message["defineBuildCommand"] as? [String: Any], + let configuration = build["configuration"] as? [String: Any] + { + commands.append(configuration) + } else if let prebuild = message["definePrebuildCommand"] as? [String: Any], + let configuration = prebuild["configuration"] as? [String: Any] + { + if let directory = prebuild["outputFilesDirectory"] as? String { + try FileManager.default.createDirectory( + atPath: filePath(directory), + withIntermediateDirectories: true) + } + commands.append(configuration) + } + } + + return commands + } + + private static func execute(_ command: [String: Any]) throws { + guard let executable = command["executable"] as? String else { + throw RunnerError(description: "a plugin command has no executable") + } + + let arguments = (command["arguments"] as? [String] ?? []).map(filePath) + var environment = ProcessInfo.processInfo.environment + for (key, value) in command["environment"] as? [String: String] ?? [:] { + environment[key] = value + } + let workingDirectory = (command["workingDirectory"] as? String).map(filePath) + let result = try process( + executable: filePath(executable), + arguments: arguments, + environment: environment, + workingDirectory: workingDirectory) + + guard result.status == 0 else { + let name = command["displayName"] as? String + ?? URL(fileURLWithPath: filePath(executable)).lastPathComponent + throw RunnerError(description: "\(name): \(errors(result.error))") + } + } + + private static func process( + executable: String, + arguments: [String] = [], + environment: [String: String]? = nil, + workingDirectory: String? = nil, + input: Data? = nil) throws -> ProcessResult + { + let directory = FileManager.default.temporaryDirectory + .appendingPathComponent("bazelize-plugin-host-\(UUID().uuidString)", isDirectory: true) + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: directory) } + + let inputURL = directory.appendingPathComponent("stdin") + let outputURL = directory.appendingPathComponent("stdout") + let errorURL = directory.appendingPathComponent("stderr") + try (input ?? Data()).write(to: inputURL) + FileManager.default.createFile(atPath: outputURL.path, contents: nil) + FileManager.default.createFile(atPath: errorURL.path, contents: nil) + + let inputHandle = try FileHandle(forReadingFrom: inputURL) + let outputHandle = try FileHandle(forWritingTo: outputURL) + let errorHandle = try FileHandle(forWritingTo: errorURL) + defer { + try? inputHandle.close() + try? outputHandle.close() + try? errorHandle.close() + } + + let process = Process() + process.executableURL = URL(fileURLWithPath: executable) + process.arguments = arguments + process.environment = environment + if let workingDirectory { + process.currentDirectoryURL = URL(fileURLWithPath: workingDirectory, isDirectory: true) + } + process.standardInput = inputHandle + process.standardOutput = outputHandle + process.standardError = errorHandle + + try process.run() + process.waitUntilExit() + try outputHandle.close() + try errorHandle.close() + + let output = try Data(contentsOf: outputURL) + let error = String(data: try Data(contentsOf: errorURL), encoding: .utf8) ?? "" + return .init(status: process.terminationStatus, output: output, error: error) + } + + private static func filePath(_ value: String) -> String { + guard value.hasPrefix("file://") else { return value } + return URL(string: value)?.path ?? value + } + + private static func errors(_ output: String) -> String { + let lines = output.split(separator: "\n").map { + $0.trimmingCharacters(in: .whitespaces) + } + let reasons = lines.filter { $0.lowercased().hasPrefix("error:") }.suffix(3) + if !reasons.isEmpty { return reasons.joined(separator: " ") } + return String(output.suffix(400)).trimmingCharacters(in: .whitespacesAndNewlines) + } + } + """# +} diff --git a/docs/SPM.md b/docs/SPM.md index 0d81d3c..1fbcdb6 100644 --- a/docs/SPM.md +++ b/docs/SPM.md @@ -72,7 +72,9 @@ App/ ├── Package.resolved # kept: the only source of pins ├── config.bazelrc ├── BUILD -├── plugins.sh # what `bazel run //:plugins` runs +├── plugins.sh # enters the Bazel-built SwiftPM plugin host +├── plugin-host.swift # compiled by Bazel for `//:plugins` +├── plugin-plan.json # plugin requests and runfile paths ├── tools/ # Bazel-native workspace inspection commands ├── Prebuilt/ ├── Targets// # unchanged @@ -94,11 +96,12 @@ App/ | `bazel run //tools:list-config` | the `--config=` this workspace defines, and the flags every build gets anyway | | `bazel run //tools:list-trait` | the traits its packages declare, which are on, and the `--config` that switches each | -Both listing commands are generated `sh_binary` targets. Their answers are -embedded from the same resolved workspace and configuration files that generate -the package rules; running them requires Bazel, but no `bazelize` executable. -The generated `tools/bazel` wrapper keeps `bazel list config|trait` as shorter -aliases and forwards every other command unchanged. +The plugin and listing commands are generated targets. `//:plugins` builds its +host, plugins and tools with Bazel, then runs entirely from their runfiles; it +does not look up `bazelize` on `PATH`. The listing answers are embedded from the +same resolved workspace and configuration files that generate the package +rules. The generated `tools/bazel` wrapper keeps `bazel list config|trait` as +shorter aliases and forwards every other command unchanged. ### How a package's sources get in @@ -209,7 +212,7 @@ No test pins how a package's rules are produced either. | `cLanguageStandard` / `cxxLanguageStandard` | `-std=`, for the language the target is written in; a target that compiles both is named instead, because one rule takes one `-std` | | `strictMemorySafety` | `-strict-memory-safety` | | `unsafeFlags` | `copts` | -| build tool plugin, own package | built by Bazel, run by bazelize (`bazel run //:plugins`); what it writes is globbed into the target that asked for it | +| build tool plugin, own package | host, plugin and tools all built and run by Bazel (`bazel run //:plugins`); what it writes is globbed into the target that asked for it | | build tool plugin, dependency | not run; the plugin is named at the end of the run | | command plugin | nothing: it runs when someone asks for it by name, never during a build | | macro target | `swift_compiler_plugin`, and `plugins` on whatever declares the macro | diff --git a/docs/SPM_ZH.md b/docs/SPM_ZH.md index 346c9b3..7e37351 100644 --- a/docs/SPM_ZH.md +++ b/docs/SPM_ZH.md @@ -65,7 +65,9 @@ App/ ├── Package.resolved # 保留:pin 的唯一來源 ├── config.bazelrc ├── BUILD -├── plugins.sh # `bazel run //:plugins` 跑的就是它 +├── plugins.sh # 進入 Bazel 建出的 SwiftPM plugin host +├── plugin-host.swift # 由 Bazel 編成 `//:plugins` 的 host +├── plugin-plan.json # plugin request 與 runfile 路徑 ├── tools/ # Bazel 原生的 workspace 查詢指令 ├── Prebuilt/ ├── Targets// # 完全不變 @@ -87,9 +89,10 @@ App/ | `bazel run //tools:list-config` | 這個 workspace 定義了哪些 `--config=`,以及每次 build 一定會拿到的 flag | | `bazel run //tools:list-trait` | 它的 package 宣告了哪些 trait、哪些是開的,以及切換各自要用哪個 `--config` | -兩個清單指令都是產生出來的 `sh_binary` target。答案來自產生 package rules -時使用的同一份 resolved workspace 與設定檔;執行時只需要 Bazel,不需要 -`bazelize`。產生的 `tools/bazel` wrapper 仍保留較短的 +Plugin 與清單指令都是產生出來的 target。`//:plugins` 用 Bazel 建 host、 +plugin 與工具,接著完全從它們的 runfiles 執行,不會再去 `PATH` 找 +`bazelize`。清單答案來自產生 package rules 時使用的同一份 resolved +workspace 與設定檔。產生的 `tools/bazel` wrapper 仍保留較短的 `bazel list config|trait` alias,其他指令則原封不動往下傳。 ### package 的原始碼怎麼進來 @@ -190,7 +193,7 @@ target 的 `deps` 需要改。測試也不釘 package 的規則是怎麼產生 | `cLanguageStandard` / `cxxLanguageStandard` | `-std=`,看 target 實際寫的是哪種語言;同時編 C 與 C++ 的 target 兩個都不給,並具名回報——一條規則只有一個 `-std` | | `strictMemorySafety` | `-strict-memory-safety` | | `unsafeFlags` | `copts` | -| build tool plugin(自己的 package) | Bazel 建、bazelize 跑(`bazel run //:plugins`);它寫出來的東西由規則 glob 進「要求它的那個 target」 | +| build tool plugin(自己的 package) | host、plugin 與工具都由 Bazel 建置並執行(`bazel run //:plugins`);它寫出來的東西由規則 glob 進「要求它的那個 target」 | | build tool plugin(依賴的 package) | 不執行;結束時把該 plugin 的名字講出來 | | command plugin | 不處理:它是有人指名才跑,build 永遠用不到 | | macro target | `swift_compiler_plugin`,並在宣告該 macro 的 target 上加 `plugins` | @@ -424,7 +427,7 @@ package graph,用 SwiftPM 自己的 `HostToPluginMessage` 格式,它內部 | 0.5 ✅ | `//Packages` facade(alias 指向 rspm) | 所有 app,label 形狀定案 | | 1 ✅ | 純 Swift library target、`swiftLanguageMode`/`define`/upcoming・experimental feature/`strictMemorySafety`/`defaultIsolation`/`interoperabilityMode`/`unsafeFlags`;不支援的種類連同它的下游一起略過並警告;由一個 flag 切換,預設仍 rspm | 58 個 package 能單獨建起來 | | 2 ✅ | clang target(`headerSearchPath`/`publicHeadersPath`/明列 `sources`/`exclude`/module map)、resources + `Bundle.module` accessor、binary target(遠端 xcframework 與本地 archive)、system library | 7 個綠燈 app 建得起來也跑得起來;另外五個的 package 全部建得起來 | -| 3 ✅ | macro target;逐 target 的平台版本(不需要做——SwiftPM 自己就會拒絕這種圖,所以回報就是答案);build tool plugin,Bazel 建、bazelize 跑 | `spm/BuildToolPlugin` 的測試靠 plugin 產生的原始碼通過 | +| 3 ✅ | macro target;逐 target 的平台版本(不需要做——SwiftPM 自己就會拒絕這種圖,所以回報就是答案);build tool plugin 與 host 都由 Bazel 建置執行 | `spm/BuildToolPlugin` 的測試靠 plugin 產生的原始碼通過 | | 4 ✅ | rspm 依賴、`Patches/`、版本守門與模式 flag 全部移除 | 7 個綠燈 app 建得起來也跑得起來 | 階段 4 是把另一條路整個移除,而不是留一個 flag:兩條路就是兩張依賴圖,而語料裡 diff --git a/spm/README.md b/spm/README.md index 78cabe8..9717bca 100644 --- a/spm/README.md +++ b/spm/README.md @@ -85,8 +85,9 @@ The flags underneath are there for a build that wants some other combination. `bazel run //:plugins` runs this workspace's build tool plugins and writes what they generate into `Packages//Generated/`. It is a separate step -because a plugin is a program: Bazel builds it, and bazelize runs it as SwiftPM -would. +because a plugin is a program. Bazel builds the generated host, the plugins and +their tools, then runs all of them from runfiles; no `bazelize` executable is +needed at runtime. `bazel build --config=lang.` bundles that localization and `Base`, and nothing else; a build that names none bundles every one of them, which is what @@ -94,7 +95,7 @@ SwiftPM does. Several at once is the flag underneath, `--@build_bazel_rules_apple//apple/build_settings:locales_to_include=en,ja`. The listing commands are generated Bazel targets and do not need `bazelize` at -runtime. The generated `tools/bazel` wrapper also exposes them as +runtime either. The generated `tools/bazel` wrapper also exposes them as `bazel list config|trait|language`. `App/` is generated, and is not checked in. From 666a25ea9d967b50da3cabc4d1d44decee56baee Mon Sep 17 00:00:00 2001 From: yume190 Date: Wed, 23 Sep 2026 15:02:19 +0800 Subject: [PATCH 44/47] refactor(cli): one input option, and only the commands a user runs The CLI carried commands that mirrored internal steps: a plugins lane, an xcode dump that generate repeated, and a clear flag that deleted the output a run was about to rewrite. What is left is generate and dump, both reading --input, so a project path is named the same way whichever one is run. --- README.md | 6 +- Sources/Bazelize/Command.swift | 116 +++++------------- Sources/BazelizeKit/Bazel/Bazel+File.swift | 4 - Sources/BazelizeKit/Kit.swift | 61 +-------- .../SwiftPM/SwiftPM+Generator.swift | 23 +--- .../SwiftPM/SwiftPM+PluginHost.swift | 33 ----- .../XcodeTests/RoadmapTreeBuilderTests.swift | 8 +- fixture/iOS/Makefile | 3 +- spm/README.md | 4 +- 9 files changed, 43 insertions(+), 215 deletions(-) diff --git a/README.md b/README.md index 719ebb1..e8d1158 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Bazelize -A cli tool turn your xcode project or Swift package to bazel. +Bazelize generates Bazel workspaces from Xcode projects and Swift packages. --- @@ -13,13 +13,13 @@ mint install XCodeBazelize/Bazelize ## Usage ```sh -bazelize --project YOUR.xcodeproj +bazelize --input YOUR.xcodeproj --output App ``` Or a Swift package — the `Package.swift`, or the directory holding one: ```sh -bazelize --project path/to/Package.swift +bazelize --input path/to/Package.swift --output App ``` --- diff --git a/Sources/Bazelize/Command.swift b/Sources/Bazelize/Command.swift index 4dd250f..6541520 100644 --- a/Sources/Bazelize/Command.swift +++ b/Sources/Bazelize/Command.swift @@ -17,131 +17,75 @@ import Xcode struct Command: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "bazelize", - abstract: "A cli tool turn your xcode project to bazel.", + abstract: "Generate Bazel workspaces from Xcode and Swift package inputs.", + discussion: "Run without a subcommand to generate a workspace.", version: version, subcommands: [ GenerateCommand.self, - PluginsCommand.self, - XcodeCommand.self, -// RoadmapCommand.self, + DumpCommand.self ], defaultSubcommand: GenerateCommand.self) } -// MARK: - PluginsCommand - -/// Runs build tool plugins directly through bazelize. -/// -/// Generated workspaces use their Bazel-built host through -/// `bazel run //:plugins`; this command remains the direct form for callers -/// that supply already-built plugin and tool programs. -struct PluginsCommand: AsyncParsableCommand { - static let configuration = CommandConfiguration( - commandName: "plugins", - abstract: "Run the build tool plugins of a generated workspace.") - - @Option(name: [.customLong("output", withSingleDash: false)], help: "PATH/TO/OUTPUT") - var output = "." - - @Option(name: [.customLong("local", withSingleDash: false)], help: "PATH/TO/LOCAL/PACKAGE") - var locals: [String] = [] - - /// `NAME=PATH`, for plugin programs the caller already built. - @Option(name: [.customLong("plugin", withSingleDash: false)], help: "NAME=PATH/TO/PLUGIN") - var plugins: [String] = [] - - @Option(name: [.customLong("tool", withSingleDash: false)], help: "NAME=PATH/TO/TOOL") - var tools: [String] = [] - - func run() async throws { - let outputPath = Path.current + output - let notes = try await SwiftPM.runPlugins( - output: outputPath, - locals: locals.map { Path.current + $0 }, - plugins: Self.programs(plugins), - tools: Self.programs(tools)) - - for note in notes { - print(note) - } - } - - private static func programs(_ arguments: [String]) -> [String: Path] { - arguments.reduce(into: [:]) { programs, argument in - guard let separator = argument.firstIndex(of: "=") else { return } - let name = String(argument[.. Path { let custom = Path(path) return custom.isAbsolute ? custom : outputRoot + custom } } + diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift index 7359088..13e14d1 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+Generator.swift @@ -58,31 +58,10 @@ extension SwiftPM { notes.append(message) } - /// Plugins and tools an API caller already built, by target name. - /// Workspace generation leaves these empty; the generated `//:plugins` - /// target has its own Bazel-built host and runfiles. - let built: BuiltPrograms - - init( - output: Path, - workspace: Workspace, - deployment: Deployment, - built: BuiltPrograms = .init()) - { + init(output: Path, workspace: Workspace, deployment: Deployment) { self.output = output self.workspace = workspace self.deployment = deployment - self.built = built - } - - struct BuiltPrograms { - let plugins: [String: Path] - let tools: [String: Path] - - init(plugins: [String: Path] = [:], tools: [String: Path] = [:]) { - self.plugins = plugins - self.tools = tools - } } func generate(locals: [Path] = []) async throws { diff --git a/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginHost.swift b/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginHost.swift index f758349..aa8acfc 100644 --- a/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginHost.swift +++ b/Sources/BazelizeKit/SwiftPM/SwiftPM+PluginHost.swift @@ -11,32 +11,6 @@ import Subprocess import System import Util -extension SwiftPM { - /// Runs the build tool plugins of an already generated workspace. - /// - /// Nothing else is generated: the rules are already there and do not change - /// when a plugin writes a different set of files, because they glob the - /// directory the plugin writes into. What comes back is what to tell the - /// user about. - /// `plugins` and `tools` are programs a direct API caller already built. - /// Generated workspaces use the separate host embedded in `//:plugins`. - public static func runPlugins( - output: Path, - locals: [Path], - plugins: [String: Path] = [:], - tools: [String: Path] = [:]) async throws -> [String] - { - let workspace = try await loadWorkspace(output: output, root: nil, locals: locals) - let generator = Generator( - output: output, - workspace: workspace, - deployment: .init(project: [:]), - built: .init(plugins: plugins, tools: tools)) - - return await generator.runPlugins().notes - } -} - extension SwiftPM.Generator { /// Runs the build tool plugins of the packages this project owns, into the /// directory their output belongs in. @@ -169,9 +143,6 @@ extension SwiftPM.Generator { /// so compiling it needs no package graph — which is the whole reason the /// host can run one without building anything else. private func compile(plugin target: SwiftPM.PackageTarget, in package: SwiftPM.Package) async throws -> Path { - /// A direct caller may supply a plugin it already built. - if let prebuilt = built.plugins[target.name], prebuilt.exists { return prebuilt } - let built = output + ".bazelize/plugins" + package.directory + target.name if built.exists { return built } @@ -258,10 +229,6 @@ extension SwiftPM.Generator { guard target.type == "executable" else { return nil } - /// A direct caller may supply a tool it already built, avoiding a - /// SwiftPM build of the package that owns it. - if let prebuilt = built.tools[name], prebuilt.exists { return prebuilt } - let product = package.manifest.products.first { product in product.targets.contains(name) }?.name ?? name diff --git a/Tests/XcodeTests/RoadmapTreeBuilderTests.swift b/Tests/XcodeTests/RoadmapTreeBuilderTests.swift index 1891624..2c1598c 100644 --- a/Tests/XcodeTests/RoadmapTreeBuilderTests.swift +++ b/Tests/XcodeTests/RoadmapTreeBuilderTests.swift @@ -17,7 +17,7 @@ struct RoadmapTreeBuilderTests { defer { try? output.delete() } let kit = try await Kit(projectPath, nil, outputPath: output) - try await kit.run(projectPath) + try await kit.run() #expect((output + "BUILD").exists) #expect((output + "MODULE.bazel").exists) @@ -126,7 +126,7 @@ struct RoadmapTreeBuilderTests { defer { try? output.delete() } let kit = try await Kit(projectPath, nil, outputPath: output) - try await kit.run(projectPath) + try await kit.run() let appBuild = try String(contentsOfFile: (output + "Targets/IceCubesApp/BUILD").string) #expect(appBuild.contains("ios_application(")) @@ -153,7 +153,7 @@ struct RoadmapTreeBuilderTests { defer { try? output.delete() } let kit = try await Kit(projectPath, "Release", outputPath: output) - try await kit.run(projectPath) + try await kit.run() let cliBuild = try String(contentsOfFile: (output + "Targets/iina-cli/BUILD").string) #expect(cliBuild.contains("module_name = \"iina_cli\"")) @@ -194,7 +194,7 @@ struct RoadmapTreeBuilderTests { defer { try? nightlyOutput.delete() } let nightlyKit = try await Kit(projectPath, "Nightly", outputPath: nightlyOutput) - try await nightlyKit.run(projectPath) + try await nightlyKit.run() let nightlyBuild = try String(contentsOfFile: (nightlyOutput + "Targets/iina/BUILD").string) #expect(nightlyBuild.contains("Sources/iina/Assets.xcassets/AppIconNightly.appiconset/**")) diff --git a/fixture/iOS/Makefile b/fixture/iOS/Makefile index 06bd98a..9f8bc46 100644 --- a/fixture/iOS/Makefile +++ b/fixture/iOS/Makefile @@ -14,7 +14,7 @@ TESTS = \ .PHONY: bazelize bazelize: - @$(BAZELIZE) --project Example.xcodeproj --output $(OUTPUT) + @$(BAZELIZE) --input Example.xcodeproj --output $(OUTPUT) .PHONY: build build: bazelize @@ -38,5 +38,4 @@ uitest: .PHONY: clear clear: - @$(BAZELIZE) --project Example.xcodeproj --output $(OUTPUT) --clear -rm -rf $(OUTPUT) diff --git a/spm/README.md b/spm/README.md index 9717bca..c57f908 100644 --- a/spm/README.md +++ b/spm/README.md @@ -53,7 +53,7 @@ linked, so the package holding it still builds anywhere. ```sh cd spm/ -bazelize --project . --output App +bazelize --input . --output App cd App bazel run //:plugins # only the packages with a build tool plugin need this bazel test //... @@ -72,7 +72,7 @@ SwiftPM and bazelize need to be told where it is: cd spm/SystemLibrary export PKG_CONFIG_PATH="$PWD/vendor/pkgconfig" swift test -bazelize --project . --output App +bazelize --input . --output App ``` The flags are read when the workspace is generated, so only that command needs From fa4bedd300976ff99b41dcc58761c9c3a08baeb1 Mon Sep 17 00:00:00 2001 From: yume190 Date: Wed, 23 Sep 2026 15:02:26 +0800 Subject: [PATCH 45/47] build(fixture): describe the iOS project, stop tracking the generated one A checked-in pbxproj is a merge conflict waiting for the next target, and it says nothing about what the fixture is meant to cover. Project.swift says it in ~140 lines, Tuist writes the project a run needs, and mise pins the Tuist every lane resolves so a generated project is the same one everywhere. --- .github/workflows/swift.yml | 20 +- fixture/iOS/.gitignore | 4 + fixture/iOS/Example.xcodeproj/project.pbxproj | 1773 ----------------- .../xcshareddata/xcschemes/Example.xcscheme | 136 -- fixture/iOS/Makefile | 7 +- fixture/iOS/Project.swift | 139 ++ fixture/iOS/Tuist.swift | 5 + mise.toml | 2 + 8 files changed, 173 insertions(+), 1913 deletions(-) delete mode 100644 fixture/iOS/Example.xcodeproj/project.pbxproj delete mode 100644 fixture/iOS/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme create mode 100644 fixture/iOS/Project.swift create mode 100644 fixture/iOS/Tuist.swift create mode 100644 mise.toml diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index ade0804..d48f14c 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -10,6 +10,8 @@ on: - 'spm/**' - '.github/workflows/**' - 'Package.swift' + - 'fixture/iOS/**' + - 'mise.toml' pull_request: paths: - 'Sources/**' @@ -17,6 +19,8 @@ on: - 'spm/**' - '.github/workflows/**' - 'Package.swift' + - 'fixture/iOS/**' + - 'mise.toml' concurrency: group: ci @@ -29,6 +33,11 @@ jobs: steps: - uses: actions/checkout@v6 + - uses: jdx/mise-action@v3 + + - name: Generate iOS fixture + run: tuist generate --path fixture/iOS --no-open + - name: Check Xcode Version run: | xcodebuild -version @@ -81,6 +90,11 @@ jobs: steps: - uses: actions/checkout@v6 + - uses: jdx/mise-action@v3 + + - name: Generate Xcode project + run: tuist generate --path fixture/iOS --no-open + # Every `bazel` in this job reads these: the disk cache holds what the # build produced, the repository cache what it downloaded. Without the # second one a runner fetches rules_apple and the rest again every time. @@ -116,7 +130,7 @@ jobs: - name: Bazel Generation working-directory: fixture/iOS run: | - ../../bazelize --project Example.xcodeproj --output App | tee bazelize.log + ../../bazelize --input Example.xcodeproj --output App | tee bazelize.log ! grep -q "did not run the" bazelize.log - name: Build Application @@ -302,7 +316,7 @@ jobs: - name: Bazel Generation run: | - ./bazelize --project "${{ matrix.name }}/${{ matrix.project }}" --output "${{ matrix.name }}/App" | tee bazelize.log + ./bazelize --input "${{ matrix.name }}/${{ matrix.project }}" --output "${{ matrix.name }}/App" | tee bazelize.log ! grep -q "did not run the" bazelize.log - name: Build Application @@ -435,7 +449,7 @@ jobs: NOTES: ${{ matrix.notes }} run: | chmod +x bazelize - ./bazelize --project "spm/${{ matrix.name }}" --output "spm/${{ matrix.name }}/App" | tee bazelize.log + ./bazelize --input "spm/${{ matrix.name }}" --output "spm/${{ matrix.name }}/App" | tee bazelize.log if [ -n "$NOTES" ]; then if ! grep -qE "$NOTES" bazelize.log; then diff --git a/fixture/iOS/.gitignore b/fixture/iOS/.gitignore index f8f234f..5feec89 100644 --- a/fixture/iOS/.gitignore +++ b/fixture/iOS/.gitignore @@ -183,3 +183,7 @@ cache/ bazel-* +# Generated by Tuist from Project.swift. +/Derived/ +/*.xcodeproj/ +/*.xcworkspace/ diff --git a/fixture/iOS/Example.xcodeproj/project.pbxproj b/fixture/iOS/Example.xcodeproj/project.pbxproj deleted file mode 100644 index 7eb3151..0000000 --- a/fixture/iOS/Example.xcodeproj/project.pbxproj +++ /dev/null @@ -1,1773 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 56; - objects = { - -/* Begin PBXBuildFile section */ - A8120D07297670E5004F3FD3 /* LocalLib1 in Frameworks */ = {isa = PBXBuildFile; productRef = A8120D06297670E5004F3FD3 /* LocalLib1 */; }; - A8120D0929767375004F3FD3 /* LocalLib2 in Frameworks */ = {isa = PBXBuildFile; productRef = A8120D0829767375004F3FD3 /* LocalLib2 */; }; - A83BC77C298BBCDA00499A6C /* Framework3.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A83BC774298BBCD900499A6C /* Framework3.framework */; }; - A83BC783298BBCDA00499A6C /* Framework3Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = A83BC782298BBCDA00499A6C /* Framework3Tests.m */; }; - A83BC784298BBCDA00499A6C /* Framework3.h in Headers */ = {isa = PBXBuildFile; fileRef = A83BC776298BBCD900499A6C /* Framework3.h */; settings = {ATTRIBUTES = (Public, ); }; }; - A83BC787298BBCDA00499A6C /* Framework3.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A83BC774298BBCD900499A6C /* Framework3.framework */; }; - A83BC788298BBCDA00499A6C /* Framework3.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A83BC774298BBCD900499A6C /* Framework3.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - A83BC797298BBCED00499A6C /* Static2.m in Sources */ = {isa = PBXBuildFile; fileRef = A83BC796298BBCED00499A6C /* Static2.m */; }; - A83BC798298BBCED00499A6C /* Static2.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = A83BC795298BBCED00499A6C /* Static2.h */; }; - A83BC7A3298BBD7300499A6C /* Framework.h in Headers */ = {isa = PBXBuildFile; fileRef = A83BC7A1298BBD7300499A6C /* Framework.h */; settings = {ATTRIBUTES = (Public, ); }; }; - A83BC7A4298BBD7300499A6C /* Framework.m in Sources */ = {isa = PBXBuildFile; fileRef = A83BC7A2298BBD7300499A6C /* Framework.m */; }; - A849A669296D658B006BB98A /* Framework2.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A8658AFC296D009500AEFC87 /* Framework2.framework */; }; - A849A66A296D658B006BB98A /* Framework2.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A8658AFC296D009500AEFC87 /* Framework2.framework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - A8658AE3296D008800AEFC87 /* Framework1.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A8658ADB296D008800AEFC87 /* Framework1.framework */; }; - A8658AEA296D008900AEFC87 /* Framework1Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8658AE9296D008900AEFC87 /* Framework1Tests.swift */; }; - A8658AEB296D008900AEFC87 /* Framework1.h in Headers */ = {isa = PBXBuildFile; fileRef = A8658ADD296D008800AEFC87 /* Framework1.h */; settings = {ATTRIBUTES = (Public, ); }; }; - A8658AEE296D008900AEFC87 /* Framework1.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A8658ADB296D008800AEFC87 /* Framework1.framework */; }; - A8658AEF296D008900AEFC87 /* Framework1.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A8658ADB296D008800AEFC87 /* Framework1.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - A8658B04296D009600AEFC87 /* Framework2.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A8658AFC296D009500AEFC87 /* Framework2.framework */; }; - A8658B0B296D009600AEFC87 /* Framework2Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8658B0A296D009600AEFC87 /* Framework2Tests.swift */; }; - A8658B0C296D009600AEFC87 /* Framework2.h in Headers */ = {isa = PBXBuildFile; fileRef = A8658AFE296D009500AEFC87 /* Framework2.h */; settings = {ATTRIBUTES = (Public, ); }; }; - A8658B1E296D028E00AEFC87 /* Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8658B1D296D028E00AEFC87 /* Test.swift */; }; - A8658B20296D02D000AEFC87 /* Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8658B1F296D02D000AEFC87 /* Test.swift */; }; - A8658B28296D08BE00AEFC87 /* Static.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8658B27296D08BE00AEFC87 /* Static.swift */; }; - A87053C729755F9200EE09CC /* AnyCodable in Frameworks */ = {isa = PBXBuildFile; productRef = A87053C629755F9200EE09CC /* AnyCodable */; }; - A87F0AF2296C00F500FD92E0 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = A87F0AF4296C00F500FD92E0 /* Localizable.strings */; }; - A880B6FC2990F8FD000C5DCB /* SVProgressHUD.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A880B6FB2990F8EC000C5DCB /* SVProgressHUD.xcframework */; }; - A880B6FD2990F8FD000C5DCB /* SVProgressHUD.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A880B6FB2990F8EC000C5DCB /* SVProgressHUD.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - A88A1BF6296903E6003972AA /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A88A1BF5296903E6003972AA /* ExampleApp.swift */; }; - A88A1BF8296903E6003972AA /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A88A1BF7296903E6003972AA /* ContentView.swift */; }; - A88A1BFA296903E8003972AA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A88A1BF9296903E8003972AA /* Assets.xcassets */; }; - A88A1BFD296903E8003972AA /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A88A1BFC296903E8003972AA /* Preview Assets.xcassets */; }; - A88A1C07296903E8003972AA /* ExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A88A1C06296903E8003972AA /* ExampleTests.swift */; }; - A88A1C11296903E8003972AA /* ExampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A88A1C10296903E8003972AA /* ExampleUITests.swift */; }; - A88A1C13296903E8003972AA /* ExampleUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A88A1C12296903E8003972AA /* ExampleUITestsLaunchTests.swift */; }; - A88A1C2029690685003972AA /* Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = A88A1C1F29690685003972AA /* Test.swift */; }; - A8C5E98F2990A3FC00EC6696 /* libStatic2.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A83BC793298BBCED00499A6C /* libStatic2.a */; }; - A8C5E9922990A40B00EC6696 /* libStatic.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A8658B25296D08BD00AEFC87 /* libStatic.a */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - A83BC77D298BBCDA00499A6C /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = A88A1BEA296903E6003972AA /* Project object */; - proxyType = 1; - remoteGlobalIDString = A83BC773298BBCD900499A6C; - remoteInfo = Framework3; - }; - A83BC77F298BBCDA00499A6C /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = A88A1BEA296903E6003972AA /* Project object */; - proxyType = 1; - remoteGlobalIDString = A88A1BF1296903E6003972AA; - remoteInfo = Example; - }; - A83BC785298BBCDA00499A6C /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = A88A1BEA296903E6003972AA /* Project object */; - proxyType = 1; - remoteGlobalIDString = A83BC773298BBCD900499A6C; - remoteInfo = Framework3; - }; - A849A66E296E58DC006BB98A /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = A88A1BEA296903E6003972AA /* Project object */; - proxyType = 1; - remoteGlobalIDString = A88A1BF1296903E6003972AA; - remoteInfo = Example; - }; - A8658AE4296D008800AEFC87 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = A88A1BEA296903E6003972AA /* Project object */; - proxyType = 1; - remoteGlobalIDString = A8658ADA296D008800AEFC87; - remoteInfo = Framework1; - }; - A8658AE6296D008900AEFC87 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = A88A1BEA296903E6003972AA /* Project object */; - proxyType = 1; - remoteGlobalIDString = A88A1BF1296903E6003972AA; - remoteInfo = Example; - }; - A8658AEC296D008900AEFC87 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = A88A1BEA296903E6003972AA /* Project object */; - proxyType = 1; - remoteGlobalIDString = A8658ADA296D008800AEFC87; - remoteInfo = Framework1; - }; - A8658B05296D009600AEFC87 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = A88A1BEA296903E6003972AA /* Project object */; - proxyType = 1; - remoteGlobalIDString = A8658AFB296D009500AEFC87; - remoteInfo = Framework2; - }; - A8658B07296D009600AEFC87 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = A88A1BEA296903E6003972AA /* Project object */; - proxyType = 1; - remoteGlobalIDString = A88A1BF1296903E6003972AA; - remoteInfo = Example; - }; - A8658B1A296D00B000AEFC87 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = A88A1BEA296903E6003972AA /* Project object */; - proxyType = 1; - remoteGlobalIDString = A8658AFB296D009500AEFC87; - remoteInfo = Framework2; - }; - A88A1C0D296903E8003972AA /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = A88A1BEA296903E6003972AA /* Project object */; - proxyType = 1; - remoteGlobalIDString = A88A1BF1296903E6003972AA; - remoteInfo = Example; - }; - A8C5E9902990A3FC00EC6696 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = A88A1BEA296903E6003972AA /* Project object */; - proxyType = 1; - remoteGlobalIDString = A83BC792298BBCED00499A6C; - remoteInfo = Static2; - }; - A8C5E9932990A40B00EC6696 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = A88A1BEA296903E6003972AA /* Project object */; - proxyType = 1; - remoteGlobalIDString = A8658B24296D08BD00AEFC87; - remoteInfo = Static; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - A83BC791298BBCED00499A6C /* CopyFiles */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = "include/$(PRODUCT_NAME)"; - dstSubfolderSpec = 16; - files = ( - A83BC798298BBCED00499A6C /* Static2.h in CopyFiles */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A849A66B296D658B006BB98A /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - A849A66A296D658B006BB98A /* Framework2.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; - A8658AF3296D008900AEFC87 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - A8658AEF296D008900AEFC87 /* Framework1.framework in Embed Frameworks */, - A880B6FD2990F8FD000C5DCB /* SVProgressHUD.xcframework in Embed Frameworks */, - A83BC788298BBCDA00499A6C /* Framework3.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; - A8658B23296D08BD00AEFC87 /* CopyFiles */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = "include/$(PRODUCT_NAME)"; - dstSubfolderSpec = 16; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - A8337967297664A800E292DC /* Local1 */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Local1; sourceTree = ""; }; - A83BC774298BBCD900499A6C /* Framework3.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Framework3.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - A83BC776298BBCD900499A6C /* Framework3.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Framework3.h; sourceTree = ""; }; - A83BC77B298BBCDA00499A6C /* Framework3Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Framework3Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - A83BC782298BBCDA00499A6C /* Framework3Tests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Framework3Tests.m; sourceTree = ""; }; - A83BC793298BBCED00499A6C /* libStatic2.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libStatic2.a; sourceTree = BUILT_PRODUCTS_DIR; }; - A83BC795298BBCED00499A6C /* Static2.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Static2.h; sourceTree = ""; }; - A83BC796298BBCED00499A6C /* Static2.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Static2.m; sourceTree = ""; }; - A83BC7A1298BBD7300499A6C /* Framework.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Framework.h; sourceTree = ""; }; - A83BC7A2298BBD7300499A6C /* Framework.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Framework.m; sourceTree = ""; }; - A849A66C296D66CC006BB98A /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk/System/Library/Frameworks/AVFoundation.framework; sourceTree = DEVELOPER_DIR; }; - A8658ADB296D008800AEFC87 /* Framework1.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Framework1.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - A8658ADD296D008800AEFC87 /* Framework1.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Framework1.h; sourceTree = ""; }; - A8658AE2296D008800AEFC87 /* Framework1Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Framework1Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - A8658AE9296D008900AEFC87 /* Framework1Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Framework1Tests.swift; sourceTree = ""; }; - A8658AFC296D009500AEFC87 /* Framework2.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Framework2.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - A8658AFE296D009500AEFC87 /* Framework2.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Framework2.h; sourceTree = ""; }; - A8658B03296D009600AEFC87 /* Framework2Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Framework2Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - A8658B0A296D009600AEFC87 /* Framework2Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Framework2Tests.swift; sourceTree = ""; }; - A8658B1D296D028E00AEFC87 /* Test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Test.swift; sourceTree = ""; }; - A8658B1F296D02D000AEFC87 /* Test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Test.swift; sourceTree = ""; }; - A8658B25296D08BD00AEFC87 /* libStatic.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libStatic.a; sourceTree = BUILT_PRODUCTS_DIR; }; - A8658B27296D08BE00AEFC87 /* Static.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Static.swift; sourceTree = ""; }; - A87F0AF3296C00F500FD92E0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; - A87F0AF5296C00FB00FD92E0 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = ""; }; - A880B6FB2990F8EC000C5DCB /* SVProgressHUD.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = SVProgressHUD.xcframework; sourceTree = ""; }; - A88A1BF2296903E6003972AA /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; - A88A1BF5296903E6003972AA /* ExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleApp.swift; sourceTree = ""; }; - A88A1BF7296903E6003972AA /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; - A88A1BF9296903E8003972AA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - A88A1BFC296903E8003972AA /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - A88A1C02296903E8003972AA /* ExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - A88A1C06296903E8003972AA /* ExampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleTests.swift; sourceTree = ""; }; - A88A1C0C296903E8003972AA /* ExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - A88A1C10296903E8003972AA /* ExampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleUITests.swift; sourceTree = ""; }; - A88A1C12296903E8003972AA /* ExampleUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleUITestsLaunchTests.swift; sourceTree = ""; }; - A88A1C1F29690685003972AA /* Test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Test.swift; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - A83BC771298BBCD900499A6C /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A83BC778298BBCDA00499A6C /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - A83BC77C298BBCDA00499A6C /* Framework3.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A83BC790298BBCED00499A6C /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A8658AD8296D008800AEFC87 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - A849A669296D658B006BB98A /* Framework2.framework in Frameworks */, - A8C5E9922990A40B00EC6696 /* libStatic.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A8658ADF296D008800AEFC87 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - A8658AE3296D008800AEFC87 /* Framework1.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A8658AF9296D009500AEFC87 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A8658B00296D009600AEFC87 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - A8658B04296D009600AEFC87 /* Framework2.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A8658B22296D08BD00AEFC87 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A88A1BEF296903E6003972AA /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - A8C5E98F2990A3FC00EC6696 /* libStatic2.a in Frameworks */, - A87053C729755F9200EE09CC /* AnyCodable in Frameworks */, - A83BC787298BBCDA00499A6C /* Framework3.framework in Frameworks */, - A8120D0929767375004F3FD3 /* LocalLib2 in Frameworks */, - A8658AEE296D008900AEFC87 /* Framework1.framework in Frameworks */, - A8120D07297670E5004F3FD3 /* LocalLib1 in Frameworks */, - A880B6FC2990F8FD000C5DCB /* SVProgressHUD.xcframework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A88A1BFF296903E8003972AA /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A88A1C09296903E8003972AA /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - A8337964297663DF00E292DC /* Packages */ = { - isa = PBXGroup; - children = ( - A8337967297664A800E292DC /* Local1 */, - ); - name = Packages; - sourceTree = ""; - }; - A83BC775298BBCD900499A6C /* Framework3 */ = { - isa = PBXGroup; - children = ( - A83BC776298BBCD900499A6C /* Framework3.h */, - A83BC7A1298BBD7300499A6C /* Framework.h */, - A83BC7A2298BBD7300499A6C /* Framework.m */, - ); - path = Framework3; - sourceTree = ""; - }; - A83BC781298BBCDA00499A6C /* Framework3Tests */ = { - isa = PBXGroup; - children = ( - A83BC782298BBCDA00499A6C /* Framework3Tests.m */, - ); - path = Framework3Tests; - sourceTree = ""; - }; - A83BC794298BBCED00499A6C /* Static2 */ = { - isa = PBXGroup; - children = ( - A83BC795298BBCED00499A6C /* Static2.h */, - A83BC796298BBCED00499A6C /* Static2.m */, - ); - path = Static2; - sourceTree = ""; - }; - A8658ADC296D008800AEFC87 /* Framework1 */ = { - isa = PBXGroup; - children = ( - A8658ADD296D008800AEFC87 /* Framework1.h */, - A8658B1F296D02D000AEFC87 /* Test.swift */, - ); - path = Framework1; - sourceTree = ""; - }; - A8658AE8296D008900AEFC87 /* Framework1Tests */ = { - isa = PBXGroup; - children = ( - A8658AE9296D008900AEFC87 /* Framework1Tests.swift */, - ); - path = Framework1Tests; - sourceTree = ""; - }; - A8658AFD296D009500AEFC87 /* Framework2 */ = { - isa = PBXGroup; - children = ( - A8658AFE296D009500AEFC87 /* Framework2.h */, - A8658B1D296D028E00AEFC87 /* Test.swift */, - ); - path = Framework2; - sourceTree = ""; - }; - A8658B09296D009600AEFC87 /* Framework2Tests */ = { - isa = PBXGroup; - children = ( - A8658B0A296D009600AEFC87 /* Framework2Tests.swift */, - ); - path = Framework2Tests; - sourceTree = ""; - }; - A8658B17296D00B000AEFC87 /* Frameworks */ = { - isa = PBXGroup; - children = ( - A880B6FB2990F8EC000C5DCB /* SVProgressHUD.xcframework */, - A849A66C296D66CC006BB98A /* AVFoundation.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; - A8658B26296D08BE00AEFC87 /* Static */ = { - isa = PBXGroup; - children = ( - A8658B27296D08BE00AEFC87 /* Static.swift */, - ); - path = Static; - sourceTree = ""; - }; - A88A1BE9296903E6003972AA = { - isa = PBXGroup; - children = ( - A8337964297663DF00E292DC /* Packages */, - A88A1BF4296903E6003972AA /* Example */, - A88A1C05296903E8003972AA /* ExampleTests */, - A88A1C0F296903E8003972AA /* ExampleUITests */, - A8658ADC296D008800AEFC87 /* Framework1 */, - A8658AE8296D008900AEFC87 /* Framework1Tests */, - A8658AFD296D009500AEFC87 /* Framework2 */, - A8658B09296D009600AEFC87 /* Framework2Tests */, - A8658B26296D08BE00AEFC87 /* Static */, - A83BC775298BBCD900499A6C /* Framework3 */, - A83BC781298BBCDA00499A6C /* Framework3Tests */, - A83BC794298BBCED00499A6C /* Static2 */, - A88A1BF3296903E6003972AA /* Products */, - A8658B17296D00B000AEFC87 /* Frameworks */, - ); - sourceTree = ""; - }; - A88A1BF3296903E6003972AA /* Products */ = { - isa = PBXGroup; - children = ( - A88A1BF2296903E6003972AA /* Example.app */, - A88A1C02296903E8003972AA /* ExampleTests.xctest */, - A88A1C0C296903E8003972AA /* ExampleUITests.xctest */, - A8658ADB296D008800AEFC87 /* Framework1.framework */, - A8658AE2296D008800AEFC87 /* Framework1Tests.xctest */, - A8658AFC296D009500AEFC87 /* Framework2.framework */, - A8658B03296D009600AEFC87 /* Framework2Tests.xctest */, - A8658B25296D08BD00AEFC87 /* libStatic.a */, - A83BC774298BBCD900499A6C /* Framework3.framework */, - A83BC77B298BBCDA00499A6C /* Framework3Tests.xctest */, - A83BC793298BBCED00499A6C /* libStatic2.a */, - ); - name = Products; - sourceTree = ""; - }; - A88A1BF4296903E6003972AA /* Example */ = { - isa = PBXGroup; - children = ( - A88A1BF5296903E6003972AA /* ExampleApp.swift */, - A88A1BF7296903E6003972AA /* ContentView.swift */, - A88A1BF9296903E8003972AA /* Assets.xcassets */, - A88A1BFB296903E8003972AA /* Preview Content */, - A88A1C1F29690685003972AA /* Test.swift */, - A87F0AF4296C00F500FD92E0 /* Localizable.strings */, - ); - path = Example; - sourceTree = ""; - }; - A88A1BFB296903E8003972AA /* Preview Content */ = { - isa = PBXGroup; - children = ( - A88A1BFC296903E8003972AA /* Preview Assets.xcassets */, - ); - path = "Preview Content"; - sourceTree = ""; - }; - A88A1C05296903E8003972AA /* ExampleTests */ = { - isa = PBXGroup; - children = ( - A88A1C06296903E8003972AA /* ExampleTests.swift */, - ); - path = ExampleTests; - sourceTree = ""; - }; - A88A1C0F296903E8003972AA /* ExampleUITests */ = { - isa = PBXGroup; - children = ( - A88A1C10296903E8003972AA /* ExampleUITests.swift */, - A88A1C12296903E8003972AA /* ExampleUITestsLaunchTests.swift */, - ); - path = ExampleUITests; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXHeadersBuildPhase section */ - A83BC76F298BBCD900499A6C /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - A83BC7A3298BBD7300499A6C /* Framework.h in Headers */, - A83BC784298BBCDA00499A6C /* Framework3.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A8658AD6296D008800AEFC87 /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - A8658AEB296D008900AEFC87 /* Framework1.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A8658AF7296D009500AEFC87 /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - A8658B0C296D009600AEFC87 /* Framework2.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXHeadersBuildPhase section */ - -/* Begin PBXNativeTarget section */ - A83BC773298BBCD900499A6C /* Framework3 */ = { - isa = PBXNativeTarget; - buildConfigurationList = A83BC78D298BBCDA00499A6C /* Build configuration list for PBXNativeTarget "Framework3" */; - buildPhases = ( - A83BC76F298BBCD900499A6C /* Headers */, - A83BC770298BBCD900499A6C /* Sources */, - A83BC771298BBCD900499A6C /* Frameworks */, - A83BC772298BBCD900499A6C /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Framework3; - productName = Framework3; - productReference = A83BC774298BBCD900499A6C /* Framework3.framework */; - productType = "com.apple.product-type.framework"; - }; - A83BC77A298BBCDA00499A6C /* Framework3Tests */ = { - isa = PBXNativeTarget; - buildConfigurationList = A83BC78E298BBCDA00499A6C /* Build configuration list for PBXNativeTarget "Framework3Tests" */; - buildPhases = ( - A83BC777298BBCDA00499A6C /* Sources */, - A83BC778298BBCDA00499A6C /* Frameworks */, - A83BC779298BBCDA00499A6C /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - A83BC77E298BBCDA00499A6C /* PBXTargetDependency */, - A83BC780298BBCDA00499A6C /* PBXTargetDependency */, - ); - name = Framework3Tests; - productName = Framework3Tests; - productReference = A83BC77B298BBCDA00499A6C /* Framework3Tests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - A83BC792298BBCED00499A6C /* Static2 */ = { - isa = PBXNativeTarget; - buildConfigurationList = A83BC799298BBCED00499A6C /* Build configuration list for PBXNativeTarget "Static2" */; - buildPhases = ( - A83BC78F298BBCED00499A6C /* Sources */, - A83BC790298BBCED00499A6C /* Frameworks */, - A83BC791298BBCED00499A6C /* CopyFiles */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Static2; - productName = Static2; - productReference = A83BC793298BBCED00499A6C /* libStatic2.a */; - productType = "com.apple.product-type.library.static"; - }; - A8658ADA296D008800AEFC87 /* Framework1 */ = { - isa = PBXNativeTarget; - buildConfigurationList = A8658AF0296D008900AEFC87 /* Build configuration list for PBXNativeTarget "Framework1" */; - buildPhases = ( - A8658AD6296D008800AEFC87 /* Headers */, - A8658AD7296D008800AEFC87 /* Sources */, - A8658AD8296D008800AEFC87 /* Frameworks */, - A8658AD9296D008800AEFC87 /* Resources */, - A849A66B296D658B006BB98A /* Embed Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - A8658B1B296D00B000AEFC87 /* PBXTargetDependency */, - A8C5E9942990A40B00EC6696 /* PBXTargetDependency */, - ); - name = Framework1; - productName = Framework1; - productReference = A8658ADB296D008800AEFC87 /* Framework1.framework */; - productType = "com.apple.product-type.framework"; - }; - A8658AE1296D008800AEFC87 /* Framework1Tests */ = { - isa = PBXNativeTarget; - buildConfigurationList = A8658AF4296D008900AEFC87 /* Build configuration list for PBXNativeTarget "Framework1Tests" */; - buildPhases = ( - A8658ADE296D008800AEFC87 /* Sources */, - A8658ADF296D008800AEFC87 /* Frameworks */, - A8658AE0296D008800AEFC87 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - A8658AE5296D008800AEFC87 /* PBXTargetDependency */, - A8658AE7296D008900AEFC87 /* PBXTargetDependency */, - ); - name = Framework1Tests; - productName = Framework1Tests; - productReference = A8658AE2296D008800AEFC87 /* Framework1Tests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - A8658AFB296D009500AEFC87 /* Framework2 */ = { - isa = PBXNativeTarget; - buildConfigurationList = A8658B11296D009600AEFC87 /* Build configuration list for PBXNativeTarget "Framework2" */; - buildPhases = ( - A8658AF7296D009500AEFC87 /* Headers */, - A8658AF8296D009500AEFC87 /* Sources */, - A8658AF9296D009500AEFC87 /* Frameworks */, - A8658AFA296D009500AEFC87 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Framework2; - productName = Framework2; - productReference = A8658AFC296D009500AEFC87 /* Framework2.framework */; - productType = "com.apple.product-type.framework"; - }; - A8658B02296D009600AEFC87 /* Framework2Tests */ = { - isa = PBXNativeTarget; - buildConfigurationList = A8658B14296D009600AEFC87 /* Build configuration list for PBXNativeTarget "Framework2Tests" */; - buildPhases = ( - A8658AFF296D009600AEFC87 /* Sources */, - A8658B00296D009600AEFC87 /* Frameworks */, - A8658B01296D009600AEFC87 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - A8658B06296D009600AEFC87 /* PBXTargetDependency */, - A8658B08296D009600AEFC87 /* PBXTargetDependency */, - ); - name = Framework2Tests; - productName = Framework2Tests; - productReference = A8658B03296D009600AEFC87 /* Framework2Tests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - A8658B24296D08BD00AEFC87 /* Static */ = { - isa = PBXNativeTarget; - buildConfigurationList = A8658B29296D08BE00AEFC87 /* Build configuration list for PBXNativeTarget "Static" */; - buildPhases = ( - A8658B21296D08BD00AEFC87 /* Sources */, - A8658B22296D08BD00AEFC87 /* Frameworks */, - A8658B23296D08BD00AEFC87 /* CopyFiles */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Static; - productName = Static; - productReference = A8658B25296D08BD00AEFC87 /* libStatic.a */; - productType = "com.apple.product-type.library.static"; - }; - A88A1BF1296903E6003972AA /* Example */ = { - isa = PBXNativeTarget; - buildConfigurationList = A88A1C16296903E9003972AA /* Build configuration list for PBXNativeTarget "Example" */; - buildPhases = ( - A88A1BEE296903E6003972AA /* Sources */, - A88A1BEF296903E6003972AA /* Frameworks */, - A88A1BF0296903E6003972AA /* Resources */, - A8658AF3296D008900AEFC87 /* Embed Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - A8658AED296D008900AEFC87 /* PBXTargetDependency */, - A83BC786298BBCDA00499A6C /* PBXTargetDependency */, - A8C5E9912990A3FC00EC6696 /* PBXTargetDependency */, - ); - name = Example; - packageProductDependencies = ( - A87053C629755F9200EE09CC /* AnyCodable */, - A8120D06297670E5004F3FD3 /* LocalLib1 */, - A8120D0829767375004F3FD3 /* LocalLib2 */, - ); - productName = Example; - productReference = A88A1BF2296903E6003972AA /* Example.app */; - productType = "com.apple.product-type.application"; - }; - A88A1C01296903E8003972AA /* ExampleTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = A88A1C19296903E9003972AA /* Build configuration list for PBXNativeTarget "ExampleTests" */; - buildPhases = ( - A88A1BFE296903E8003972AA /* Sources */, - A88A1BFF296903E8003972AA /* Frameworks */, - A88A1C00296903E8003972AA /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - A849A66F296E58DC006BB98A /* PBXTargetDependency */, - ); - name = ExampleTests; - productName = ExampleTests; - productReference = A88A1C02296903E8003972AA /* ExampleTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - A88A1C0B296903E8003972AA /* ExampleUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = A88A1C1C296903E9003972AA /* Build configuration list for PBXNativeTarget "ExampleUITests" */; - buildPhases = ( - A88A1C08296903E8003972AA /* Sources */, - A88A1C09296903E8003972AA /* Frameworks */, - A88A1C0A296903E8003972AA /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - A88A1C0E296903E8003972AA /* PBXTargetDependency */, - ); - name = ExampleUITests; - productName = ExampleUITests; - productReference = A88A1C0C296903E8003972AA /* ExampleUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - A88A1BEA296903E6003972AA /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1420; - LastUpgradeCheck = 1420; - TargetAttributes = { - A83BC773298BBCD900499A6C = { - CreatedOnToolsVersion = 14.2; - }; - A83BC77A298BBCDA00499A6C = { - CreatedOnToolsVersion = 14.2; - TestTargetID = A88A1BF1296903E6003972AA; - }; - A83BC792298BBCED00499A6C = { - CreatedOnToolsVersion = 14.2; - }; - A8658ADA296D008800AEFC87 = { - CreatedOnToolsVersion = 14.2; - LastSwiftMigration = 1420; - }; - A8658AE1296D008800AEFC87 = { - CreatedOnToolsVersion = 14.2; - TestTargetID = A88A1BF1296903E6003972AA; - }; - A8658AFB296D009500AEFC87 = { - CreatedOnToolsVersion = 14.2; - LastSwiftMigration = 1420; - }; - A8658B02296D009600AEFC87 = { - CreatedOnToolsVersion = 14.2; - TestTargetID = A88A1BF1296903E6003972AA; - }; - A8658B24296D08BD00AEFC87 = { - CreatedOnToolsVersion = 14.2; - }; - A88A1BF1296903E6003972AA = { - CreatedOnToolsVersion = 14.2; - }; - A88A1C01296903E8003972AA = { - CreatedOnToolsVersion = 14.2; - TestTargetID = A88A1BF1296903E6003972AA; - }; - A88A1C0B296903E8003972AA = { - CreatedOnToolsVersion = 14.2; - TestTargetID = A88A1BF1296903E6003972AA; - }; - }; - }; - buildConfigurationList = A88A1BED296903E6003972AA /* Build configuration list for PBXProject "Example" */; - compatibilityVersion = "Xcode 14.0"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - "zh-Hant", - ); - mainGroup = A88A1BE9296903E6003972AA; - packageReferences = ( - A87053C529755F9200EE09CC /* XCRemoteSwiftPackageReference "AnyCodable" */, - ); - productRefGroup = A88A1BF3296903E6003972AA /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - A88A1BF1296903E6003972AA /* Example */, - A88A1C01296903E8003972AA /* ExampleTests */, - A88A1C0B296903E8003972AA /* ExampleUITests */, - A8658ADA296D008800AEFC87 /* Framework1 */, - A8658AE1296D008800AEFC87 /* Framework1Tests */, - A8658AFB296D009500AEFC87 /* Framework2 */, - A8658B02296D009600AEFC87 /* Framework2Tests */, - A8658B24296D08BD00AEFC87 /* Static */, - A83BC773298BBCD900499A6C /* Framework3 */, - A83BC77A298BBCDA00499A6C /* Framework3Tests */, - A83BC792298BBCED00499A6C /* Static2 */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - A83BC772298BBCD900499A6C /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A83BC779298BBCDA00499A6C /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A8658AD9296D008800AEFC87 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A8658AE0296D008800AEFC87 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A8658AFA296D009500AEFC87 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A8658B01296D009600AEFC87 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A88A1BF0296903E6003972AA /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - A88A1BFD296903E8003972AA /* Preview Assets.xcassets in Resources */, - A87F0AF2296C00F500FD92E0 /* Localizable.strings in Resources */, - A88A1BFA296903E8003972AA /* Assets.xcassets in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A88A1C00296903E8003972AA /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A88A1C0A296903E8003972AA /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - A83BC770298BBCD900499A6C /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - A83BC7A4298BBD7300499A6C /* Framework.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A83BC777298BBCDA00499A6C /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - A83BC783298BBCDA00499A6C /* Framework3Tests.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A83BC78F298BBCED00499A6C /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - A83BC797298BBCED00499A6C /* Static2.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A8658AD7296D008800AEFC87 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - A8658B20296D02D000AEFC87 /* Test.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A8658ADE296D008800AEFC87 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - A8658AEA296D008900AEFC87 /* Framework1Tests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A8658AF8296D009500AEFC87 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - A8658B1E296D028E00AEFC87 /* Test.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A8658AFF296D009600AEFC87 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - A8658B0B296D009600AEFC87 /* Framework2Tests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A8658B21296D08BD00AEFC87 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - A8658B28296D08BE00AEFC87 /* Static.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A88A1BEE296903E6003972AA /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - A88A1BF8296903E6003972AA /* ContentView.swift in Sources */, - A88A1BF6296903E6003972AA /* ExampleApp.swift in Sources */, - A88A1C2029690685003972AA /* Test.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A88A1BFE296903E8003972AA /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - A88A1C07296903E8003972AA /* ExampleTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A88A1C08296903E8003972AA /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - A88A1C11296903E8003972AA /* ExampleUITests.swift in Sources */, - A88A1C13296903E8003972AA /* ExampleUITestsLaunchTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - A83BC77E298BBCDA00499A6C /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = A83BC773298BBCD900499A6C /* Framework3 */; - targetProxy = A83BC77D298BBCDA00499A6C /* PBXContainerItemProxy */; - }; - A83BC780298BBCDA00499A6C /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = A88A1BF1296903E6003972AA /* Example */; - targetProxy = A83BC77F298BBCDA00499A6C /* PBXContainerItemProxy */; - }; - A83BC786298BBCDA00499A6C /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = A83BC773298BBCD900499A6C /* Framework3 */; - targetProxy = A83BC785298BBCDA00499A6C /* PBXContainerItemProxy */; - }; - A849A66F296E58DC006BB98A /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = A88A1BF1296903E6003972AA /* Example */; - targetProxy = A849A66E296E58DC006BB98A /* PBXContainerItemProxy */; - }; - A8658AE5296D008800AEFC87 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = A8658ADA296D008800AEFC87 /* Framework1 */; - targetProxy = A8658AE4296D008800AEFC87 /* PBXContainerItemProxy */; - }; - A8658AE7296D008900AEFC87 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = A88A1BF1296903E6003972AA /* Example */; - targetProxy = A8658AE6296D008900AEFC87 /* PBXContainerItemProxy */; - }; - A8658AED296D008900AEFC87 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = A8658ADA296D008800AEFC87 /* Framework1 */; - targetProxy = A8658AEC296D008900AEFC87 /* PBXContainerItemProxy */; - }; - A8658B06296D009600AEFC87 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = A8658AFB296D009500AEFC87 /* Framework2 */; - targetProxy = A8658B05296D009600AEFC87 /* PBXContainerItemProxy */; - }; - A8658B08296D009600AEFC87 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = A88A1BF1296903E6003972AA /* Example */; - targetProxy = A8658B07296D009600AEFC87 /* PBXContainerItemProxy */; - }; - A8658B1B296D00B000AEFC87 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = A8658AFB296D009500AEFC87 /* Framework2 */; - targetProxy = A8658B1A296D00B000AEFC87 /* PBXContainerItemProxy */; - }; - A88A1C0E296903E8003972AA /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = A88A1BF1296903E6003972AA /* Example */; - targetProxy = A88A1C0D296903E8003972AA /* PBXContainerItemProxy */; - }; - A8C5E9912990A3FC00EC6696 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = A83BC792298BBCED00499A6C /* Static2 */; - targetProxy = A8C5E9902990A3FC00EC6696 /* PBXContainerItemProxy */; - }; - A8C5E9942990A40B00EC6696 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = A8658B24296D08BD00AEFC87 /* Static */; - targetProxy = A8C5E9932990A40B00EC6696 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - A87F0AF4296C00F500FD92E0 /* Localizable.strings */ = { - isa = PBXVariantGroup; - children = ( - A87F0AF3296C00F500FD92E0 /* en */, - A87F0AF5296C00FB00FD92E0 /* zh-Hant */, - ); - name = Localizable.strings; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - A83BC789298BBCDA00499A6C /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.yume190.Framework3; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Debug; - }; - A83BC78A298BBCDA00499A6C /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.yume190.Framework3; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Release; - }; - A83BC78B298BBCDA00499A6C /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 37MR9UKGT4; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.yume190.Framework3Tests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Example"; - }; - name = Debug; - }; - A83BC78C298BBCDA00499A6C /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 37MR9UKGT4; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.yume190.Framework3Tests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Example"; - }; - name = Release; - }; - A83BC79A298BBCED00499A6C /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 37MR9UKGT4; - OTHER_LDFLAGS = "-ObjC"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - A83BC79B298BBCED00499A6C /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 37MR9UKGT4; - OTHER_LDFLAGS = "-ObjC"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - A8658AF1296D008900AEFC87 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.bazel.Framework1; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Debug; - }; - A8658AF2296D008900AEFC87 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.bazel.Framework1; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Release; - }; - A8658AF5296D008900AEFC87 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.bazel.Framework1Tests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Example"; - }; - name = Debug; - }; - A8658AF6296D008900AEFC87 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.bazel.Framework1Tests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Example"; - }; - name = Release; - }; - A8658B12296D009600AEFC87 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MACH_O_TYPE = mh_dylib; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.bazel.Framework2; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Debug; - }; - A8658B13296D009600AEFC87 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MACH_O_TYPE = mh_dylib; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.bazel.Framework2; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Release; - }; - A8658B15296D009600AEFC87 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.bazel.Framework2Tests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Example"; - }; - name = Debug; - }; - A8658B16296D009600AEFC87 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.bazel.Framework2Tests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Example"; - }; - name = Release; - }; - A8658B2A296D08BE00AEFC87 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - OTHER_LDFLAGS = "-ObjC"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - A8658B2B296D08BE00AEFC87 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - OTHER_LDFLAGS = "-ObjC"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - A88A1C14296903E8003972AA /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.2; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - A88A1C15296903E8003972AA /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.2; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - A88A1C17296903E9003972AA /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\""; - ENABLE_PREVIEWS = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - OTHER_SWIFT_FLAGS = "-DYDebug -D A"; - PRODUCT_BUNDLE_IDENTIFIER = com.bazel.Example; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - A88A1C18296903E9003972AA /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\""; - ENABLE_PREVIEWS = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - OTHER_SWIFT_FLAGS = "-DYRelease -D B"; - PRODUCT_BUNDLE_IDENTIFIER = com.bazel.Example; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - A88A1C1A296903E9003972AA /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.2; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.bazel.ExampleTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Example"; - }; - name = Debug; - }; - A88A1C1B296903E9003972AA /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.2; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.bazel.ExampleTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Example"; - }; - name = Release; - }; - A88A1C1D296903E9003972AA /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.bazel.ExampleUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = ""; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = Example; - }; - name = Debug; - }; - A88A1C1E296903E9003972AA /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.bazel.ExampleUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = ""; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = Example; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - A83BC78D298BBCDA00499A6C /* Build configuration list for PBXNativeTarget "Framework3" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - A83BC789298BBCDA00499A6C /* Debug */, - A83BC78A298BBCDA00499A6C /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - A83BC78E298BBCDA00499A6C /* Build configuration list for PBXNativeTarget "Framework3Tests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - A83BC78B298BBCDA00499A6C /* Debug */, - A83BC78C298BBCDA00499A6C /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - A83BC799298BBCED00499A6C /* Build configuration list for PBXNativeTarget "Static2" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - A83BC79A298BBCED00499A6C /* Debug */, - A83BC79B298BBCED00499A6C /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - A8658AF0296D008900AEFC87 /* Build configuration list for PBXNativeTarget "Framework1" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - A8658AF1296D008900AEFC87 /* Debug */, - A8658AF2296D008900AEFC87 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - A8658AF4296D008900AEFC87 /* Build configuration list for PBXNativeTarget "Framework1Tests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - A8658AF5296D008900AEFC87 /* Debug */, - A8658AF6296D008900AEFC87 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - A8658B11296D009600AEFC87 /* Build configuration list for PBXNativeTarget "Framework2" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - A8658B12296D009600AEFC87 /* Debug */, - A8658B13296D009600AEFC87 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - A8658B14296D009600AEFC87 /* Build configuration list for PBXNativeTarget "Framework2Tests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - A8658B15296D009600AEFC87 /* Debug */, - A8658B16296D009600AEFC87 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - A8658B29296D08BE00AEFC87 /* Build configuration list for PBXNativeTarget "Static" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - A8658B2A296D08BE00AEFC87 /* Debug */, - A8658B2B296D08BE00AEFC87 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - A88A1BED296903E6003972AA /* Build configuration list for PBXProject "Example" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - A88A1C14296903E8003972AA /* Debug */, - A88A1C15296903E8003972AA /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - A88A1C16296903E9003972AA /* Build configuration list for PBXNativeTarget "Example" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - A88A1C17296903E9003972AA /* Debug */, - A88A1C18296903E9003972AA /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - A88A1C19296903E9003972AA /* Build configuration list for PBXNativeTarget "ExampleTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - A88A1C1A296903E9003972AA /* Debug */, - A88A1C1B296903E9003972AA /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - A88A1C1C296903E9003972AA /* Build configuration list for PBXNativeTarget "ExampleUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - A88A1C1D296903E9003972AA /* Debug */, - A88A1C1E296903E9003972AA /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - -/* Begin XCRemoteSwiftPackageReference section */ - A87053C529755F9200EE09CC /* XCRemoteSwiftPackageReference "AnyCodable" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/Flight-School/AnyCodable"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.6.7; - }; - }; -/* End XCRemoteSwiftPackageReference section */ - -/* Begin XCSwiftPackageProductDependency section */ - A8120D06297670E5004F3FD3 /* LocalLib1 */ = { - isa = XCSwiftPackageProductDependency; - productName = LocalLib1; - }; - A8120D0829767375004F3FD3 /* LocalLib2 */ = { - isa = XCSwiftPackageProductDependency; - productName = LocalLib2; - }; - A87053C629755F9200EE09CC /* AnyCodable */ = { - isa = XCSwiftPackageProductDependency; - package = A87053C529755F9200EE09CC /* XCRemoteSwiftPackageReference "AnyCodable" */; - productName = AnyCodable; - }; -/* End XCSwiftPackageProductDependency section */ - }; - rootObject = A88A1BEA296903E6003972AA /* Project object */; -} diff --git a/fixture/iOS/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme b/fixture/iOS/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme deleted file mode 100644 index eb9b7d0..0000000 --- a/fixture/iOS/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme +++ /dev/null @@ -1,136 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/fixture/iOS/Makefile b/fixture/iOS/Makefile index 9f8bc46..55a8d40 100644 --- a/fixture/iOS/Makefile +++ b/fixture/iOS/Makefile @@ -1,4 +1,5 @@ BAZELIZE = ../../.build/debug/bazelize +TUIST = tuist OUTPUT = App # The simulator rules_apple runs unit tests on; override for other Xcode versions. @@ -12,8 +13,12 @@ TESTS = \ //Targets/Framework2Tests \ //Targets/Framework3Tests +.PHONY: project +project: + @$(TUIST) generate --no-open + .PHONY: bazelize -bazelize: +bazelize: project @$(BAZELIZE) --input Example.xcodeproj --output $(OUTPUT) .PHONY: build diff --git a/fixture/iOS/Project.swift b/fixture/iOS/Project.swift new file mode 100644 index 0000000..6811cb1 --- /dev/null +++ b/fixture/iOS/Project.swift @@ -0,0 +1,139 @@ +import ProjectDescription + +let deploymentTargets = DeploymentTargets.iOS("16.2") + +func settings(_ values: SettingsDictionary = [:]) -> Settings { + .settings( + base: [ + "IPHONEOS_DEPLOYMENT_TARGET": "16.2", + "SWIFT_VERSION": "5.0", + ].merging(values) { _, value in value }) +} + +func target( + _ name: String, + product: Product, + sources: SourceFilesList, + dependencies: [TargetDependency] = [], + headers: Headers? = nil, + resources: ResourceFileElements? = nil, + settings targetSettings: SettingsDictionary = [:] +) -> Target { + .target( + name: name, + destinations: .iOS, + product: product, + bundleId: "com.bazel.\(name)", + deploymentTargets: deploymentTargets, + infoPlist: product == .app || product == .unitTests || product == .uiTests + ? .extendingDefault(with: [:]) + : nil, + sources: sources, + resources: resources, + headers: headers, + dependencies: dependencies, + settings: settings(targetSettings)) +} + +let project = Project( + name: "Example", + organizationName: "com.bazel", + options: .options( + automaticSchemesOptions: .enabled(), + defaultKnownRegions: ["en", "Base", "zh-Hant"], + developmentRegion: "en"), + packages: [ + .remote( + url: "https://github.com/Flight-School/AnyCodable", + requirement: .upToNextMajor(from: "0.6.7")), + .local(path: "Local1"), + ], + settings: settings(), + targets: [ + target( + "Static", + product: .staticLibrary, + sources: ["Static/**/*.swift"]), + target( + "Static2", + product: .staticLibrary, + sources: ["Static2/**/*.m"], + headers: .headers(public: ["Static2/**/*.h"])), + target( + "Framework3", + product: .framework, + sources: ["Framework3/**/*.m"], + headers: .headers(public: ["Framework3/**/*.h"])), + target( + "Framework2", + product: .framework, + sources: ["Framework2/**/*.swift"], + headers: .headers(public: ["Framework2/**/*.h"])), + target( + "Framework1", + product: .framework, + sources: ["Framework1/**/*.swift"], + dependencies: [ + .target(name: "Framework2"), + .target(name: "Static"), + ], + headers: .headers(public: ["Framework1/**/*.h"])), + target( + "Example", + product: .app, + sources: ["Example/**/*.swift"], + dependencies: [ + .target(name: "Framework1"), + .target(name: "Framework3"), + .target(name: "Static2"), + .package(product: "AnyCodable"), + .package(product: "LocalLib1"), + .package(product: "LocalLib2"), + .xcframework(path: "SVProgressHUD.xcframework"), + ], + resources: [ + "Example/Assets.xcassets", + "Example/Preview Content/**", + "Example/*.lproj/**", + ], + settings: [ + "ASSETCATALOG_COMPILER_APPICON_NAME": "AppIcon", + "ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME": "AccentColor", + "DEVELOPMENT_ASSET_PATHS": "\"Example/Preview Content\"", + "OTHER_SWIFT_FLAGS": "-DYDebug -D A", + ]), + target( + "ExampleTests", + product: .unitTests, + sources: ["ExampleTests/**/*.swift"], + dependencies: [.target(name: "Example")]), + target( + "Framework1Tests", + product: .unitTests, + sources: ["Framework1Tests/**/*.swift"], + dependencies: [ + .target(name: "Example"), + .target(name: "Framework1"), + ]), + target( + "Framework2Tests", + product: .unitTests, + sources: ["Framework2Tests/**/*.swift"], + dependencies: [ + .target(name: "Example"), + .target(name: "Framework2"), + ]), + target( + "Framework3Tests", + product: .unitTests, + sources: ["Framework3Tests/**/*.m"], + dependencies: [ + .target(name: "Example"), + .target(name: "Framework3"), + ]), + target( + "ExampleUITests", + product: .uiTests, + sources: ["ExampleUITests/**/*.swift"], + dependencies: [.target(name: "Example")]), + ]) diff --git a/fixture/iOS/Tuist.swift b/fixture/iOS/Tuist.swift new file mode 100644 index 0000000..23713fe --- /dev/null +++ b/fixture/iOS/Tuist.swift @@ -0,0 +1,5 @@ +import ProjectDescription + +/// Stops Tuist from walking up into the Bazelize repository, whose `Package.swift` +/// belongs to the generator rather than to this fixture. +let tuist = Tuist(project: .tuist()) diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..3dbbb61 --- /dev/null +++ b/mise.toml @@ -0,0 +1,2 @@ +[tools] +tuist = "4.200.5" From 7ef26fdd1df79d6c30bcbc8e437031327ef74c0c Mon Sep 17 00:00:00 2001 From: yume190 Date: Wed, 23 Sep 2026 15:30:14 +0800 Subject: [PATCH 46/47] fix: a static framework is a library, and xcodeproj names targets that exist Two of the TODOs left in the tree were answerable. A framework target that links statically has no bundle anything loads, so generating ios_framework for it built a dylib no rule embedded. It now aliases the library its dependents already link, which is what Xcode does with MACH_O_TYPE = staticlib. The rules_xcodeproj rule named a top level target ':App' that no generated BUILD declares, so 'bazel build //:xcodeproj' failed analysis on the label. It lists the targets the run generated, under the project's own name. The rest of the TODOs described work that was already done: .bazelrc imports config.bazelrc, objc_library and framework generation landed, and the commented-out exports_files block outlived the question it asked. --- Sources/BazelRules/Rules+Builtin.swift | 12 +++ Sources/BazelizeKit/Bazel/Bazel+BazelRC.swift | 6 +- Sources/BazelizeKit/Bazel/Bazel+Module.swift | 9 +- .../BazelizeKit/Bazel/Bazel+RootBuild.swift | 29 ------- Sources/BazelizeKit/Bazel/CodeBuilder.swift | 9 -- .../Codegen/Codegen+Framework.swift | 2 - .../Codegen/Codegen+StaticFramework.swift | 82 +++++++------------ .../BazelizeKit/Codegen/Codegen+Target.swift | 6 +- .../Language/Codegen+ObjcLibrary.swift | 2 - Sources/BazelizeKit/Kit.swift | 2 - .../BazelizeKit/Plugin/Plugin+XcodeProj.swift | 49 +++++------ .../Model/Config/Xcode+BuildSettings.swift | 4 + .../XcodeTests/RoadmapTreeBuilderTests.swift | 18 ++++ fixture/iOS/Project.swift | 5 ++ fixture/iOS/StaticFramework1/Test.swift | 14 ++++ 15 files changed, 113 insertions(+), 136 deletions(-) create mode 100644 fixture/iOS/StaticFramework1/Test.swift diff --git a/Sources/BazelRules/Rules+Builtin.swift b/Sources/BazelRules/Rules+Builtin.swift index ee92327..500af73 100644 --- a/Sources/BazelRules/Rules+Builtin.swift +++ b/Sources/BazelRules/Rules+Builtin.swift @@ -31,6 +31,18 @@ extension Rules.Builtin.Call { } } + /// The `module(...)` directive every `MODULE.bazel` opens with. + public static func module( + name: String, + version: String) + -> Starlark.Statement.Call + { + .init("module") { + "name" => name + "version" => version + } + } + public static func bazel_dep( name: String, version: String, diff --git a/Sources/BazelizeKit/Bazel/Bazel+BazelRC.swift b/Sources/BazelizeKit/Bazel/Bazel+BazelRC.swift index fd5d8ee..a6bf6b2 100644 --- a/Sources/BazelizeKit/Bazel/Bazel+BazelRC.swift +++ b/Sources/BazelizeKit/Bazel/Bazel+BazelRC.swift @@ -12,11 +12,7 @@ extension Bazel { /// [config](https://bazel.build/docs/configurable-attributes) /// [.bazelrc](https://bazel.build/run/bazelrc) /// - /// - /// TODO: - /// TIP: `import %workspace%/config.bazelrc` in `.bazelrc` - /// - /// /config.bazelrc + /// /config.bazelrc, which the generated `.bazelrc` imports. struct BazelRC: BazelFile { let path: Path private(set) var code = "" diff --git a/Sources/BazelizeKit/Bazel/Bazel+Module.swift b/Sources/BazelizeKit/Bazel/Bazel+Module.swift index 0f7e0df..ea5a1e7 100644 --- a/Sources/BazelizeKit/Bazel/Bazel+Module.swift +++ b/Sources/BazelizeKit/Bazel/Bazel+Module.swift @@ -5,6 +5,7 @@ // Created by Yume on 2023/1/30. // +import BazelRules import Foundation import PathKit import Starlark @@ -32,10 +33,10 @@ extension Bazel { } private func setup() { - builder.add("module") { - "name" => "example" - "version" => "0.0.1" - } + builder.call( + Rules.Builtin.Call.module( + name: "example", + version: "0.0.1")) builder.bazel_dep( name: "bazel_skylib", version: skylib.rawValue) diff --git a/Sources/BazelizeKit/Bazel/Bazel+RootBuild.swift b/Sources/BazelizeKit/Bazel/Bazel+RootBuild.swift index c628e0f..6d380c9 100644 --- a/Sources/BazelizeKit/Bazel/Bazel+RootBuild.swift +++ b/Sources/BazelizeKit/Bazel/Bazel+RootBuild.swift @@ -81,34 +81,5 @@ extension Bazel { flag_values: [":mode": config])) } } - - /// ~~export files not in~~ - /// [Bazel Package](https://bazel.build/concepts/build-ref) - /// - /// don't need to export files - func exportUncategorizedFiles(_: Kit) { -// let all = kit.project.all -// .filter(\.isFile) -// .compactMap(\.label) -// .filter { label in -// label.hasPrefix("//:") -// } -// .map { label in -// // TODO: remove //: -// """ -// "\(label.delete(prefix: "//:"))" -// """ -// } -// -// guard all.count != 0 else { return } -// -// builder.custom("") -// builder.custom(""" -// # export files not in [Bazel Package](https://bazel.build/concepts/build-ref) -// exports_files([ -// \(all.map(\.withComma).withNewLine.indent(1)) -// ]) -// """) - } } } diff --git a/Sources/BazelizeKit/Bazel/CodeBuilder.swift b/Sources/BazelizeKit/Bazel/CodeBuilder.swift index 48e2408..fc88c4e 100644 --- a/Sources/BazelizeKit/Bazel/CodeBuilder.swift +++ b/Sources/BazelizeKit/Bazel/CodeBuilder.swift @@ -75,10 +75,6 @@ extension CodeBuilder { symbols: statementLoad.symbols) } -// func load(_ code: String) { -// statements.append(.custom(code)) -// } - func load(loadableRule rule: LoadableRule) { load( module: rule.module, @@ -88,11 +84,6 @@ extension CodeBuilder { // MARK: - RuleBuild extension CodeBuilder { - // FIXME: (@yume190) todo remove add - func add(_ rule: String, @ArgumentBuilder builder: () -> [ArgumentBuilder.Target]) { - statements.append(.call(.init(rule, builder: builder))) - } - func call(_ call: Starlark.Statement.Call) { statements.append(.call(call)) } diff --git a/Sources/BazelizeKit/Codegen/Codegen+Framework.swift b/Sources/BazelizeKit/Codegen/Codegen+Framework.swift index 0393c64..7c6868c 100644 --- a/Sources/BazelizeKit/Codegen/Codegen+Framework.swift +++ b/Sources/BazelizeKit/Codegen/Codegen+Framework.swift @@ -1,5 +1,3 @@ -// TODO: https://github.com/XCodeBazelize/Bazelize/issues/8 framework(static/dynamic) - extension Target { func generateFrameworkCode(_ builder: CodeBuilder, _ kit: Kit) { switch platformSDK { diff --git a/Sources/BazelizeKit/Codegen/Codegen+StaticFramework.swift b/Sources/BazelizeKit/Codegen/Codegen+StaticFramework.swift index 202555f..c1a9c2a 100644 --- a/Sources/BazelizeKit/Codegen/Codegen+StaticFramework.swift +++ b/Sources/BazelizeKit/Codegen/Codegen+StaticFramework.swift @@ -1,58 +1,32 @@ -//// -//// StaticFramework.swift -//// -//// -//// Created by Yume on 2022/5/4. -//// // -// import Foundation -// import XcodeProj +// Codegen+StaticFramework.swift // -////XcodeProj.PBXProductType.static -// extension PBXNativeTarget { -// func generateStaitcFrameworkCode(_ kit: Kit) -> String { -//// let bundle_id = buildSettings.bundleID ?? "" -// let bundle_id = "" -// let podDeps: String = kit.pod?[name] ?? "" -// let xcodeDeps = "" -// let xcodeSPMDeps = spm_deps.joined(separator: "\n") // -// let code = """ -// load("@build_bazel_rules_apple//apple:ios.bzl", "ios_static_framework") -// load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +// Created by Yume on 2022/5/4. // -// swift_library( -// name = "_\(name)", -// module_name = "\(name)", -// srcs = [ -// \(srcs) -// ], -// deps = [ -// # Cocoapod Deps -// \(podDeps.indent(2)) -// -// # Xcode SPM Deps -// \(xcodeSPMDeps.indent(2)) -// ], -// ) -// -// ios_static_framework( -// name = "\(name)", -// bundle_id = "\(bundle_id)", -// families = [ -// "iphone", -// "ipad", -// ], -// minimum_os_version = "13.0", -// infoplists = [":Info.plist"], -// deps = [":_\(name)"], -// frameworks = [ -// # Xcode Target Deps -// \(xcodeDeps) -// ], -// ) -// """ -// -// return code -// } -// } + +import BazelRules +import Foundation +import Starlark + +extension Target { + /// A framework target that links statically: `MACH_O_TYPE = staticlib`, or the + /// product type Xcode gives a target created as a static framework. + /// + /// Nothing loads such a framework at runtime — its objects end up inside + /// whatever links it — so there is no bundle to build. The label a dependent + /// names stays valid by pointing at the library the target already generates. + var isStaticFramework: Bool { + if productType == "com.apple.product-type.framework.static" { return true } + guard productType == "com.apple.product-type.framework" else { return false } + return prefer(\.machOType) == "staticlib" + } + + func generateStaticFrameworkCode(_ builder: CodeBuilder, _: Kit) { + builder.call( + Rules.Builtin.Call.alias( + name: name, + actual: .named("\(name)_library"), + visibility: .public)) + } +} diff --git a/Sources/BazelizeKit/Codegen/Codegen+Target.swift b/Sources/BazelizeKit/Codegen/Codegen+Target.swift index 857bb6c..5a42380 100644 --- a/Sources/BazelizeKit/Codegen/Codegen+Target.swift +++ b/Sources/BazelizeKit/Codegen/Codegen+Target.swift @@ -45,7 +45,11 @@ extension Target { generateApplicationCode(builder, kit) case "com.apple.product-type.tool": generateCommandLineApplicationCode(builder, kit) - case "com.apple.product-type.framework": + case "com.apple.product-type.framework", "com.apple.product-type.framework.static": + guard !isStaticFramework else { + generateStaticFrameworkCode(builder, kit) + break + } generateStrings(builder, kit) generateFrameworkCode(builder, kit) case "com.apple.product-type.library.static": diff --git a/Sources/BazelizeKit/Codegen/Language/Codegen+ObjcLibrary.swift b/Sources/BazelizeKit/Codegen/Language/Codegen+ObjcLibrary.swift index 80699ef..3918bfc 100644 --- a/Sources/BazelizeKit/Codegen/Language/Codegen+ObjcLibrary.swift +++ b/Sources/BazelizeKit/Codegen/Language/Codegen+ObjcLibrary.swift @@ -9,8 +9,6 @@ import BazelRules import Foundation import Starlark -// TODO: https://github.com/XCodeBazelize/Bazelize/issues/7 - extension Target { func generateObjcLibrary(_ builder: CodeBuilder, _ kit: Kit, aliasPublic: Bool = true) { let project = kit.project diff --git a/Sources/BazelizeKit/Kit.swift b/Sources/BazelizeKit/Kit.swift index 8ffce5d..0aefd6d 100644 --- a/Sources/BazelizeKit/Kit.swift +++ b/Sources/BazelizeKit/Kit.swift @@ -233,8 +233,6 @@ extension Kit { /// {WORKSPACE}/BUILD private final func generateBuild() throws { build.setup(config: project.config) - -// build.exportUncategorizedFiles(self) for plugin in builtinPlugins { plugin.build(build.builder) } diff --git a/Sources/BazelizeKit/Plugin/Plugin+XcodeProj.swift b/Sources/BazelizeKit/Plugin/Plugin+XcodeProj.swift index 5c360a3..7c5269b 100644 --- a/Sources/BazelizeKit/Plugin/Plugin+XcodeProj.swift +++ b/Sources/BazelizeKit/Plugin/Plugin+XcodeProj.swift @@ -6,7 +6,7 @@ // import Foundation -import XcodeProj +import Starlark // MARK: - PluginXcodeProj @@ -19,40 +19,33 @@ final class PluginXcodeProj: PluginBuiltin { version: dep.rawValue) } - // TODO: - /// top target - /// custom project_name - /// not support swift_library -> static library - /// target_environments `device` need provision_profile + /// Every target that generated a rule, under the project's own name. + /// + /// A label that names nothing fails analysis of the whole rule, so a target + /// without sources — which generates no rule — is left out. Each one is a + /// top level target at its default environment: building for a device needs a + /// provisioning profile, which no Xcode project hands over. override func build(_ builder: CodeBuilder) { - let targets = kit.project.targets - let other = targets + let labels = kit.project.targets + .filter(\.hasSources) .map(\.name) .sorted() .map { name in - """ - "//Targets/\(name):\(name)", - """ - }.withNewLine.indent(2) + Starlark.Label.named("//Targets/\(name):\(name)") + } + + guard !labels.isEmpty else { return } builder.load( module: "@rules_xcodeproj//xcodeproj:defs.bzl", - symbols: ["top_level_target", "xcodeproj"]) + symbols: ["xcodeproj"]) - builder.custom(""" - # Xcode - xcodeproj( - name = "xcodeproj", - # Custom Project Name - project_name = "App", - tags = ["manual"], - top_level_targets = [ - # main target, maybe some `ios_application` - top_level_target(":App", target_environments = ["device", "simulator"]), - # all other target - \(other) - ], - ) - """) + builder.call( + Starlark.Statement.Call("xcodeproj") { + "name" => "xcodeproj" + "project_name" => kit.project.name + "tags" => ["manual"] + "top_level_targets" => labels + }) } } diff --git a/Sources/Xcode/Model/Config/Xcode+BuildSettings.swift b/Sources/Xcode/Model/Config/Xcode+BuildSettings.swift index 79b9d3d..8ba2256 100644 --- a/Sources/Xcode/Model/Config/Xcode+BuildSettings.swift +++ b/Sources/Xcode/Model/Config/Xcode+BuildSettings.swift @@ -177,6 +177,10 @@ extension Xcode.BuildSettings { public var testHost: String? { self["TEST_HOST"] } public var bundleLoader: String? { self["BUNDLE_LOADER"] } public var enableModules: Bool { self["CLANG_ENABLE_MODULES"] == "YES" } + + /// `MACH_O_TYPE`: how a target's objects are linked — `staticlib` makes a + /// framework target a static one, which has no bundle to load at runtime. + public var machOType: String? { self["MACH_O_TYPE"] } } extension StringProtocol { diff --git a/Tests/XcodeTests/RoadmapTreeBuilderTests.swift b/Tests/XcodeTests/RoadmapTreeBuilderTests.swift index 2c1598c..f620acb 100644 --- a/Tests/XcodeTests/RoadmapTreeBuilderTests.swift +++ b/Tests/XcodeTests/RoadmapTreeBuilderTests.swift @@ -77,6 +77,24 @@ struct RoadmapTreeBuilderTests { #expect(static2Build.contains("objc_library(")) #expect(static2Build.contains("name = \"Static2_objc\"")) + /// A static framework has no bundle to load: its label is the library, and + /// whatever links it gets the objects. + let staticFrameworkBuild = try String(contentsOfFile: (output + "Targets/StaticFramework1/BUILD").string) + #expect(staticFrameworkBuild.contains("swift_library(")) + #expect(staticFrameworkBuild.contains("alias(")) + #expect(staticFrameworkBuild.contains("name = \"StaticFramework1\"")) + #expect(staticFrameworkBuild.contains("actual = \"StaticFramework1_library\"")) + #expect(!staticFrameworkBuild.contains("ios_framework(")) + #expect(exampleBuild.contains("//Targets/StaticFramework1:StaticFramework1_library")) + + /// Every label `xcodeproj` names has to exist, and the project it writes is + /// the one that was read. + let rootBuild = try String(contentsOfFile: (output + "BUILD").string) + #expect(rootBuild.contains("xcodeproj(")) + #expect(rootBuild.contains("project_name = \"Example\"")) + #expect(rootBuild.contains("//Targets/Example:Example")) + #expect(!rootBuild.contains("top_level_target(")) + let prebuiltBuild = try String(contentsOfFile: (output + "Prebuilt/BUILD").string) #expect(prebuiltBuild.contains("apple_dynamic_xcframework_import(")) #expect(prebuiltBuild.contains("name = \"SVProgressHUD\"")) diff --git a/fixture/iOS/Project.swift b/fixture/iOS/Project.swift index 6811cb1..0c0d3f6 100644 --- a/fixture/iOS/Project.swift +++ b/fixture/iOS/Project.swift @@ -59,6 +59,10 @@ let project = Project( product: .staticLibrary, sources: ["Static2/**/*.m"], headers: .headers(public: ["Static2/**/*.h"])), + target( + "StaticFramework1", + product: .staticFramework, + sources: ["StaticFramework1/**/*.swift"]), target( "Framework3", product: .framework, @@ -86,6 +90,7 @@ let project = Project( .target(name: "Framework1"), .target(name: "Framework3"), .target(name: "Static2"), + .target(name: "StaticFramework1"), .package(product: "AnyCodable"), .package(product: "LocalLib1"), .package(product: "LocalLib2"), diff --git a/fixture/iOS/StaticFramework1/Test.swift b/fixture/iOS/StaticFramework1/Test.swift new file mode 100644 index 0000000..f5ee515 --- /dev/null +++ b/fixture/iOS/StaticFramework1/Test.swift @@ -0,0 +1,14 @@ +// +// Test.swift +// StaticFramework1 +// +// Created by Yume on 2026/9/23. +// + +import Foundation + +public enum StaticFramework1 { + public static func test() -> String { + "StaticFramework1+Swift" + } +} From bba4043e6b1fb56fffdfc8a797feb37c9e5ca2ee Mon Sep 17 00:00:00 2001 From: yume190 Date: Wed, 23 Sep 2026 16:19:55 +0800 Subject: [PATCH 47/47] ci(spm): skip a package with nothing to build, name the build system that compiles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two lanes were red on what SwiftPM does, before bazelize was involved. Marking is a package that is only a plugin, which SwiftPM refuses to build rather than building nothing: 'The package does not contain a buildable target'. The control step asks the manifest what is in the package and skips the ones with no non-plugin target. TargetResource asserts that a versioned data model is compiled, and under '--build-system native' nothing is: no momc, no actool, no ibtool, the bundle carries Model.xcdatamodeld as it was written. Which build system SwiftPM defaults to moved with the toolchain — a runner's default is native, a current Xcode's is swiftbuild — so the lane names the one the package is about, the way TargetEmbed names the other. --- .github/workflows/swift.yml | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index d48f14c..101f0fd 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -367,14 +367,18 @@ jobs: - name: SwiftSettings - name: SystemLibrary - name: TargetEmbed - # `.embedInCode` generates nothing under the default build system - # in this toolchain, which is SwiftPM's own gap rather than - # anything this package does — and the only reason this rule has a - # package of its own. + # `.embedInCode` generates nothing under `swiftbuild`, which is + # SwiftPM's own gap rather than anything this package does — and the + # only reason this rule has a package of its own. swiftpm: --build-system native - name: TargetExclude - name: TargetPath - name: TargetResource + # `native` copies a platform resource rather than compiling it: no + # `momc`, no `actool`, no `ibtool`. Which build system is the + # default moved with the toolchain, so the one that compiles what + # this package is about is named. + swiftpm: --build-system swiftbuild - name: TargetSources - name: Trait traits: Fast,Slow @@ -412,6 +416,9 @@ jobs: # one that stopped building would be found here rather than in whatever # it broke. A package with no tests is built and not run — there is # nothing to run — and `App/` is generated, not a package of the fixture. + # A package that is only a plugin has nothing to build either: SwiftPM + # refuses the build rather than doing nothing, so it is skipped by what + # its manifest declares. - name: SwiftPM Build And Test working-directory: spm/${{ matrix.name }} env: @@ -422,7 +429,10 @@ jobs: while read -r manifest; do package=$(dirname "$manifest") echo "::group::$package" - if ! (cd "$package" && swift build $ARGS); then + if ! (cd "$package" && swift package dump-package | + jq -e '[.targets[] | select(.type != "plugin")] | length > 0' > /dev/null); then + echo "only plugins here, nothing to build" + elif ! (cd "$package" && swift build $ARGS); then echo "::error::swift build failed for $package" status=1 elif [ -d "$package/Tests" ] && ! (cd "$package" && swift test $ARGS); then