Conversation
…kage
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=<name>` 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.
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=<Package>.<Trait>` turns one on and `<Package>.<Trait>-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.
SwiftPM compiles a package with `-D<Trait>` 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=<Package>.<Trait>` 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.
yume190
marked this pull request as draft
September 21, 2026 07:11
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.
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.
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.
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.
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.
A lane per package in spm/, and each trait selected through the config the workspace generated for it rather than by flipping one flag.
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.
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.
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.
Only .build/checkouts was read, and a registry package is not a checkout: SwiftPM unpacks it under registry/downloads/<scope>/<name>/<version>, 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.
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.
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.
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.
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.
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.
…ackage 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.
`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.
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.
…ay do 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.
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.
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.
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.
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.
`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.
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.<code>` 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.