A few weeks ago, Apple released new versions of Xcode and Command Line Tools. Not thinking too hard about how my pkgsrc developer environment often gets broken by OS or tool updates, I updated promptly. For one thing, I’m kinda used to it. For another, it doesn’t usually break. For a third thing, managing dependencies — anything not my code that can break my code — means responsibility for dealing with the inevitable trouble, and therefore the sooner I find it the better. (More on my approach to life with dependencies.)
A vendor-provided toolchain is a significant dependency.
So I accepted the Command Line Tools update, and it commandeered my spare time for two weeks as I hurried carefully to repair
one of the world’s biggest continuous-integration cauldrons
on
one of its most popular platforms.
When I ran my usual pkg_rolling-replace -suv
to rebuild anything outdated, it did not go well at all.
This article uses “we” because the continued smooth operation of pkgsrc on macOS reflects the contributions of many developers on many occasions, including this one: I happened to be first on the scene, but several of us of were discussing the problems and potential workarounds and all of “my” commits were adjusted accordingly.
Did I mention that a few weeks ago we were aiming to stabilize for yesterday’s quarterly release? Suddenly, if we didn’t scramble to straighten things out for macOS users, we’d have to manage a complicated situation for a while. But if we created a mess on other platforms by moving rashly, that’d be even worse.
The usual feedback mechanism for proposed infrastructure changes is to compare full bulk builds before and after. There was no time for that.
Happily, the conclusion of the story is boring: as always, the pkgsrc 2024Q1 stable branch supports macOS and its developer tools, including the latest releases of each. (So does -current pkgsrc, of course, if that’s your thing.)
Curious what we had to do to keep it boring? Read on.
Stricter clang
defaults
Upstream Clang 16 and GCC 14 have promoted several warnings to errors by default, and Apple’s Clang 15 has followed suit. (Gentoo has very helpfully documented this for packagers.) These changes are intentional and well-intentioned, pushing maintainers to ship more reliable code. But pkgsrc’s job is to build nearly 30,000 codebases we don’t maintain. And stricter compiler defaults break a lot of builds.
As you might hope, we can make the breakage go away in one place.
In pkgsrc, packages declare which programming languages are required for their build. The compiler framework then selects package-and-platform-appropriate compilers, places them preferentially in the package’s build environment, and — crucially — intercepts compiler invocations and rewrites them for a variety of purposes.
When we look into pkgsrc’s clang
logic, we find prior art for this specific class of problem.
In September 2020, Xcode 12 (and its associated Command Line Tools) arrived even later in our quarterly schedule and promoted -Wimplicit-function-declaration
to an error.
The surgical fix: on macOS only, if invoking clang
reveals the new stricter default, we
pass -Wno-error=implicit-function-declaration
to demote the error back to a warning.
Apple Clang 15’s new strictures aren’t observable in the same way, so we adjust our workaround:
if clang
doesn’t complain when we try demoting the new errors back to warnings, we
pass those arguments too,
via the same compiler-framework mechanism.
Missing m4
and yacc
This messy regression found only in the Command Line Tools 15.3.0.0.1.1708646388 update — not in the corresponding full Xcode 15.3 (build 15E204a) update — must have been unintended.
On macOS, some of the familiar Unix tools in /usr/bin
are in fact stubs.
When invoked, they either execute into the corresponding installed program (living somewhere under /Library/Developer
) or prompt the user to install the Command Line Tools.
This Command Line Tools update uninstalls m4
and yacc
from /Library/Developer
.
But since the OS-provided /usr/bin/m4
and /usr/bin/yacc
stubs still exist, running m4
or yacc
still does something:
it pops up a window prompting you to reinstall the CLT.
Unfortunately, as the latest available version doesn’t provide those tools, reinstalling is a waste of time.
As you might once again hope, we can hide the problem without personally visiting 29,000+ packages.
In pkgsrc, we also have a framework to control which non-compiler tools are invoked during builds. Packages declare which tools are required for their build. The tools framework then selects package-and-platform-appropriate incarnations of the declared tools and places them preferentially in the package’s build environment.
We just got handed a few new twists to handle in the framework, is all.
First, because this clever new CLT failure mode outfoxes our usual tool-detection mechanism, we special-case m4
and yacc
detection on macOS, performing an
existence check for the stubs’ targets.
Then the selection mechanism’s usual fallback logic can provide them some other way.
This prevents the primary source of needless CLT install popups.
For non-macOS platforms, no change.
Second, because some packages might not yet be declaring all their tool dependencies, we special-case m4
and yacc
handling on macOS:
when they’re not declared, we
place them in the build environment anyway,
as no-ops.
If the package build happens to invoke them, nothing happens.
This prevents the secondary source of needless CLT install popups, at the risk of breaking macOS builds for packages that are missing these tool declarations and have heretofore gotten lucky;
in such cases, the breakage will be obvious and the fix easy.
For non-macOS platforms, no change.
(At leisure, we might like to broaden this approach to help find and fix all undeclared tools on all platforms.)
Third, because the flex
tool expects to invoke a GNU-compatible m4
, we adjust the tools framework to
infer gm4
from a flex
declaration
so that the framework controls which m4
gets found.
This more correctly expresses our intent on all platforms, and in the macOS package build environment it restores /usr/bin/flex
to a working state.
Broader xcrun
search
We were already relying on xcrun
for a couple of things, so when our new tool-detection special cases were sometimes getting surprising results from it, that was concerning.
Turns out xcrun
no longer looks solely in Apple-controlled locations, but also consults the environment’s $PATH
.
By
invoking xcrun
with an empty PATH
and --no-cache
,
we obtain controlled, predictable tool detection.
Conclusion
Under the constraints, we changed as little as possible, as safely as possible, as similarly as possible to previous proven changes, avoiding novel constructs or any whiff of unforeseen consequences. We could not have done nearly as safe or thorough a job without good abstractions already in place. Total lines of pkgsrc infrastructure code changed: less than 100. Now that 2024Q1 is out, we have room to refactor.
These 15.3 updates also include a brand new linker. So far it hasn’t given us any trouble. If that changes, wanna guess whether we have one place to take care of it?