Framework Changes¶
A curated log of notable changes to the spksrc build framework — the mk/
tree and the conventions every package Makefile relies on — newest first.
This is not an exhaustive changelog (for that, see git log -- mk/). Each
entry is collapsed to its date and title; expand it (▸) for what changed, why,
and a link to the pull request. Efforts that span several pull requests are a
single entry with the individual PRs nested inside.
Highlights — the short version¶
If you only read one thing, read this. The details are in the dated log below.
-
make helpknows your package. Run it inside any package directory for a context-aware list of the targets and variables that actually apply there: -
Ask an architecture what stands in the way.
make check-x86-5.2(ormake ARCH=x86 TCVERSION=5.2 check) walks the whole dependency tree and lists every capability gate that architecture fails, or says every gate is met:===> tvheadend: x86-5.2 check: 17 failed, 14 more behind an optional dependency required cross/ffmpeg8 gcc 4.7.3 < 4.9 cross/python314 gcc 4.7.3 < 4.8 ... optional cross/frei0r gcc 4.7.3 < 7.5 ...required is what the build needs; optional is what an
OPTIONAL_DEPENDSbranch would demand if turned on, and never a reason to refuse. A clear architecture answersx86-5.2 check: OK.The pre-check uses the same walk, so a refused build names every blocker at once instead of stopping at the first. See Architecture Support.
-
Take a dependency where it can be built, without copying its conditions.
$(call depend,cross/vvenc,--enable-libvvenc)adds vvenc and its switch wherever vvenc's own floors are met, and leaves both out elsewhere;$(call depend,cross/x)is a plainDEPENDS. A package like ffmpeg no longer repeats, in anifeqper library, the floors each library already declares. See Macros. -
Ask the toolchain for a tool.
$(call tc,gcc),$(call tc,ar)— the absolute path of a cross tool, following whichever overlay provides it. Never write$(TC_PATH)$(TC_PREFIX)gcc: it silently resolves to the vendor compiler as soon as an overlay is active.$(call native,gcc)is the same for the build host. See Macros. -
Declare what a package needs, not where it fails. Instead of hand-maintaining an
UNSUPPORTED_ARCHSlist, state the capability floor:MIN_GCC_VERSION,MIN_GLIBC_VERSION,MIN_RUSTC_VERSION,REQUIRE_64BIT. The framework refuses exactly the architectures whose toolchain cannot meet it, with a human-readable reason, and the gate stays correct on its own as toolchains move. See Architecture Support. -
Every build system uses the same variable names now. CMake no longer has its own
CMAKE_ARGS: pass configure options throughCONFIGURE_ARGSfor autotools, CMake and Meson alike. Compile and install options are likewise unified onCOMPILE_ARGSandINSTALL_ARGS(the oldCOMPILE_MAKE_OPTIONS/INSTALL_MAKE_OPTIONSare gone). See Build System Selection and Compile and Install Arguments. -
A build directory is one variable.
CMAKE_BUILD_DIR/MESON_BUILD_DIR/NINJA_BUILD_DIRare unified onBUILD_DIR, which also opts an autotools package into an out-of-tree build. -
Flaky download host? Add a mirror. Set
PKG_DIST_MIRRORSto one or more fallback base URLs; each is tried in turn and still checked againstdigests. See Source downloads and mirrors. -
include ../../mk/spksrc.common.mkbefore any macro call. Theversion_*macros (and friends) are only defined oncespksrc.common.mkis included, so a Makefile must include it before the firstversion_ge,version_lt, … it uses. -
Turning a package off? Prefer
DISABLEDoverBROKEN. ADISABLEDfile in a package folder skips it exactly likeBROKEN, but reads as an intentional choice rather than a failure. Put the reason in the file. See the package lifecycle guide. -
The target ABI reaches every language now. A toolchain declares its ABI once in
TC_EXTRA_BUILD_FLAGS; the framework folds it into everyTC_EXTRA_<LANG>FLAGSand the link, so C, C++, Fortran and the linker all agree on the ABI. Link-time libraries (-lrt,-latomic) live inTC_EXTRA_LDFLAGSand are passed plainly, with-latomicauto-dropped where the gcc lacks it. They are not wrapped in--as-needed:LDFLAGSprecedes the objects, so a front-placed library there resolves nothing yet and the wrap discarded it outright. See Extra flags a toolchain can declare. -
Build a host tool once, host it, reuse it. A native package hosts its build output as a release archive with a single line —
ARCHIVE_NAME— so an expensive tool (llvm, the gcc-8.5 overlays) is built once and pulled in viaDEPENDSinstead of rebuilt from source.make nativecleandrops a native package's build cookies to re-run it from scratch, the native counterpart ofspkclean. -
One build tree, one set of
tc_vars. The toolchain directory no longer holds generated variables at all; each build writes its own under its work directory, and a dependency reads the tree that pulled it in. Two builds can run side by side against the same toolchain. See tc_vars Files. -
Rust builds on the legacy archs, and overlays are a first-class notion. The archs
rustuphas no usablerust-stdfor — PowerPC e500 (qoriq,ppc853x), ARMv588f6281,x86-5.2— now get a Rust toolchain built from source, published as an archive and pulled in like any other dependency. That fixes long-standing failures on those archs (SPE float, the ppc853x TLS relocations,AtomicU64). It also generalises: a component shipped beside a base toolchain is an overlay, switched withOVERLAY_RUSTC/OVERLAY_BINUTILSfromlocal.mk, andmake helpshows which are active for your arch.
October 13th 2026 — The Rust build environment is isolated, and DEFAULT_ENV splits in two (#7584)
-
A bare
CFLAGSis what cc-rs falls back to for both sides of a cross build.tc_vars.flags.mkonly records make variables;tc_autotools_varsis what putsCFLAGSinto the environment, and from there the target sysroot and-idirafterreached host build scripts -- thepsmcrate failing on old-gcc archs (#7573). -
tc_rust_varsnow names every variable cc-rs consults:CFLAGS_<triple>andCXXFLAGS_<triple>for the target,HOST_CC,HOST_CXX,HOST_ARand emptyHOST_CFLAGS/HOST_CXXFLAGSfor the host, beside theCC_/CXX_/AR_<triple>it already emitted. cc-rs prefers those over the bare value, so the autotoolsCFLAGSis never consulted there -- with nothing kept out of the environment.ADDITIONAL_C*FLAGSstay deferred, as intc_autotools_vars, so a package's own additions still reach cc-rs. -
DEFAULT_ENVheld two jobs, and that is what made #7577 breakable: narrowing the list to isolate the rust env also dropped a build-system marker, so librsvg lost its ninja install and pngquant3 its flags (reverted in #7581). They are separate now --BUILD_SYSTEMisautotools,cargo,cmake,mesonornone, whatinstall.mkandcompile.mkask about;TC_VARS_ENVlists whichtc_vars.<word>.mkto load. A cargo build loadsrustalone, reading the per-triple andHOST_*variables rather than the bareCCandCFLAGS;cross-rust.mkdeclared no build system at all before, so a pure Rust package fell through to the autotools default. -
A bare
CFLAGScannot be made safe by naming the host's. cc-rs adds it to whateverHOST_CFLAGSsays rather than choosing between them, so one in the environment puts the target ABI on a host compile whatever the host value -- measured onpsmat comcerto2k, which fails on-mfloat-abi=hardwithHOST_CFLAGSempty and with-O2alike. The isolation rests on the bare flags being absent from a cargo environment, which is why they stay autotools' own. A package that genuinely needs one names it in its ownENV:cross/nushelldoes, sinceaws-lc-sysbuilds BoringSSL through CMake, which reads the bareCFLAGSand nothing cc-rs offers, and without the sysroot includebcm.ccompiles against the host glibc and the link ends on undefined__isoc23_*. -
cross/fishandcross/helixstate their target flag throughADDITIONAL_CFLAGS. Both namedCFLAGS_<triple>themselves -- fish-std=c99for pcre2-sys under gcc < 5, helix-O3-- andtc_rust_varsnames it too, so whicheverENV +=ran last took the variable whole: fish includes the framework after its own line and lost the flag, helix includes it before and kept the flag while losing the sysroot includes.ADDITIONAL_CFLAGSis deferred intoCFLAGS_<triple>, so it concatenates and both arrive. helix also drops its hand-writtenCC_/CXX_/AR_/RANLIB_<triple>: they spelled the vendor path directly and so ignored a gcc overlay, where the framework's come from thetcmacro. -
noneholds wherever it is set.cross-virtual.mkandspk.mkset it for something that builds nothing, and eachenv-<system>.mkdeclines to append over it -- the guard has to be local, sinceenv-default.mkis last only for cross packages, not for a python spk whose metas come after it (spk-meta/python.mkreacheswheel.mk, which includesenv-cmake.mk). -
tc_rust_varsstates the Rust identity a package reads:RUST_TARGETandTC_RUSTCas before, nowTC_RUSTUP_TOOLCHAINbeside them, socross/netdataandcross/nushellcan callcargo +$(TC_RUSTUP_TOOLCHAIN)without a Rust mk file of their own -- that is whatinvalid toolchain name ''was. The fragment also carriesRANLIB_<triple>andHOST_RANLIB, which cc-rs reads through the same per-triple andHOST_*prefixes asAR, and the deterministicCARGO_TERM_*thatenv-rust.mkused to put in every environment that read it. -
The words
cmakeandmesonnever loaded anything. Their generated files aretc_vars.cmake,tc_vars.meson-crossandtc_vars.meson-native, with no.mksuffix, while the wildcard globstc_vars.<word>.mk. It matched nothing for those two, silently, which is exactly what hid the marker role -- the architecture page already said there was notc_vars.cmake.mkto pick up; only the variable name implied otherwise. -
Both are appended, never claimed with
?=, which cannot compose a list: the first file to run claimed the variable and silenced every later one, so the result depended on each package's include order.env-default.mk, always last throughspksrc.cross-cc.mk, supplies what nobody asked for.flagsstays autotools' companion; CMake and Meson read the flags through their own generated toolchain and cross file. -
Nothing is declared per package any more. The explicit
env-rust.mkincludes incross/librsvg-latestandcross/netdataare gone;cross/fishbuilds crates through Corrosion and declared nothing, and gets the rust tc_vars all the same.cross/zncis the only package that declared any of this and the only one the split simplifies -- three lines to one, since itsBUILD_SYSTEMis cmake and what it needed was the two fragmentsznc-buildmodreads. -
nonecarried both senses too, and the second was vestigial:cross-virtual.mkdoes includeinstall.mk, so the filter ran and setINSTALL_ARGSfor a package that installs nothing. It is nowBUILD_SYSTEM = nonewithTC_VARS_ENVempty. -
Note for packagers:
HOST_CC,HOST_CXXandHOST_ARnow reach every cross package's environment, sincerustis inTC_VARS_ENVfor all of them. For autotools that isHOST_CC's conventional meaning -- a compiler for the build host -- and it was previously unset. -
Documented: Toolchain Variable Files.
October 7th 2026 — A git download can include submodules, and name a tag (#7549)
-
git archiveleaves a submodule an empty directory, so a project whose release tarball bundles its submodules could not be built from git. znc is one: its GitHub archive has nothird_party/Csocket, and the hosts carrying its release tarball kept failing (znc.in's expired certificate, then a mirror that dropped it -- #7547). -
PKG_GIT_SUBMODULESlists the submodule paths to add. Each is archived at the commit the superproject pins atPKG_GIT_HASH, from the URL its.gitmodulesgives, and appended to the tarball;gzip -nkeeps it reproducible, so the digests hold. See Source downloads and mirrors. -
PKG_GIT_TAGnames the revision by tag instead ofPKG_GIT_HASH(one or the other), so a version bump isPKG_VERSand the digests. -
znc builds from git, at tag
znc-1.10.3, with Csocket and cctz. The release tarball's pregenerated modpython sources are not in git: the build host's SWIG generates them.
October 2nd 2026 — An optional dependency follows its own floors, not a copy of them (#7525)
-
ffmpeg repeated every library's floor.
ifeq ($(call version_ge,$(TC_GCC),7.5),1)aroundDEPENDS += cross/vvencand its--enable-libvvenc, the same 7.5 thatcross/vvencdeclares, then the package again in anOPTIONAL_DEPENDSlist -- five places to keep in step, in each of ffmpeg4 to ffmpeg8. A floor raised in the library and not in ffmpeg failed the whole build instead of dropping one feature. -
$(call depend,<pkgs>,<switches>[,<else>])(spksrc.common/macros.mk) declares the dependency optional, andspksrc.rules/depend.mkadds it toDEPENDSwith its switches wheredependency-unsupportedfinds its tree clear for the build, passing<else>where not.nopstands for no switch,|separates alternatives (first supported wins). With one argument it is a plainDEPENDS. -
Computed once per work directory, the candidates in parallel, into
depend-<package>.mk; later parses include that file. A dependency walk never computes verdicts, so the pre-check andmake checkwalk the required tree only, and every walk gets its own stamp directory. -
ffmpeg4 to ffmpeg8 and tvheadend use it for every library that comes with a switch, gated or not: a library that one day stops supporting an arch then drops out of the build with its switch, instead of refusing the whole package. The gates go: libjxl (highway's glibc floor comes with its tree), vvenc (its
UNSUPPORTED_ARCHS), libplacebo, liblc3, openh264, libaom, svt-av1, svt-hevc, x265... Where a condition lived only in ffmpeg, it moved into the library:MIN_GCC_VERSION7.5 in libvmaf_2.3, 4.8 in libvmaf_1.5 and librabbitmq. Still plainDEPENDS: what has no switch (cairo, flac, pngquant -- whose place first in the list matters). Stillifeq: tvheadend's ffmpeg choice (paired withspk/tvheadend) and the videodriver blocks. -
What changed in the builds: libaom joins ffmpeg5-8 on armv7 and qoriq (the arch list was ffmpeg's alone; ffmpeg4 already builds it there), and shine joins ffmpeg4 on 88f6281 and x86-5.2 (shine declares gcc 4.6, ffmpeg4 asked 4.8; built on 4.6.4).
-
Required alternatives:
$(call depend,cross/a|cross/b)with no switch picks the first version whose tree supports the build, else the last, which then refuses it -- a virtual package in one line. Resolved in dependency walks too. -
The order is the Makefile's: each call leaves a placeholder in
DEPENDSandCONFIGURE_ARGSwhere it stands, and the outcome takes its place -- as anifeqat that line would. A package that must be built first (tvheadend's pngquant) can use the macro too. -
make checkreports the outcome:optional in use/optional unused. -
Renamed to match the macro:
spksrc.rules/depend.mkresolves the calls; what buildsDEPENDSis nowspksrc.rules/dependency.mk(withspksrc.kernel/dependency.mk), its cookie.dependency_done(was.depend_done). Targets and hooks keep their names. -
Documented: How DEPENDS and OPTIONAL_DEPENDS are processed.
September 29th 2026 — CMake builds that compile Rust crates get their cross setup from the framework (#7520)
-
cargo runs at build time, the toolchain file only at configure time. Its
set(ENV{CARGO_TARGET_<T>_LINKER} ...)never reached the cargo that ninja starts, so a cmake project building crates (Corrosion) had to rebuild the whole cross setup in its Makefile:cross/fishcarried its own arch →RUST_TARGETtable, and a comment sayingenv-rust.mkcould not be included. -
cmake builds now load the rust tc_vars (
DEFAULT_ENV ?= cmake rust): cargo seesCARGO_BUILD_TARGETandCARGO_TARGET_<T>_{LINKER,AR,RUSTFLAGS}at build time. A cmake project without Rust only gets environment variables nothing reads. -
tc_vars.rust.mkalso exportsCC_/CXX_/AR_<triple>, the names cc-rs reads for a crate's build script compiling C (pcre2-sys in fish). Without them cc-rs looked for a barex86_64-pc-linux-gnu-gccon the PATH. This servesspksrc.cross-rust.mkpackages as well. -
Those three paths come from
$(call tc,...)like every other tool since #7523, so they follow an active overlay;RUST_TARGET_LENVis the suffix,$(subst)rather than atrper line sinceRUST_TARGETis already lower case. -
The toolchain file sets
Rust_CARGO_TARGET, which Corrosion cannot infer when cross-compiling and otherwise falls back to the host triple. -
cross/fishis down to its one package-specific need:-std=c99for pcre2-sys on gcc < 5.
September 29th 2026 — Two exported variables were re-running their shell for every process make started (#7522)
-
Parsing a package forked 2647 shells and took 8.4 s. 919 of them were
uname -mand 848git log, from two variables that are both exported and recursively expanded:SOURCE_DATE_EPOCH(spksrc.spk.mk) andPYTHONPATH, whose value reachesunamethroughPYTHON_LIB_NATIVE(spksrc.python-crossenv.mk). -
make rebuilds the environment for every child process, re-expanding each exported recursive variable as it does. So the two ran their
$(shell)once per spawned shell, and each spawned shell made the other run again. -
Both are now assigned immediately:
SOURCE_DATE_EPOCHbehind an$(origin ...)guard so it still yields to a value from the environment or the command line, anduname -mhoisted intoHOST_ARCH, which the generatedpython-cc.mkalready carried under that name. -
Measured: parse 8.40 s to 0.92 s,
work-*/PLISTfor mosquitto 19 s to 1 s,dependency-flatfor ffmpeg8 13 s to 6 s. The make database is otherwise identical, variable for variable and rule for rule.
September 29th 2026 — Three more variables were forking on every expansion (#7524)
-
AVAILABLE_TCVERSIONSforked once per toolchain, every time it was read.$(shell echo <arch>-<vers> | cut -f2 -d'-')inside a$(foreach)over 188 directories, in a recursively expanded variable -- 376 forks withAVAILABLE_KERNEL_VERSIONSbeside it.$(word 2,$(subst -, ,...))does the same split in make; no base toolchain directory carries more than one dash. -
INSTALL_PREFIX_VARrandirnameon a constant path, andtc-capability.mkran foursedover the same Makefile forTC_GCC,TC_GLIBC,TC_KERNELandTC_BINUTILS. The first is$(dir), the second one pass emittingKEY=valueand split with$(filter). -
Measured on an spk parse, against master: 2510 forks and 8.4 s down to 459 and 1.1 s; stacked on #7522, 749 forks down to 244 and 1.02 s to 0.39 s. Values are unchanged, and the four capability fields were compared over all 198 toolchain Makefiles.
-
:=is not free.ARCHS_WITH_GENERIC_SUPPORTwas left recursive on purpose: forcing it immediate costs 783 forks, because it is computed whether or not anything reads it. Making a variable immediate pays only where it is actually read.
September 29th 2026 — One overlay-aware tool lookup, shared by tc_vars and the packages (#7523)
-
The generated cmake, meson and autotools files resolved tools on their own. Three shell loops in
spksrc.toolchain/tc_vars.mkrebuilt the toolchain bin path by hand and swapped in the binutils overlay with acaseoverTC_BINUTILS_TOOLS-- a second implementation of what$(call tc,...)(spksrc.common/macros.mk) already does, and the one a package is told to use. -
They knew nothing of a gcc overlay.
$(call tc,...)picksTC_OVERLAY_GCC_PATHand appendsTC_GCC_SUFFIX; the shell copies had neither, soCC,CXX,CMAKE_C_COMPILERand meson'scwould all have kept pointing at the vendor gcc once a gcc overlay was switched on, silently. -
The loops are now
$(foreach)in make, so each path comes from$(call tc,...)on the target side and$(call native,...)on the build side.TOOLScarries one whitespace-free entry per tool -- meson's name, the autotools and cmake name, the binary -- which also retires thesedandtrthat parsed it, andTC_BINUTILS_TOOLS, whose list the macro already holds. -
TC_PATHand the twoTC_OVERLAY_*_PATHare defined where they are emitted, so the generator and thetc_vars.mkit writes resolve a tool the same way. -
cargo's
ARandLINKERcome from the macro too. They were the last two paths built by hand, so an active binutils overlay reachedldandasbut not thearthat packs the archives cargo then links. -
Output is unchanged but for one character: the seven generated files, over all 198 toolchains, differ from master only in
CMAKE_SHARED_LINKER_FLAGS, which carried a trailing space whenever no binutils overlay contributed a flag.
September 29th 2026 — A dependency walk checks the tree once and visits each dependency once (#7521)
-
The pre-check re-walked the whole subtree at every parse.
_TREE_GATESrunsdependency-unsupportedover the package's tree whenever a package is parsed withARCH/TCVERSION: its own make, then itscross-stage1andcross-stage2sub-makes, for every package of the build. The owner's walk had already covered all of them. On an already built gstreamer,make dependspent 764 make calls and 42 s on that (gst-plugins-base alone visited each of its dependencies 16 times). -
Now once per WORK_DIR. Once the tree passed,
PRECHECK_TREE_DONEis exported with the WORK_DIR it was checked for; every later parse building into that WORK_DIR skips the walk. The package's own checks (capability floors,UNSUPPORTED_ARCHS, DSM window) still run everywhere. Same gstreamer, same load: 95 make calls, 11 s. -
depend_targetvisits each dependency once per run (dep_seen, as the PLIST walk since #7516), stamps in$(WORK_DIR)/.DEPEND. An already built tree gains nothing there, cookies already stop the recursion; a fresh build does, where a package still to build walks back into dependencies another one already built. -
Both are bound to WORK_DIR, never handed down blindly.
DEPEND_WALKnames the WORK_DIR that owns the walk; a sub-make building into another one (toolchain, toolkit) checks and walks on its own instead of trusting stamps it did not write. The owner clears.DEPENDbefore walking, so a failed or interrupted run leaves nothing behind;make spkcleanremoves it too.
September 28th 2026 — The spk PLIST is written whole or not at all, and walked once per dependency (#7516)
-
A partial
work-*/PLISTwas taken as up to date. The file is its own make target, written straight through> $@. When the walk behind it broke off, acat_PLISTthat failed, or a build whose output pipe died (an interruptedmake arch-*, a closed terminal), the partial file stayed, and every later run packaged from it: a gstreamer build then succeeded with 146 of its 851 entries, a 6 MB package instead of 42 MB, or stopped attar: Cowardly refusing to create an empty archivewhen nothing had been written yet. -
Now written to
PLIST.tmpand moved in place only once complete, withpipefailand a failingcat_PLISTstopping the walk instead of being swallowed by the loop.make spkcleanremoves a leftoverPLIST.tmp. -
Each dependency is walked once.
cat_PLISTrecursed into every dependency once per path leading to it, a full make parse each time: gstreamer's walk made at least 591 calls for 67 distinct dependencies (zlib alone 132 times), tens of minutes on a loaded machine. The spk side now hands down a stamp directory, and a dependency already visited is skipped before its make is started. gstreamer's PLIST, same machine and load: 3603 s before, 239 s after. -
dep_seen(spksrc.common/macros.mk) is that test, shared with the dependency-tree walk, which had its own inline copy. Stamps are named as there (cross__zlib) and now created withmkdir, so two parallel visitors cannot both claim one.
September 28th 2026 — CMake builds always use the generated toolchain file (#7515)
-
CMAKE_USE_TOOLCHAIN_FILEis gone, and with it the "legacy" mode that passed the cross toolchain through-D_CMAKE_TOOLCHAIN_PREFIXand friends. That mode relied onCC/CXXreaching cmake through the environment, but those live intc_vars.autotools.mk, which cmake builds no longer load sinceDEFAULT_ENV ?= cmake(#6877): cmake silently fell back to the host/usr/bin/cc. On aarch64 the link fails (libcairo.so: file in wrong format); on x64 it succeeds with the wrong compiler and nobody notices. -
The two remaining users move over.
cross/frei0r(pulled in by ffmpeg and gstreamer) andcross/fish, which already hands cargo its compilers throughCC_<target>/CARGO_TARGET_<TARGET>_LINKER. -
The toolchain file now sets
CMAKE_MODULE_LINKER_FLAGSbeside the EXE and SHARED ones. A cmakeMODULElibrary (a plugin: frei0r builds nothing else) was linked without the staging-L/rpath and could not find its dependencies.
September 28th 2026 — A plist pattern is the files it stands for, not a path (#7518)
-
A package built on demand describes its libraries with a pattern.
cross/boost_1.92/PLISTsayslib:lib/libboost_*.so.1.92.0, because it does not know which libraries the caller asked it to build.include_toolchain_specific_librarieshanded that toobjdumpas written -- a path that does not exist -- so the files it stands for were never examined, and the toolchain runtime library they needed was never carried. Silently: with no file seen, the loop moved on. -
znc 1.10.3-30 is what it cost. boost 1.92's
charconvandlocalelinklibquadmathfor__float128, they were the only files in the package that did, and the published package exits at start-up onlibquadmath.so.0: cannot open shared object file. -
Each entry is now resolved inside the staging directory before it is read, the way
copy.mkresolves it before packing, so a pattern contributes the files it matches and a plain path behaves exactly as before.
September 24th 2026 — The switches reach every crossing, and the spk one narrows (#7497)
-
Eight sub-make crossings still dropped
FWRD_ARGS, so a build-wide switch reached part of a build and not the rest. #7458 gave the switches one list; this finishes carrying it. -
What gets lost is not obvious. A value given on the command line survives a bare sub-make on its own, through
MAKEFLAGS. A value set in a package Makefile, or left at its?=default, does not:
So the crossings that dropped it worked for whoever typed VIDEODRV=0 on the
command line, and silently did not for anyone else.
-
Now forwarded: the
cat_PLISTwalk into a dependency and its spk-side twin (the plist must describe what was built, not what a default would have built), thecross/*dependency loop andNATIVE_DEPENDS, the dependency-tree walker (a tree computed without the switches can name packages the build will not produce — the shape of the phantom-arch bug #7431 fixed), andkernel-required, which recurses overBUILD_DEPENDSandDEPENDSwhose contents themselves vary with the switches. -
The two native loops keep their
env -iisolation, deliberately and with a comment saying so: they build host tools, and the one native package that cares about a switch sets its own?=default. Forwarding a0into it would override that default and break it, which is the opposite of what the switch means for a host tool. -
The spk → spk crossing goes the other way and gets narrower.
FWRD_ARGS_SPKcarriesVIDEODRValone: whether a meta is in the build at all is a decision about the run, and one that disagrees links a libdrm nobody can resolve. Which compiler a package builds with is not — that belongs to the package and its own work dir, so a meta recomputes it rather than inheriting whatever its caller happened to choose.
September 24th 2026 — An spk is rebuilt only when it changed, and is reproducible (#7499)
-
Run the same build twice and the second run no longer rewrites the
.spk. Today it does:make arch-x64-7.2a second time, with nothing changed, produces a fresh archive with different bytes and a new timestamp. Now it sees the same content and leaves the file alone.make spkclean && make arch-x64-7.2still regenerates everything, on purpose --spkcleanremoves the digest with the rest of the packaging state, so the next build has nothing to compare against; being reproducible, it comes out byte-identical anyway. -
Which is why a
.spk's timestamp now means something. It is the moment the package was built, not the moment a later pass happened to touch it. -
It was rebuilt on every invocation, with different bytes each time, so its timestamp recorded the last repackaging rather than the build. That is what made a budget-stopped CI run look as though
synocli-videodriverhad been built after ffmpeg when it had been built two hours before it. -
make --debug=bnames the cause:Prerequisite scripts/service-setup does not exist— for a file that is plainly there.spksrc.service.mkdeclares six generated files.PHONYso a changed variable can never leave a stale one behind. The cost is that make stops seeing them as files, and anything depending on them is remade unconditionally. -
The guarantee is kept and the decision moved. A phony
spk-md5sumtarget still runs every time and digests what actually goes into the package; it rewrites.md5sum_doneonly when that digest changes, and the.spkhangs off that file alone.SPK_CONTENTis complete by then, because icons and wizards extend it from inside their own recipes, which are prerequisites of the digest. -
It is named and cleaned like the status cookies it sits beside, and the CI already collects
.*_doneinto the diagnostics — unlike the empty cookies, this one arrives with a readable fingerprint, which is what answers "why was this repackaged".spkcleanremoves it with the rest of the packaging state: without that, an explicitspkcleanleft the digest behind and the next build declined to repackage at all. -
SOURCE_DATE_EPOCHmakes the archive reproducible, and covers the three producers at once:tarreads it for member dates,gzipneeds only-n, and ImageMagick honours it for the PNGtIMEchunk that-stripdoes not remove. The value is the last commit touching the package, so it answers "which revision is this" rather than "when did you run make"; it falls back to the Makefile's mtime outside a git tree and yields to the environment. git metadata survives a checkout where file mtimes do not, so CI gets the same value as a workstation. -
Being reproducible and knowing when to repackage stay separate questions. The content digest compares the mtimes on disk, not the ones written into the archive — so no producer upstream has to be deterministic, the cross builds whose libraries land in the staging tree included.
-
Measured on
spk/demoservice(noarch) andspk/mercurial(x64-7.1 — icon,SERVICE_SETUP, python meta, wheels; 20 members including both PNGs andWIZARD_UIFILES):
September 23rd 2026 — A linker floor, for what a newer as and ld can lift (#7495)
-
MIN_BINUTILS_VERSIONjoins the capability floors, besideMIN_GCC_VERSION,MIN_GLIBC_VERSION,MIN_KERNEL_VERSIONandMIN_RUSTC_VERSION. A package states the assembler and linker it needs, and the architectures whose toolchain is older are refused with a reason. -
It is a build-time floor, like gcc. It asks what
asandldcan encode and resolve, and a newer binutils answers it. That distinguishes it from glibc and the kernel, which ask about the machine the binary will run on and which nothing in the toolchain can change. -
TC_BINUTILSis declared by every toolchain, besideTC_GCC,TC_GLIBCandTC_KERNEL. Not one value is inferred: all 115 distinct toolchain archives were downloaded and their own<target>-ld --versionread, covering the 188 declarations. It comes out as one binutils per gcc generation --gcc binutils gcc binutils 4.3.7 2.18.50 4.9.4 2.25 4.6.4 2.22 7.5.0 2.30 4.7.3 2.22 8.5.0 2.30 4.8.3 2.24 10.3.0 2.35.1 4.9.3 2.25 12.2.0 2.38
-- but that was worth confirming rather than assuming, because the toolchains do not
come from one place: 88f6281 ships a Marvell build, ppc853x a plain GNU one,
the DSM 6.2.4 family crosstool-NG/Linaro, DSM 7.x plain GNU again. binutils tracks
the compiler, not the DSM release -- hi3535-6.2.4 sits on gcc 4.8.3 with 2.24
while its 4.9.3 neighbours in the same release are on 2.25, and comcerto2k-7.1
pulls the same 4.9.3 archive as comcerto2k-6.1 in a lineup otherwise on 8.5.0.
The span is the point: an object a current compiler emits can carry a relocation or a debug format a 2008 linker never learned.
-
bandwhichanddutreeare the first two converted. Both carriedUNSUPPORTED_ARCHS = $(OLD_PPC_ARCHS)under a comment quotingunknown relocation type 95. Reloc 95 isR_PPC_TLSGD, the markerldneeds to resolve the TLS general-dynamic sequence rustc emits; binutils grew it for 32-bit PowerPC on 2009-03-04 and shipped it in 2.20. Reproduced directly -- the same object fails to link withppc853x-5.2's 2.18.50 and links with 2.25 and 2.30:
They now declare MIN_BINUTILS_VERSION = 2.20. Exactly one toolchain in the tree
falls under it -- ppc853x-5.2 -- so the set of refused architectures is unchanged;
what changes is that the refusal names the linker, and lifts itself if that
architecture ever gets a newer one.
- What was deliberately not converted.
cross/duaandcross/procsexclude the same architectures under a comment quotingDwarf Error: found dwarf version '4', this reader only handles version 2 and 3. That line is not the failure: a DWARF-4 object links cleanly on 2.18.50. It appears only whenldalready has an error to report and cannot read the debug info well enough to name a file and line -- so it is 2.18.50 printing a worse diagnostic about some other problem. Converting those two to a linker floor would have encoded a cause that was never verified; they keep their arch list until someone reproduces the real one.
September 23rd 2026 — A rustc floor reads the toolchain the build selects (#7500)
-
The floor answered with the oldest rustc an architecture ships.
MIN_RUSTC_VERSIONcompares againstTC_RUSTC, which came from the first entry a glob over the arch's rust consumers returned — and that sorts oldest-first. It now readsTC_OVERLAY_RUSTC, the consumeroverlay.mkselected, which is wherespksrc.toolchain.mkresolvesTC_RUSTCfrom too. -
Latent while an arch ships one rust consumer, wrong once it ships several. On
qoriq-6.2.4with the gcc overlay active the build receives 1.98.1, yet every floor above 1.82 refused the arch —bat-latest(1.87),eza-latest,lsd-latestandms-edit(1.85),fd-latest(1.90), all reported asrustc 1.82.0 < …. All five clear it now, and all five are refused again with the overlay off, where 1.82.0 really is what the build gets.
September 28th 2026 — The host toolchain is named, not emptied (#7429)
-
spksrc.native/env-default.mkemptiedCC,CPP,ARand the rest. That kept a cross toolchain's tools from leaking into a native package built as a dependency, but it also left builds that need a host compiler without one —gcc'sparams.listis generated by$(CPP), and an emptyCPPmakes the shell execute the header instead of preprocessing it, a failure the pipeline'ssedthen swallows. Naming the host tools does both jobs. -
$(call native,<tool>)joins$(call tc,<tool>)inmacros.mk— the absolute path of a tool on the build host, the bare name when it is absent so a missing tool fails by its own name. It usescommand -v, a POSIX shell builtin, rather than the externalwhich.spksrc.native/toolchain-rust.mktakes it for the five host tools in the rust bootstrapconfig.toml.
September 19th 2026 — A kernel floor, for what no compiler can lift (#7468)
-
MIN_KERNEL_VERSIONjoins the capability floors, besideMIN_GLIBC_VERSIONandMIN_GCC_VERSION. A package declares the kernel it needs and the arch is refused with a reason, instead of being listed in anUNSUPPORTED_ARCHSline that says where it fails but never why. -
It is a runtime floor, like glibc. A gcc floor asks what the compiler can do, and a newer compiler answers it. A kernel floor asks what the machine running the binary can do; nothing in the toolchain changes that answer.
-
The Intel media stack is the case in point. libva, gmmlib, intel-media-driver, intel-mediasdk and
synocli-videodrivertalk to i915 ioctls; DSM 5.2 ships a 3.2 kernel that predates the render-node interface libva needs. They now declareMIN_KERNEL_VERSION = 3.10rather than enumerating archs:
x86-5.2 and evansport-6.2.4 (kernel 3.2.40) and evansport-7.0 (3.2.101) are
refused with kernel <version> < 3.10 (a runtime floor: no toolchain can lift it).
x64-6.2.4 (3.10.105), x64-7.1, apollolake-7.1, braswell-6.2.4 and
x86-6.2.4 were checked to still build, and no package loses an arch it ships on
today: synocli-videodriver already stops at REQUIRED_MIN_DSM = 6 and excludes
i686_ARCHS, and synocli-videodriver-tools at 6.2.4.
cross/tvheadendgets the same floor, because it does not have it by inheritance. It addscross/libvafor the whole ofx64_ARCHS, andx86is in that list while running a 3.2 kernel -- so without a guard the floor would turn a missing vaapi into an unsupported package. Guarding theDEPENDSleavesx86-5.2building tvheadend exactly as before, without the video stack.
September 19th 2026 — One runtime library for all of a package, not for its first binary (#7466)
-
The copy that shipped was the one the first binary needed.
include_toolchain_specific_librarieswalked the plist, matched the first binary that asked forlibatomic/libquadmath/libgfortran, installed whichever copy satisfied it, and stopped (break 2). A find by name returns every copy under the toolchain root at once -- the sysroot's, the compiler'slib64, a multilib -- and they are not the same version, so when the first binary listed is the least demanding the oldest copy is what gets carried. -
The union decides now. Every plist
lib/binentry and every wheel.sois read first, their required symbol versions merged, and one copy chosen that provides all of it. The log line changed with it:Providing [<versions>] for <lib>instead ofFound in <file>. -
Shown before and after on the same staging tree: one binary needing
GLIBCXX_3.4/CXXABI_1.3.9, one needingGLIBCXX_3.4.21, two candidates under the toolchain root. Before, the older candidate is installed on the strength of the first binary alone; after, the union is[CXXABI_1.3 CXXABI_1.3.9 GLIBCXX_3.4 GLIBCXX_3.4.21]and the copy providing all of it wins. -
_select_tclib_is gone: the per-binary selection it implemented has no caller left.
September 19th 2026 — The last spelled-out toolchain path (#7467)
The conversion started in #7441 and continued in #7454 left one package behind.
-
cross/libhdhomerunbuilt its own path intotoolchain/syno-$(ARCH)-$(TCVERSION)/work/...and handed it toCROSS_COMPILE.CROSS_COMPILEcan only express a prefix, so it assumes the compiler is<path>/<target>-gccand nothing else. -
It names its two tools instead. Its upstream Makefile assigns
CCandSTRIPwith:=, and a value on the make line overrides that, soCC="$(call tc,gcc)" STRIP="$(call tc,strip)"is enough andCROSS_COMPILEgoes. -
Byte-identical output before and after on
88f6281-6.2.4andx64-7.1: the binary, the shared library and every installed header.
September 19th 2026 — The overlay binutils was built with no optimisation at all (#7469)
-
An
ENVline above the include never wins.native/binutils-2.30setENV += CFLAGS="-O2"before including the native front-end, andenv-default.mkappends its ownENV += CFLAGS="$(NATIVE_CFLAGS)"when that include is read.NATIVE_CFLAGSis empty, and the last assignment wins inenv VAR=... cmd-- soCFLAGS=""reached configure and make, which also overrides the-O2configure would otherwise have chosen. Every published binutils overlay, v1 and v2, is an unoptimised build. -
Set
NATIVE_CFLAGS, don't append toENV. It is the variableenv-default.mkreads, so filling it is order-independent and cannot be undone by moving a line. (native/gcc-8.5escapes the trap only because itsENVline happens to sit below its own include.) -
What it buys, measured on
qoriq-6.2.4, same host, three runs:ason a 2 MB.sgoes 0.83-0.98s to 0.39s for ten assemblies (~2.2x),ld -r0.31-0.33s to 0.24-0.25s for fifty (~1.3x).ldshrinks 3441688 to 2929056 bytes,as2491896 to 1834688. -
--disable-install-libbfdgoes with it. The consumers symlinkusr/local/bin/<target>-{ld,as}and nothing else, but the archive shippedlibbfd.aandlibopcodes.a-- and-ffat-lto-objectsholds both IR and objects in them. Onx86-5.2that was 7.3 MB and 2.6 MB against 2.3 MB for the whole ofbin/. The archive goes 9.1 MB to 2.5 MB, below even the unoptimised v1 at 3.1 MB. -
v3 archives are published for the five archs that have a binutils overlay, and each consumer pins the new rev.
September 19th 2026 — make digests in a toolchain folder, without moving the rules (#7465)
-
make digestsworked everywhere except the three front-ends.toolchain,kernelandtoolkitincludespksrc.rules.mkbeforechecksum.mk, andgenerate-digests.mknames its rule$(DIGESTS_FILE). A target name is fixed when its rule is parsed, so the variable was still empty there and the rule was named after nothing:make digestsanswered "Nothing to be done". -
The one-line fix is in the rule file.
generate-digests.mknow declaresDIGESTS_FILE ?= digestsitself, right above the rule that needs it.checksum.mkstill owns the value; the?=only makes the name exist early enough to be a target. -
Reordering the includes is not the fix, which #7464 found out the hard way. Moving
spksrc.rules.mkbelowpatch.mkmade toolchain patches apply from the wrong directory, andpatchthen asks "Skip this patch? [y]" on the same stdin the patch arrives on: the question is eaten, the answer never comes, and the build hangs until the job's hard timeout. Ten CI archs were killed at 5h48 having built nothing, where the run before finished the same archs in 2h09 to 4h30. The include order is restored. -
A resolved target is not a built one.
make -nresolved the reordered tree perfectly; only an actual build reaches thepatchrecipe. Framework changes to these three front-ends need a real toolchain, kernel and toolkit built end to end, which is how this one was checked -- digests regenerated byte-identical included.
September 15th 2026 — One list carries the build-wide switches (#7458)
- Written out at every crossing. A switch only means something downstream if it
survives the process boundary, and
OVERLAY_RUSTC/OVERLAY_BINUTILSwere spelled out by hand at each one. Eleven call sites provision a toolchain or a toolkit, and they had already drifted:cross-cc.mkforwarded the selectors totcvars,spk.mkandkernel.mkcalled the same target without them. - Silent when it goes wrong, which is what makes it worth fixing: the child falls
back to its own
?=default and resolves a different toolchain. Nothing errors, the build just uses another compiler, or provisions an overlay nobody asked for. -
Declared once, beside the switch:
and every crossing reads $(FWRD_ARGS), in one shape:
@$(MAKE) WORK_DIR=<dir> $(FWRD_ARGS) --no-print-directory -C <dir> <target>.
The toolkit calls carry it too, where the switches mean nothing today: carving out
exceptions is what let the tcvars calls drift apart in the first place.
- Placement is load-bearing. FWRD_ARGS sits between the include that declares the
names and stage0.mk, whose $(shell) is the first crossing to read them; put it
lower and a switch assigned in a package Makefile is lost. Only a makefile assignment
shows this -- a value from the command line arrives regardless, make exporting those
to its children.
- Two variables, not one. FWRD_VARS holds names and FWRD_ARGS the pairs, so the
conversion is idempotent: spksrc.common.mk is read three times for a cross package
and twice for a native one, and a single variable would re-convert its own output
into OVERLAY_BINUTILS='0'=''.
- What belongs in the list, and what does not: a decision that must hold
identically for every package of the run. The overlays qualify (a shared ABI);
GCC_DEBUG_INFO and GCC_NO_DEBUG_INFO do not -- they are per-package, set by
spk/tvheadend and by cross/llvm-140 and the intel stack respectively, and the
list crosses into other packages, where one package's choice must not be imposed.
supported.mk carries both by hand instead, as arguments rather than the
environment prefix it used -- the prefix loses to a package's own assignment, an
argument does not -- and only when set, an empty one being an override of its own.
- The two debug switches now refuse each other. env-default.mk let
GCC_DEBUG_INFO win by if/else while cmake, ninja and install tested
GCC_NO_DEBUG_INFO on its own and stripped anyway -- a build that compiled symbols
and then threw them away. Setting both is now an error.
- TC_GCC stops being re-derived. env-default.mk put it into every build's
environment by running the cross compiler -- TC_GCC=$$(eval $$(echo …gcc
-dumpversion)), 61 times in one tvheadend build -- for a value tc_vars.mk had
already written beside it. It now reads $(TC_GCC), like the TC_GLIBC and
TC_KERNEL lines under it. Same answer on every arch measured; and one answer
rather than two, which matters once an overlay makes the vendor compiler the
wrong one to ask.
- VIDEODRV is the first to use it. #7455 landed it with the name spelled out in
depend.mk's env -i; it is now FWRD_VARS += VIDEODRV and $(FWRD_ARGS), which
also promotes it from an environment prefix to a command-line variable there.
- Package-facing: nothing to change. A new build-wide switch is one line,
FWRD_VARS += <NAME>, next to where it is declared.
September 13th 2026 — An automatic build does what the change asks for, no more (#7455)
- The cost.
synocli-videodriveris the heaviest build in the tree -- mesa, the Intel compute runtime, the graphics compiler, Vulkan, shaderc -- and it changes almost never. Every automatic run that touched an ffmpeg consumer paid for it again, becausespk/ffmpeg*declaresVIDEODRV_PACKAGEand the meta follows. VIDEODRV = 0drops the meta and every option that depends on it:META_DEPENDSis empty,spk/synocli-videodriverleavesBUILD_DEPENDS,synocli-videodriver-toolsleavesSPK_DEPENDS,cross/ffmpeg4-8configure without--enable-libdrm,--enable-vaapi,--enable-libmfx, the OpenCL/Vulkan set and--enable-libplacebo, andcross/tvheadendwithout--enable-vaapi/--enable-qsv. Anything else (--enable-v4l2-m2m) is untouched. Undeclared builds as before, which is what a local tree does;make setupwrites it commented intolocal.mk, below the overlay switches and reading the same way -- command line > environment >local.mk> the default. Unlike them, only an explicit0/offleaves the meta out: an unexpected value builds, rather than quietly publishing an ffmpeg with no acceleration and no error to show for it.- Who sets it. Only the automatic CI runs (
push,pull_request), and only when change detection did not already namesynocli-videodriveror its tools package -- a change undercross/libva,cross/mesaor any other videodriver dependency does name it through the dependency list, and that run builds the meta in full. A manualworkflow_dispatchnever sets it, so published packages always carry hardware acceleration. It also crosses theenv -ithat isolates an spk meta source (depend.mk): a meta that disagreed with its consumer linked a libdrm the consumer then could not resolve --libavutil.so: undefined reference to 'drmGetVersion'. - The CI list stopped injecting metas while it was at it.
prepare.shresolvedPYTHON_PACKAGE/FFMPEG_PACKAGE/VIDEODRV_PACKAGErecursively and added each meta to the list of packages to build -- redundant, sincepython.mk,ffmpeg.mkandvideodriver.mkeach putspk/<meta>inBUILD_DEPENDSand the dependent builds it anyway. It also built metas for architectures the dependent refuses:spk/homeassistantdeclaresUNSUPPORTED_ARCHS = $(ARMv7_ARCHS), yet armv7 spent a full run on the injectedpython314. Such a run now builds nothing. - And a clean that no longer happens.
build.shassembledpackages_to_keepfrom itsffmpeg_versions/python_versionsarrays and never read it: the per-package clean it shielded those artifacts from is gone, the runners having grown enough disk this year to hold every work dir for a whole run. All three are removed.
September 13th 2026 — Ask the toolchain for a tool, never spell its path (2 PRs)
A package that needs a compiler or a binutils tool by path used to write
$(TC_PATH)$(TC_PREFIX)gcc. That is right only while the toolchain is the vendor one:
an overlay lives somewhere else entirely (<consumer>/work/install/usr/local/bin
against <work>/<target>/bin) and its gcc family carries a version suffix, so the
hand-built path silently keeps resolving to the vendor tool with an overlay active --
no error, just the wrong compiler. $(call tc,<tool>) answers the question instead.
$(call tc,<tool>), and the packages that spelled paths (#7441)
- The macro takes a tool name and returns its absolute path, through
TC_OVERLAY_GCC_PATHfor the gcc family (gccg++c++cppgfortran, plusTC_GCC_SUFFIX), throughTC_OVERLAY_BINUTILS_PATHfor binutils (ldasarnmranlibstripobjdumpobjcopyreadelf), and throughTC_PATHfor anything else.TC_OVERLAY_<c>_PATHis empty unless that overlay is active, so a call is already correct with no overlay and stays correct when one is grafted on -- nothing to revisit in the package. TC_OVERLAY_GCC_PATHis emitted too, empty today since no gcc overlay exists yet. The contract is whole, so switching one on needs no change here.- Converted:
cross/fish,haproxy,libcap,libcap_2.51,lua,lua-5.3,lzip,lzlib,pgvector,plzip,postgis,unzip, and themesoncrossfile and python-crossenv generators. - Documented under Macros.
- Pull request: #7441
ffmpeg and x264: name every tool instead of deriving it (#7454)
--cross-prefix was ffmpeg's single answer for nine tools, and it derives them by
string concatenation -- precisely the hand-built path the macro exists to replace.
- Every tool is named: --cc, --cxx, --ar, --nm, --ranlib, --strip,
each $(call tc,...), so each follows whichever overlay provides it. as, ld
and dep_cc still default from cc inside configure, which is what we want;
--enable-cross-compile, which --cross-prefix used to imply, is now stated
and leads the block; --ranlib no longer goes through $(RANLIB).
- The x86 assembler is not as. --x86asmexe=nasm on the i686/x64 families
is unchanged and stays a plain name: that nasm is a native tool, not a cross
one, so it is deliberately not a $(call tc,...).
- x264 takes CC from the environment (CC="${CC-${cross_prefix}gcc}"), so
naming it there is enough.
- One switch per CONFIGURE_ARGS line throughout the tool block, so a diff shows
which tool changed.
- Pull request: #7454
a full run on the injected python314. Such a run now builds nothing. The exemption
that kept an injected meta in the standard builds goes with it: every list now holds
only what declares that REQUIRED_MIN_DSM itself, and none of them may be exempt.
September 7th 2026 — The build tree owns its tc_vars, the toolchain owns none (#7442)
- What was shared:
toolchain/syno-<arch>-<vers>/workheld onetc_vars*set.tcvarsshort-circuits to an empty target once its cookie exists, so the first build to reach a toolchain fixed that file and every build after inherited it -- in both directions. A build that wanted an overlay never got it provisioned; one that did not was handedTC_EXTRA_LDFLAGS/TC_OVERLAY_*/TC_GCC_SUFFIXit never asked for. A file shared by every build tree has no correct owner, so there is no longer a file. -
Each tree generates its own, in its own work dir, and reads nothing else.
WORK_DIRalready carried that scope:depend.mkpasses the root's value down through$(ENV),directories.mkkeeps what it is handed (ifndef), and theenv -iaround an spk meta source is where one tree ends and the next begins:
Two builds may therefore run side by side against the same toolchain -- two SPKs,
or a cross package built directly to test it -- each with its own answer.
- stage0 writes the tree's tc_vars.mk at parse time and includes that rather
than the toolchain's. Only the identity file: the other four embed
INSTALL_PREFIX-derived paths, and INSTALL_PREFIX is recipe environment that
$(shell) cannot see that early. It needs no extracted toolchain either, every
value in it being a constant of toolchain/syno-*/Makefile, so TC_GCC now
resolves on a cold tree and the heavy bootstrap keys on the extracted
$(TC_TARGET) instead of a generated file.
- Ahead of the pre-check. The generation is unconditional and runs before an arch
can be refused, so a refused package still leaves a work dir holding what the
refusal was judged against -- spk/tvheadend/work-ppc853x-5.2/tc_vars.mk with its
TC_GCC := 4.3.7.
- Package-facing: nothing to change. make -C toolchain/<TC> toolchain now only
downloads, extracts and patches; anything that read toolchain/*/work/tc_vars*
should read the build's own work dir instead.
- The dependency walk goes with it. dep-flat-mk-% recursed without forwarding
WORK_DIR, so every package it visited wrote a tc_vars.mk of its own: a single
make check left 141 work directories behind for a command that builds nothing. It
now forwards it as depend.mk does through $(ENV), and the walk reads the root's.
September 7th 2026 — An arch exclusion says why, and names every blocker (#7439)
- Seven restated floors gone.
spk/tvheadend,chromaprint,comskipandspk/ffmpeg5-8each declaredMIN_GCC_VERSION = 4.9, restating what their owncross/package declares -- andcross/<same>is a directDEPENDS, so the walk finds it. Declare a floor on thecross/package, where the requirement is a fact about the code; thespk/inherits it by being walked. Verdicts unchanged. make check-<arch>-<tcvers>reports every capability gate a package's whole dependency tree fails for that architecture, or answers<arch>-<vers> check: OK.make ARCH=x86 TCVERSION=5.2 checkis the same thing with the pair in variables. A pure diagnostic: it extracts no toolchain and builds nothing, it reads the floors declared across the tree.-
The pre-check names every gate, not the first. A package is as blocked by a floor it never declared as by one it did, and the pre-check only ever saw its own -- so removing the blocker it named revealed the next, one build at a time. It now runs the same walk and lists all of them before stopping:
===> check: cross/x264 gcc 4.3.7 < 4.6 ===> check: cross/python314 gcc 4.3.7 < 4.8 ===> check: cross/ffmpeg8 gcc 4.3.7 < 4.9 ... pre-check.mk:80: *** Arch 'ppc853x-5.2' is not supported by tvheadend (18 failed check(s) in the tree). Stop.It reuses
dependency-flat's walk, which already stamps each package so a diamond is visited once, already forwards ARCH/TCVERSION so every package evaluates its own conditionalDEPENDS, and already setsDEPENDENCY_WALK=1so a refused package reports instead of aborting.DEP_FLAT_VERDICTmakes each package visited print its own verdict, so one walk yields both the tree and every gate in it.Cost is one walk per parse -- 2.8s warm on tvheadend's 141-package tree, against builds measured in tens of minutes. Verdict-neutral on the archs measured: every
spk/package across x64-7.1, 88f6281-6.2.4 and ppc853x-5.2 gives the same refused/allowed set as before. What changes is that the whole story is told at once. -
What was missing:
UNSUPPORTED_ARCHSstates where a package fails and never why, and the archs are often added by an include rather than by the package -- so the message named a package with no such list in its own Makefile.UNSUPPORTED_ARCHS_REASONis now carried into the refusal in parentheses:spksrc.cross/env-go.mkandenv-dotnet.mksay theirs. A capability floor is still the better answer where one fits; this is for exclusions that are not capability checks. -
One accumulator instead of two.
_tc_cap_joinaccumulatedTC_CAPABILITY_UNSUPPORTEDandunsupported_reason_joinaccumulatedUNSUPPORTED_ARCHS_REASON-- the same operation under two names, and the second was not a macro at all (no argument, just a lazy read of a global). Both are now$(call comma_append,<list>,<item>). - A
$(,)that expanded to nothing.spksrc.common.mkdefinedempty/space/$(,)below everyspksrc.common/include, so an included file expanding$(,)in a:=assignment got the empty string -- which is whytc-capability.mkcarried a private comma variable. The utility block moved above the includes. spksrc.cross/env-dotnet.mklost 26 tab-indented lines sitting outside any recipe, and its$(error)became a$(warning): an unsupported arch should be reported by the pre-check, not by a parse abort in an env file.- Package-facing: nothing to change. Add
UNSUPPORTED_ARCHS_REASONalongside anUNSUPPORTED_ARCHSyou introduce, and the refusal will carry it.
September 4th 2026 — Build logs keep what the console showed (#7396)
-
What was lost: a package refused by a pre-check left a build log holding a single line.
$(error)fires at parse time, so no recipe of the inner make ever runs -- and the teeing lived inside a recipe. The reason for the refusal, make's own*** ... Stop.and theError 1cascade all went to the console alone, and the[BEGIN]/[END]markers only ever reachedstatus-build.log. -
Teeing moved one level up. It now happens in
arch-%, above the make that fails, so that make's output and its exit cascade both pass through the pipe. A parse-time$(error)produces no recipe to tee from, which is why writing the message by hand was not enough on its own. precheck_fatalreplaces nine copies of the sameifneq/shell/errorblock and sends one message to all three readers: the console, the package's build log, and the CI's collected unsupported list. It also fixes a timestamp in that list -- written$(date --date=now ...)it was make expanding an undefined variable, so every line began with a bare" - "; escaped to$$(date ...)the shell runs it.- One logging mechanism instead of two.
LOG_WRAPPEDand the new helper carried the same body --script(1)for a pty so stderr is captured too,sedto strip ANSI on the way to the file, and aLOGGING_ENABLEDguard so exactly one level tees and nothing is written twice. They are now_runlog(the shell fragment, underscored because it needspipefailand bash from its caller) andRUNLOG(the facade: builds the make command from a goal, writes toDEFAULT_LOG, reports failure toSTATUS_LOG).LOG_WRAPPEDis gone; nine call sites renamed, no behaviour change. -
Toolchain lines name their directory. A status line for an overlay consumer logged an empty arch and called itself
toolchain, because the branch meant for it was guarded on$(TC_NAME)$(TC_VERS)and a consumer setsTC_VERSbut noTC_NAME. Both branches now use the directory name minus itssyno-prefix, so every overlay is visible:
Neither TC_NAME nor TC_ARCH can do this alone: a consumer sets no TC_NAME,
and on the generic toolchains TC_ARCH holds the reference model, so
syno-aarch64-6.2.4 would have logged rtd1296 and syno-x64-7.1 apollolake.
- TIME_CMD: GNU time(1) is now resolved by path. The recipes that pipe need
bash for pipefail, and under bash time is a keyword that rejects -o, so
PSTAT_TIME broke as soon as the teeing recipe switched shells. Empty if absent,
which simply disables PSTAT.
- Known limit: an error can only be captured one level above the make that
fails, so the outermost make: *** [arch-x64-7.1] Error 2 is structurally
unreachable -- there is no outer make to tee it.
- Package-facing: nothing changes for a successful build -- one [BEGIN], one
[END], no duplicated output. A package that calls the old LOG_WRAPPED should
use RUNLOG.
- Pull request: #7396
September 4th 2026 — Link librt and libatomic instead of discarding them (#7433)
-
What:
TC_EXTRA_LDFLAGSno longer wraps-lrt/-latomicin-Wl,--as-needed ... -Wl,--no-as-needed: -
Why:
--as-neededkeeps a library only if it resolves a symbol that is already undefined when the linker reaches it.LDFLAGSis placed before the objects and libraries being linked, so nothing is undefined yet and the bracketed-lrtwas dropped every time. The declaration introduced by #7314 was therefore inert — measurably identical to declaring nothing: -
How it surfaced: on glibc < 2.17 toolchains (
ppc853x-5.2,88f6281-5.2,88f6281-6.1),libsrt.socallsclock_gettimewithout declaring it. ffmpeg reported the resulting link failure asERROR: srt >= 1.3.0 not found using pkg-config, which reads as a missing dependency rather than a missing library. -
Package-facing: the five per-package workarounds it forced are gone —
cross/ffmpeg4throughcross/ffmpeg8each carried
to put back what the toolchain had already declared. A package should not have
to re-declare a toolchain library; if you have such a workaround, drop it.
- Cost: one DT_NEEDED entry on binaries that never call into librt or
libatomic. Both are part of the toolchain's own runtime and present on every
target that ships them.
- Also in this PR: dependency-list now keys its output on the package
folder instead of $(NAME). Three packages set SPK_NAME to something
else — spk/ffmpeg4 (ffmpeg), spk/mkvtoolnix_22 (mkvtoolnix),
spk/mono_58 (mono) — while every consumer of the list already looked the
key up as a directory (.github/actions/prepare.sh, spk-meta/base.mk). The
three silently fell out of the CI build matrix, and the latter two collided
with the mkvtoolnix / mono folders that still exist beside them. This is
the rule SPK_FOLDER already states in spksrc.rules/pre-check.mk. The
hand-maintained fixup list in prepare.sh (nzbdrone -> sonarr3,
python -> python2, both stale) is removed with it.
- Confirmed: with DSM 5.2 switched on for the run, cross/ffmpeg4 -- the only
package in the tree with no MIN_GCC_VERSION, and so the only one that reaches
a pre-2.17 toolchain -- built on both, with its workaround removed:
```
ffmpeg4: (ppc853x-5.2) DONE glibc 2.8, gcc 4.3.7
ffmpeg4: (88f6281-5.2) DONE glibc 2.15, gcc 4.6.4
```
- Documented in Extra flags a toolchain can declare.
- Pull request: #7433
August 17th 2026 — Custom from-source Rust toolchains, and the overlay family (#7353)
- What: the archs
rustupships no usable prebuiltrust-stdfor now build Rust from source —native/rustc-1.82produces arust-<id>-<rev>.txz, a per-arch consumer undertoolchain/syno-<arch>-<dsm>_rust-<vers>_gcc-<gcc>/downloads it, and the base toolchain pulls that in throughDEPENDS. Tier-3 PowerPC e500 (qoriq,ppc853x) has no prebuilt std at all; ARMv588f6281andx86-5.2only ship one built against a newer glibc than DSM provides. - Why it matters beyond Rust: it introduces the
OVERLAY_<component>family — a component shipped beside a base toolchain rather than replacing it. Rust and binutils 2.30 are the first two; a gcc-8.5 overlay is the next. Every decision is resolved once, inmk/spksrc.common/overlay.mk, which keeps three questions apart: available (TC_OVERLAY_<c>— the consumer dir), requested (OVERLAY_<c>), active (OVERLAY_<c>_ON). Conflating them is what produced the bugs this split now prevents. -
Switches, written to
local.mkbymake setupand overridable per build:with precedence
command line > environment > local.mk > defaults. A request that cannot be honored degrades to the stock tools and says so in a banner rather than failing.make helpincross/,spk/anddiyspk/prints the switches, the value in effect, and what it resolves to for your arch. -
Arch issues fixed along the way: per-target rustflags, so the PowerPC SPE codegen options actually reach the compiler (
-Ctarget-cpu=e500 -Ctarget-feature=+spe, not+efpu2) — the qoriqbat/lsdSIGILL of #7304; the ppc853x TLS/PIE relocations, by routing only the Rust link through binutils 2.30 while C keeps the vendoras/ld; andAtomicU64on 32-bit, by widening the target spec wherever the arch's gcc shipslibatomic(which unblockedhelixon qoriq). - Package-facing: nothing changes on a standard arch. On a legacy one, a package
that needs a newer toolchain than these pin should say so with
MIN_RUSTC_VERSION/MIN_GLIBC_VERSIONrather than an arch list. See Toolchain: custom from-source Rust. - Choosing instead of refusing: a floor refuses an arch, but the
cross/<pkg>virtuals have to pick a version of themselves instead.TC_RUSTCis now published besideTC_GCC/TC_GLIBC/TC_KERNEL— the rustc a toolchain pins, orstablewhen it uses rustup's newest — so a virtual routes with$(call version_ge,$(TC_RUSTC),<vers>).cross/bat,cross/ripgrep,cross/lsdandcross/ezamoved off$(ARMv5_ARCHS) $(PPC_ARCHS), which had been standing in for "pinned to rustc 1.82" and so silently missedx86-5.2, a fourth arch this PR pins.cross/helixlikewise replaced its ARMv7L exclusion withMIN_GCC_VERSION = 4.9: its C++ tree-sitter grammars build with-std=c++14, which g++ rejects before 4.9. Same story for the$(OLD_PPC_ARCHS)exclusions on the rust packages, which already named their reason in a comment:cross/batandcross/fdreachpipe2through a dependency crate, so they declareMIN_GLIBC_VERSION = 2.9;cross/ripgrep's pcre2 wants-std=c11, soMIN_GCC_VERSION = 4.6. Those lists read "except qoriq" whileqoriq-5.2runs the same glibc 2.8 and gcc 4.3.7 asppc853x, so the floors refuse an arch the lists let through.cross/sdandcross/ezalose their exclusion outright: their only blocker was std's ownpthread_setname_np, which the weak-link patch above resolves, and both are confirmed building and running on ppc853x (@hgy59) — sosynocli-fileships them there. - Pull request: #7353
July 24th 2026 — Host a native build's output as a reusable archive (#7327, #7386)
-
What: a new opt-in step,
spksrc.build/archive.mk(included byspksrc.native-cc.mk), tars a native package's install tree into a release archive afterinstall, so an expensive tool is built once and re-consumed viaDEPENDSinstead of rebuilt from source. A package enables it with a single line:
The rest defaults — ARCHIVE_EXT (txz, mapped to the tar compression like
extract.mk does in reverse), ARCHIVE_DIR ($(WORK_DIR)) and ARCHIVE_KEEP
(./install) — with an optional debug-symbol strip and ARCHIVE_EXCLUDES. It
follows the usual pre_/archive_target/post_ pattern (override ARCHIVE_TARGET,
or set it to nop), runs automatically from _all, is a no-op unless
ARCHIVE_NAME is set, and is guarded by a status cookie like every other step
(extract, compile, ...) so it runs once per work dir. print-archive-name
lets a generator resolve the name without building.
- Also: nativeclean, the native counterpart of spkclean, drops the master
package's build cookies so every step re-runs on the next make while keeping the
work dir; and the variables carry no redundant NATIVE_ prefix (ARCHIVE_*,
matching ARCHIVE_CMD / ARCHIVE_COOKIE). The step just runs and lets tar
fail if nothing was built, rather than pre-checking a sentinel.
- Why: the archive step was open-coded per package (native/llvm-14.0-build
carried its own build-archive recipe); this factors it into one shared,
defaulted helper. native/llvm-14.0-build adopts it here; the gcc-8.5
overlays reuse it in #7324.
- Relocated (#7386): moved spksrc.native/archive.mk →
spksrc.build/archive.mk — the helper is a shared build step, not native-only
(a from-source rustc reuses it in #7353). No behaviour change.
- Pull requests: #7327,
#7386
July 23rd 2026 — Carry the runtime library the binary asks for, by symbol version (#7322)
- What: the strip step copies the runtime libraries DSM does not ship
(
libatomic,libquadmath,libgfortran-- theTC_LIBS_DEFAULTlist) from the toolchain. It now selects the copy whose symbol versions satisfy the binary (readelf -Vvsstrings), instead of the first one a plainfind -nameturns up. - Why: a find by name returns every copy in the toolchain at once (the
sysroot's, the compiler's
lib64, a multilib) and handed the first to abasenameexpecting one. Choosing by symbol version is correct with the multilib case today and ready for several gcc versions to coexist under a future overlay. - No package-facing change.
- Pull request: #7322
July 23rd 2026 — Detect Fortran by probing the compiler (#7321)
- What:
TC_HAS_FORTRANwas a static "7.x / SRM 1.3 / 6.2.4-x64 ship gfortran" table; it is now a probe of the actualgfortranbinary, evaluated (likeTC_HAS_LIBATOMIC) in the tc_vars sub-make after the toolchain is extracted, so cross packages read the baked result. - Why: the table is a proxy for the stock toolchains only -- it cannot see a compiler swapped in underneath, e.g. a gcc overlay that adds gfortran to an arch the table calls Fortran-less. Probing the binary stays correct whatever provides it, and gives the same answer as the table on every stock toolchain today.
- No package-facing change.
- Pull request: #7321
July 23rd 2026 — Toolchain ABI and link flags reach every language (#7314)
A toolchain's ABI/arch flags now consistently reach every language and the link, and two link-time libraries stopped being hand-maintained arch lists.
- What: a toolchain declares its ABI once in
TC_EXTRA_BUILD_FLAGS(-march,-mcpu,-mfpu,-mfloat-abi, ...); the framework folds it into eachTC_EXTRA_<LANG>FLAGS(C / CPP / C++ / Fortran) and intoTC_EXTRA_LDFLAGS, so every language and the gcc link driver build with the same ABI.-lrt(glibc < 2.17,clock_gettime) and-latomic(ARMv5 / PowerPC, no native 64-bit atomics) moved out of about two dozen per-package arch lists intoTC_EXTRA_LDFLAGS;-latomicis kept only when the toolchain's gcc actually ships it, detected withgcc -print-file-name=libatomic.so. Both are wrapped in-Wl,--as-needed ... -Wl,--no-as-neededso, now that they are declared toolchain-wide, a binary records alibrt/libatomicdependency only when it truly references one. The wrap was removed in #7433 — see the September 4th 2026 entry: it discarded both libraries instead of pruning them. - Why: passing the ABI only through
CFLAGSsilently built C++ / Fortran objects with a different ABI than the C they link against, and the rt/atomic arch lists had to be rechecked by hand each time a toolchain moved.TC_EXTRA_RUSTFLAGSis left out on purpose — rustc takes its ABI through-Ctarget-cpu, and a crate's C dependencies get it viaCFLAGS_<target> = TC_EXTRA_CFLAGS. - Documented in Extra flags a toolchain can declare.
- Pull request: #7314
July 23rd 2026 — Declare toolchain capabilities instead of arch lists (#7313)
A package can now say what it needs from a toolchain rather than list the architectures where it happens to fail today.
- What: three declarative floors —
MIN_GCC_VERSION,MIN_GLIBC_VERSIONandREQUIRE_64BIT— checked against the toolchain's ownTC_GCC/TC_GLIBC(andTC_KERNEL), now declared in each toolchain Makefile and read statically. An unmet floor makespre-check.mkrefuse that architecture with a human-readable reason, and several reasons accumulate (a 32-bit target on an old gcc reports both). About two dozencross/packages andffmpeg7/8dropped theirUNSUPPORTED_ARCHSarch lists in favour of a floor. - Why: a hardcoded arch list says where a package fails, not why; it must be rechecked by hand every time a toolchain moves and cannot express "any arch whose gcc is older than X". A declared floor can, and stays correct on its own.
- Beyond a DSM floor:
REQUIRED_MIN_DSMwas frequently used as a proxy for "needs a recent enough compiler", then topped up withUNSUPPORTED_ARCHSfor the architectures a single DSM floor still missed — a DSM version does not map to one gcc across every arch, so an older platform can ship an older gcc on the same DSM.MIN_GCC_VERSIONstates the real requirement and covers all of those cases at once, dynamically.REQUIRED_MIN_DSM/REQUIRED_MAX_DSM/REQUIRED_MIN_SRMandUNSUPPORTED_ARCHSremain for genuine OS-version and per-arch constraints that are not a capability floor. - Also:
TC_GCCis read from the toolchain Makefile instead of runninggcc -dumpversion, so the compiler version is known before anything is extracted. - Documented in Architecture Support.
- Pull request: #7313
July 13th 2026 — Build-variable standardization (3 PRs)
A three-part effort so that every build system (autotools, CMake, Meson) exposes the same package-facing variable names. Before it, a Makefile looked different depending on the underlying build tool; after it, the same variable means the same thing everywhere.
CONFIGURE_ARGS — unify the configure arguments (#7279)
CMake was the odd one out: it used CMAKE_ARGS / ADDITIONAL_CMAKE_ARGS
while autotools and Meson already passed their options through
CONFIGURE_ARGS.
- What: renamed
CMAKE_ARGS→CONFIGURE_ARGSandADDITIONAL_CMAKE_ARGS→ADDITIONAL_CONFIGURE_ARGSacross every cmake package (no alias);ADDITIONAL_CONFIGURE_ARGSis now honoured by autotools and Meson too. - Why: one variable for "arguments to the configure step" regardless
of the build system.
ADDITIONAL_CONFIGURE_ARGSremains for the rare case where a package reusesCONFIGURE_ARGSfor its own auxiliary invocations and needs extra args to reach only the framework's call (seecross/x265). - Documented in Build System Selection.
- Pull request: #7279
COMPILE_ARGS / INSTALL_ARGS — unify the compile & install arguments (#7280)
The compile and install steps had an autotools-only slot
(COMPILE_MAKE_OPTIONS / INSTALL_MAKE_OPTIONS) with no CMake/Meson
equivalent.
- What: introduced
COMPILE_ARGSandINSTALL_ARGS. On the autotools / plain-make path they are the make command (replacing the removed*_MAKE_OPTIONS); for CMake and Meson they are appended as-is tocmake --build/cmake --installandninja/ninja install. - Defaults on the make path: when unset,
COMPILE_ARGSdefaults to-j$(NCPUS)andINSTALL_ARGStoinstall DESTDIR=… prefix=…, so a package's own make routines can reference them and inherit sensible behaviour (e.g.cross/cairo-1.16gains-j;cross/glibc-*drop their explicit-j). - Why the scoping matters: the defaults are gated by
DEFAULT_ENV(and anINSTALL_TARGETpython check) so they never leak into CMake/Meson/rust/python builds — the native cmake/meson env files declareDEFAULT_ENVfor the same reason. - Documented in Compile and Install Arguments.
- Pull request: #7280
BUILD_DIR — unify the build directory (#7282)
CMake, Meson and Ninja each had their own build-directory variable
(CMAKE_BUILD_DIR / MESON_BUILD_DIR / NINJA_BUILD_DIR).
- What: unified them on a single
BUILD_DIR, set per build system by the matching env file, and extended out-of-tree build support to the autotools / plain-make path: it builds in-source by default and opts in to an out-of-tree build by settingBUILD_DIR. - Why: one name for "where the build happens", and a framework
mechanism for out-of-tree autotools builds that packages previously
hand-rolled —
cross/glibcdropped its three custom configure/compile/install targets in favour of a one-lineBUILD_DIR. - Pull request: #7282
July 13th 2026 — Disable a package with BROKEN or DISABLED (#7283)
- What: a package is skipped when it has a
BROKENor aDISABLEDfile in its folder — both are honoured byspksrc.rules/pre-check.mk(build time) and the CIprepare.sh(package selection). UseDISABLEDwhen a package is intentionally turned off andBROKENwhen it is actually failing. - Why: the marker file now reads its intent. First use:
spk/ffmpeg{4,5,6}were disabled — no new release is planned and disabling them keeps their large codec dependency trees out of the build. See the package lifecycle guide. - Pull request: #7283
July 2nd 2026 — Context-aware make help (#7250)
Context-aware help at the repo root and inside each package.
Pull request: #7250
July 2nd 2026 — Build logs off the repo root (#7256)
Build logs are no longer pinned to the repository root, keeping the tree clean.
Pull request: #7256
July 1st – August 24th 2026 — Bounded mirror fallback for downloads (4 PRs)
A download used to be a single request to PKG_DIST_SITE: if that host was
down, or its TLS certificate had just expired, the build failed.
- What (#7254): the download logic was split into per-method macros
(
DOWNLOAD_GIT/SVN/HG/HTTP, dispatched onPKG_DOWNLOAD_METHOD), and a plain download now walks a list of candidate URLs, stopping at the first success. Mirrors of the big source hosts are tried automatically, andPKG_DIST_MIRRORSlets a package name its own fallback base URLs. Mirroring applies tohttponly — the VCS methods fetch a revision from one place and tar it themselves. - Why bounded: the candidate list is finite (primary URL + family mirrors
PKG_DIST_MIRRORS, de-duplicated) and each is retriedDOWNLOAD_TRIEStimes, so a download that cannot succeed fails rather than looping.- Why it is safe: every candidate is checked against
digests, so a mirror serving different bytes fails the build instead of poisoning it. -
The families since (#7349, #7384, #7401): the variables were renamed
<FAMILY>_MIRRORS→MIRROR_<FAMILY>, and the table grew to seven:
MIRROR_FREEDESKTOP is the one that does not preserve the path: its
mirrors flatten the layout, so only the file name is appended to each base
and the bases embed $(PKG_NAME). Every base is ?=, so local.mk can
override any of them.
- X.org was dropped in #7349, deliberately: no working mirror was found.
Its base pointed at mirror.csclub.uwaterloo.ca/x.org/releases, which
404s, and it matched only www.x.org/releases/ while every X.org package
in the tree downloads from www.x.org/archive/individual/.... A family
with no reachable mirror and no matching URL only adds candidates that
fail. Those packages have no automatic fallback as a result; libX11
moved to xorg.freedesktop.org and is covered by MIRROR_FREEDESKTOP.
The durable answer for the rest is the sources release, as below.
- Documented in
Source downloads and mirrors.
- Pull requests: #7254,
#7349,
#7384,
#7401
January 29th – July 1st 2026 — Reorganize mk/ into functional submodules (8 PRs)
mk/ was a flat pile of spksrc.*.mk files. Over six months it was
reorganized so related logic lives together in concern-based submodules,
while the entry-point files a package Makefile actually includes
(spksrc.cross-cc.mk, spksrc.spk.mk, spksrc.cross-cmake.mk, …) stay at
the root. This was not one big-bang move but a sequence of PRs, each carving
out one concern at a time.
Final layout:
mk/
├── spksrc.cross-cc.mk ┐ entry points a Makefile includes
├── spksrc.cross-cmake.mk │ stay at the root (unchanged include paths)
├── spksrc.spk.mk ┘
├── spksrc.common/ archs, directories, macros, logs, stage0
├── spksrc.build/ per-step recipes: configure, compile, install, patch, …
├── spksrc.cross/ cross-build env per build system: cmake, meson, rust, go
├── spksrc.native/ native-build env per build system
├── spksrc.rules/ depend, dependency-tree, pre-check, digests, tests
├── spksrc.spk/ spk assembly: copy, icon, strip, publish
├── spksrc.spk-meta/ meta initiators: ffmpeg, python, videodriver
├── spksrc.service/ DSM service scripts and installers
├── spksrc.wheel/ python wheel build/install
├── spksrc.toolchain/ toolchain fetch + tc_vars generation
├── spksrc.toolkit/ build-host toolkit (mirrors toolchain)
└── spksrc.kernel/ kernel-module build support
spksrc.common/ — split common.mk (#6906)
Split the overloaded spksrc.common.mk into spksrc.common/
(archs.mk, macros.mk, logs.mk, …) and had every cross/ and spk/
Makefile include spksrc.common.mk before any version_* macro call.
Pull request: #6906
spksrc.toolchain/ (#6914)
Moved spksrc.tc.mk and the toolchain logic into spksrc.toolchain/
(tc-base.mk, tc-versions.mk, tc_vars.mk, …).
Pull request: #6914
spksrc.toolkit/ (#6973)
Reorganized the build-host toolkit the same way, mirroring the toolchain
layout (tk-base.mk, tk-versions.mk, tk_vars.mk, …).
Pull request: #6973
spksrc.kernel/ (#6994)
Split kernel-module support into spksrc.kernel/ (base.mk,
headers.mk, module.mk, versions.mk, …).
Pull request: #6994
spksrc.spk-meta/ — meta initiators (#7008)
Split the ffmpeg / python / videodriver meta initiators out of the
monolithic mk files into spksrc.spk/ and spksrc.spk-meta/, and renamed
the meta-spk initiator to spksrc.spk-meta.mk.
Pull request: #7008
The bulk: build / cross / native / rules / service / wheel (#7237)
The largest move: carved the remaining per-step recipes, per-build-system
env files, rules, service scripts and wheel logic into
spksrc.build/, spksrc.cross/, spksrc.native/, spksrc.rules/,
spksrc.service/ and spksrc.wheel/.
Pull request: #7237
June 28th 2026 — Boxed *.mk file headers (#7242)
Standardized every *.mk header to one boxed format, so each file states its
purpose, inputs and outputs consistently.
Pull request: #7242
June 22nd–25th 2026 — Meta cross-dependency environment (4 PRs)
How a "meta" package (ffmpeg, videodriver, python) exposes its cross-dependencies to consumers.
Introduce the meta cross-dependency environment (#7222)
Introduced META_PKGCONFIG_DIRS + tc_vars.meta.mk.
Pull request: #7222
Resolve CMake meta deps via the toolchain file (#7223)
Resolve OPENSSL_ROOT_DIR + CMAKE_FIND_ROOT_PATH through the generated
toolchain file instead of ad-hoc detection.
Pull request: #7223
June 10th 2026 — Toolchain cache poisoning fix (#7196)
A stale toolchain cache was silently dropping TC_GCC-gated DEPENDS.
Pull request: #7196
June 10th 2026 — Stage-0 toolchain bootstrap (#7184)
Bootstrap the toolchain before deriving TC_GCC, so version-gated
dependencies (version_ge TC_GCC …) parse correctly on a cold tree. This
refines the stage0 minimal environment introduced in March (see below).
Pull request: #7184
January – May 2026 — Faster, parallel dependency resolution (5 PRs)
Dependency resolution used to run through a shell script
(mk/dependency-list.sh) walked serially. It is now a pure-Makefile
implementation (spksrc.rules/dependency-tree.mk) that the framework can
walk in parallel.
Faster dependency-flat (#6894)
Rewrote the flat dependency walk and fixed a basename collision in the
parallel walk (packages of the same name in different directories, e.g.
native/erlang and cross/erlang, shared a done-file).
Pull request: #6894
Pure-Makefile dependency-tree (#6952)
Replaced the legacy dependency-list.sh with
spksrc.dependency-tree.mk, consolidating resolution into the framework
and enabling parallel builds — up to ~2.9× faster.
Pull request: #6952
April 19th – 22nd 2026 — Deduplicate applied patches (2 PRs)
- What: a patch listed both as an
archand as agroup(armv7, x64 could be both) was applied twice.spksrc.build/patch.mknow guarantees each patch appears once inPATCHES, with md5sum-based deduplication. - Why: applying a patch twice either fails or double-applies a hunk; the build should be independent of how a patch happened to be listed.
- Pull requests: #7098, #7104
April 5th – 7th 2026 — Standardize meta package variable names (4 PRs)
The ffmpeg, videodriver and python "meta" packages each exposed their install prefix under a differently-shaped variable name.
- What: standardized the
*_INSTALL_PREFIXvariables —VIDEODRV_*INSTALL_PREFIX(#7043),FFMPEG_*INSTALL_PREFIX(#7044),PYTHON_*INSTALL_PREFIX(#7045) — and the surrounding python / openssl / ffmpeg / videodriver variable names (#7041). - Why: a consumer of any meta package now finds the same variable shape regardless of which one it depends on. This is the naming half of the meta cross-dependency environment (the June 22nd–25th entry above).
- Pull requests: #7041, #7043, #7044, #7045
March – April 2026 — stage0 minimal environment (3 PRs)
- What:
spksrc.common/stage0.mkloads a minimal environment early — just enough for theversion_*macros andTC_GCC— so a package Makefile can callversion_*before the full toolchain environment exists (#7031), with a more robustBASEDIRdetection (#7032) and a follow-up fix for a subtle ordering bug (#7078). - Why: it keeps the full toolchain environment from leaking into the
dependency traversal, and lets version-gated
DEPENDSparse on a cold tree. The stage-0 toolchain bootstrap (#7184, above) builds on this. - Pull requests: #7031, #7032, #7078