Skip to content

Feature spm fixtures - #37

Draft
yume190 wants to merge 32 commits into
masterfrom
feature-spm-fixtures
Draft

yume190 wants to merge 32 commits into
masterfrom
feature-spm-fixtures

Conversation

@yume190

@yume190 yume190 commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

…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
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant