Skip to content

Development - #36

Merged
yume190 merged 174 commits into
masterfrom
Development
Sep 20, 2026
Merged

yume190 merged 174 commits into
masterfrom
Development

Conversation

@yume190

@yume190 yume190 commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

yume190 added 30 commits April 11, 2026 15:17
# Conflicts:
#	.gitignore
`.copy` keeps an item's own name and inner structure and nothing above it, while
a structured resource keeps its whole path: GRDB's `Betty.jpeg` ended up at
`Sources/GRDBTests/GRDBTests/Betty.jpeg` inside the bundle, and the test asking
the bundle for `Betty.jpeg` found nothing.

Each directory a copied item sits in becomes a resource group that strips the
path above the item. A group and a glob cannot be added together in one
attribute, so once there is a group the processed resources become one too.
A `swift_test` never bundles resources: `apple_resource_bundle` is something a
bundling rule consumes, so `Bundle.module` had nothing to find and GRDB's tests
died on the first resource they asked for. A test target is now a library plus
the macOS test bundle over it, at the version the package asks of macOS.
The generated manifest's `path:` dependencies were read back out of it with a
regular expression, to find the local packages whose sources are read in place.
The plugin that writes that manifest already has them, so they are handed to the
loader instead: one function and one failure mode fewer, and a local package can
no longer go missing because the manifest could not be read.
A package in the project's own repository has its build tool plugins run while
the workspace is generated, and the run said they were not: the report covered
every package, and a plugin target was listed as an unsupported kind on top of
that. Only a dependency's plugins go unrun, so only those are named.

A plugin that was meant to run and could not is named as well, through the
notes a run prints rather than the unified log nothing shows: what the target
loses is whatever the plugin generates, and the compile error that follows names
those files instead of the plugin.

A plugin target is never a rule this workspace builds, whatever its capability,
so the distinction between the two kinds no longer decides anything and is gone.
Only the Swift files a plugin wrote were taken, so a plugin that generates C
sources, a header for them, or a resource wrote into a target that never saw
them. SwiftPM splits what a plugin produced by the target's own file rules, into
its sources and its resources, and that split is now mirrored:

- an extension the target compiles goes into its `srcs`;
- a header is neither compiled nor bundled. It is an input of the generated
  source beside it, which includes it by name — the only thing SwiftPM offers
  either, since a hand-written source cannot reach a generated header;
- everything else is a resource, so a target whose only resources come from a
  plugin gets a bundle, as it does under SwiftPM.
The corpus's plugins are all linters and TbCodeGenerater's writes Swift, so
nothing proved a plugin's other outputs reach the target that asked for it. The
fixture's local package now has a build tool plugin whose tool — the package's
own executable target — writes a Swift file and a resource for a Swift target,
and a C source with its header for the C-family one. Both targets use what it
wrote, so the app fails to build if any of the three kinds is dropped.

The header is declared rather than included by the hand-written source: SwiftPM
puts a generated header on no search path of one, which is what the rules
mirror.
The SwiftPM notes now say which of a plugin's files become sources, which is an
include of the generated source beside it, and which are bundled, plus that a
plugin which could not run is named. Two decisions are recorded rather than left
open: `Package.swift` and `Package.resolved` stay because `.build/checkouts` is
where a rule's sources live and only those two rebuild it, and every SwiftPM
step stays a toolchain command instead of libSwiftPM — measured at 0.6s per
manifest, and slower when run concurrently.
A project references Xcode's own settings as freely as its own, and nobody can
type them in: iina writes `$(SDK_VERSION)` and `$(XCODE_VERSION_ACTUAL)` into
its `Info.plist`, and neither appears anywhere in the project file, so both keys
were dropped for carrying a reference that could not be resolved.

They are read from the installed toolchain instead, as defaults a project that
states one of them itself still wins over: `xcodebuild -showsdks` answers for
every platform's SDK name and version at once, and `xcodebuild -version` for the
four digit number Xcode spells itself as. `CONFIGURATION` needs no asking — it
is the name of the configuration being generated.

`xcodebuild -showBuildSettings` knows all of them without a table, but it
resolves the package graph and takes some ten seconds per target; these two
commands take under a second for a whole run.

The platform is the one a target resolves to rather than whatever `SDKROOT`
says, because that setting is optional and `auto` names no SDK.
No stage implements `.package(id:)`: nothing in the corpus uses one, and the
work is recognising one more kind of dependency rather than changing the shape of
the output, so the open question becomes a stated non-goal with what happens
until then. The note about substituting `$(SETTING)` in a plist says where that
landed instead.
Every file found under a target's plugin output directory was linked into one
flat directory by its name alone, and everything found there was taken as the
target's own. Neither holds: a plugin has a directory of its own under that root
and writes a tree inside it if it likes, and SwiftPM leaves the files a previous
build declared in place rather than deleting them — so a run picked up a file no
plugin produces any more and bundled it.

The output directory of a target is therefore removed before SwiftPM is asked to
build it, and what it then contains is linked keeping its path. The fixture's
plugin writes its resource into a subdirectory to hold that.
Two modules read the same `.xcodeproj`: `XCode`, which nothing but the plugin
loader still imported, and `XCode2`, which everything that generates a workspace
is written against. The first one is gone, and the second holds the name —
spelled the way Apple spells the product, so the module, its types, its files
and the `bazelize xcode` subcommand all read `Xcode`.

Removing it takes libSwiftPM with it: `SwiftPMDataModel` was linked for one
function in the old parser, and it pulled SwiftDriver, llbuild, swift-crypto and
the rest behind it. The package resolves 9 dependencies now instead of 18, and
no longer pins a branch to match the toolchain — which is what the SwiftPM notes
said the alternative to the `swift` command would cost.

`XCodeBazelize` and the `XCodeProj` repo enum keep their spelling: one is the
organisation, the other names generated files.
`Cocoapod` was not a target of this package, so nothing built it and nothing
could: it read the Xcode parser that is gone. Its tests, the `--skip
CocoapodTests` the test target still carried, the three `pod` steps in CI and
`Env.pod` — which no code read — go with it.

Everything that spelled the product `XCode` now spells it `Xcode`: the
`XcodeProj` repo enum and the plugin named after it, the file they live in, and
the prose in the docs. `XCodeBazelize` keeps its capitals, being the name of the
organisation this repository lives in rather than a word about Xcode.
`swift package resolve` was run in the output directory whatever the project
was, and a project with no Swift packages has no manifest written there for it
to read: the run ended on `Could not find Package.swift`, after the rules were
already written, so the workspace looked generated and was not. stats is such a
project.
A file whose path does not start with the project's is dropped to its name
alone, and two spellings of one path do not compare equal: `/tmp` is a link to
`/private/tmp`, and a file resolved through XcodeProj keeps the link where the
root has already lost it. Every such file then globbed as
`Sources/<name>/**` — matching nothing — and the package failed to load with
`glob pattern didn't match anything`.

Both sides are resolved with `realpath` before the comparison, which is what
the SwiftPM plugin already does for the same reason.
The corpus is not in this repository — `app/` and `spm/` are ignored — so CI
never touched what the notes measure. Two lanes now do, both downloading the
`bazelize` artifact the release build already uploads rather than building it
again: one clones an app, generates its workspace and builds it with Bazel, the
other clones a package whose tests only compile through a source its own build
tool plugin generates.

Every entry pins the revision it was measured against, so upstream moving is a
deliberate bump rather than a red build nobody caused. Each lane keeps a Bazel
disk cache of its own, and the matrix does not fail fast: one app failing says
nothing about the others.
`swift build` had its standard error discarded, so a plugin that did not run was
reported as "swift build failed" and nothing else — which is every reason at
once: a toolchain that cannot build the target, a checkout it could not fetch,
a source that does not compile. The error lines SwiftPM printed are carried into
the note instead.

CI stops on that note rather than on what it causes: a target missing the
sources a plugin generates fails to compile several steps later, naming those
sources and never the plugin.
Two things the runner's Swift 6.3 does that 6.4 does not, both of which end as
"cannot find X in scope" several steps after the cause:

- a build tool plugin's tool is looked up as a product, so an executable target
  without a product of the same name fails the whole package with "no product
  named". The fixture's `local1-tool` product is named `Local1Tool` now.
- a package with no `platforms:` builds its macro for the oldest macOS SwiftPM
  supports, which is older than swift-syntax declares. The fixture states
  macOS 10.15.

6.3 also does not run a build tool plugin for a C-family target at all — a
clean build produces the Swift target's generated sources and nothing for the
clang one — so the fixture no longer calls into what the plugin generates for
`LocalTarget3`. The rules still compile it wherever the toolchain produces it.

iina and MacPass leave the CI matrix commented out until they are green on a
runner rather than only here.
iina waits on the dylib-as-source problem the notes already describe, MacPass on
a Carthage step nothing has proven on a runner, and TbCodeGenerater on a product
rename in its own repository. The three differences between the runner's Swift
6.3 and the 6.4 used here are written down beside them, because each one ends as
a missing symbol rather than as anything about a plugin.
Asking SwiftPM to run them meant building the target they are attached to,
which fails for reasons that have nothing to do with the plugin — and on Swift
6.3 silently does not run a plugin for a C-family target at all, reporting
success and producing nothing. bazelize is the host now: it compiles the plugin
against the toolchain's `PackagePlugin`, hands it the package graph and the
directory its output belongs in, and runs the commands it asks for.

The protocol is SwiftPM's: a length-prefixed JSON message each way over the
plugin's standard input and output. Only what a build tool plugin uses is
mirrored, and a message that cannot be decoded is reported as the toolchain
speaking a protocol this does not, rather than read as an absent command.

What this buys, measured on both toolchains: the fixture's C-family target gets
its generated sources on Swift 6.3, which `swift build` never produced there.
The files are written where they belong rather than linked out of `.build`, so
the generated workspace no longer needs the package's build directory to exist.

The tool a plugin runs is still built by SwiftPM — it is an executable target
with ordinary dependencies — but only that one product, looked up in the
manifest rather than guessed from the target's name.
What a build tool plugin writes is the plugin's to decide, so changing the
plugin changes the files a target compiles while nothing else about the project
moves — and regenerating the whole workspace to pick that up is the wrong unit
of work. The generated workspace now carries the command instead:

    bazel run //:plugins

It runs `bazelize plugins`, which compiles each plugin, hands it the package
graph and the directory its output belongs in, and writes what comes back into
`Packages/*/Generated/*Plugin` — the rules are not touched.

They do not need touching, because they no longer name a plugin's files: the
rules glob the directory by the kinds of file found in it. A plugin that starts
writing `Generated2.swift` instead of `Generated.swift` needs `bazel run
//:plugins` and nothing else. Only the kinds that are there are globbed, so an
empty directory fails the package loudly rather than compiling without sources.

`sh_binary` comes from rules_shell, which Bazel no longer ships itself.
The guard that stops a lane when a plugin did not run matched the old wording,
which no longer exists: a run now names the plugin it could not run rather than
saying a target's plugins failed as a group.

The package lane is commented out with the reason it cannot pass yet — Swift
6.3 looks a plugin's tool up as a product, and TbCodeGenerater's product is
named `tbCodeGenerater` while its plugin names the target `TbCodeGenerater`, so
the package does not build on a runner at all.
Each lane kept a Bazel disk cache and nothing else, so every run fetched
rules_apple, rules_swift and the rest again, and resolved the app's Swift
packages from scratch before a rule was written. Three caches now, written into
`~/.bazelrc` so every `bazel` in the job reads them: the disk cache for what a
build produced, the repository cache for what it downloaded, and the output
directory's `.build` for what SwiftPM resolved.

The fixture lane was caching a directory nothing writes, keyed on a `WORKSPACE`
file that no longer exists; it caches the same three things now.

`swift_deps.bzl` and `swift_deps_index.json` go with them: rspm generated those
into the fixture and nothing has read them since it was removed.
bazelize used to generate into the fixture directory itself, and the leftovers
were still there: a `BUILD` in every target's source directory, a `Package.swift`
and its resolution at the fixture root, a `.bazelrc` importing a `config.bazelrc`
that no longer exists, a `.bazelversion` pinning a Bazel the workspace stopped
using, and 450MB of `cache/` and `.build/` from builds run out of that layout.
The workspace is `App/` now, generated whole by `make bazelize`, and it carries
its own `.bazelrc` and `.bazelversion`.

The ignore rules followed the old layout too, naming each file it generated
into the fixture. One directory is ignored now — and explicitly: `App/` was
only being ignored because a rule meant for the corpus directory `app/` matches
it on a case-insensitive filesystem.
…time

The runner ran four simulator tests at once against a simulator that did not
exist yet, so each paid to create and boot its own: the first test took 247s,
the second 152s, and the last timed out at 302s having spent all of it waiting.
None of that is the tests — locally the same four take four seconds against a
simulator that is already there.

The lane creates and boots the device up front, outside any test's clock, and
runs the tests serially so they share it — rules_apple's runner reuses a
simulator of the right type and version rather than making another. The timeout
is raised to 900s and failures print their log, so the next one of these says
what happened instead of only that it took too long.
Two ways the last run failed, neither of them the code under test:

The simulator step named a runtime this repository is developed against —
`iOS-27-0` — and the runner ships whatever its own Xcode has, so `simctl`
answered `Invalid runtime` and the step took the job down with it. The runner's
newest available iOS runtime and an iPhone on it are looked up instead, created
only if there is none, and the device and version are passed to `bazel test`
rather than written into it. A device name has spaces (`iPhone Air`), so the
three fields come back one per line.

The app lane restored `<app>/App/.build` before cloning the app, and `git
clone` refuses a directory that exists. The cache is restored after the clone.
A plugin was compiled by bazelize and its tool built by SwiftPM, which is the
one thing left that could fail for reasons having nothing to do with the
plugin: a toolchain that looks a plugin's tool up as a product cannot even load
the package the tool lives in, so nothing about that package can be generated
with its plugins run.

Both are Bazel targets now. A plugin is a `swift_binary` with the toolchain's
`PackagePlugin` on its search path — it links nothing else — and the tool
already was one. `//:plugins` carries them as `data`, so `bazel run //:plugins`
builds them and hands their paths to the host, which speaks the protocol and
runs what the plugin asks for. No second Bazel inside the first one's lock, and
no SwiftPM in the plugin path at all.

The rules glob a target's plugin directory whenever the target declares a
plugin, before anything has written there, so `allow_empty` is set: a package
that cannot load is a package that cannot run `//:plugins` to fill it.

Measured with the plugin directory, `.bazelize` and the package's `.build`
deleted: `bazel run //:plugins` puts all four of the fixture's generated files
back without compiling a plugin or building a tool itself, and does the same
for TbCodeGenerater. The lane for that package is a lane again.
The fixture is an Xcode project, and Xcode's build system does not run a build
tool plugin for a C-family target: `LocalTarget3` called a symbol from the
source `Local1Gen` generates for it, so opening the fixture in Xcode ended in
`Undefined symbol: _local1_plugin_value`. `Local1Macros` and `Local1Tool` go
with it — the package is back to the three plain targets it had.

What those covered is covered by a package that is not an Xcode project:
`spm/TbCodeGenerater`, whose tests only compile through a source its own build
tool plugin generates, and whose plugin and tool are built by Bazel. The SwiftPM
notes said the fixture stood for macros and generated sources; they name that
package now.
`//:plugins` is written into the root `BUILD` for any project that has Swift
packages — whether one of those packages has a build tool plugin is not known
when that file is written — but the `//Packages:plugins` it names was only
written when there was a plugin to put in it. A project with packages and no
plugins therefore had a label that does not resolve, and `bazel build //...`
failed to analyse before it built anything. Rectangle is such a project.

The group and the script are written for any project with packages now, empty
when there is nothing to run: a command that does nothing beats a workspace
that does not load.
@yume190
yume190 merged commit e1a35a1 into master Sep 20, 2026
9 checks passed
@yume190
yume190 deleted the Development branch September 20, 2026 11:07
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