Toolchain Management¶
This document describes how spksrc manages Synology cross-compilation toolchains.
Overview¶
Synology provides official cross-compilation toolchains for each DSM version and architecture. spksrc downloads, extracts, and configures these toolchains automatically.
Toolchain Directory Structure¶
toolchain/
├── syno-x64-7.2/
│ ├── Makefile
│ ├── digests
│ └── work/
│ ├── x86_64-pc-linux-gnu/ # Extracted toolchain
│ ├── tc_vars.mk # Generated variables
│ ├── tc_vars.autotools.mk
│ ├── tc_vars.flags.mk
│ ├── tc_vars.cmake
│ └── tc_vars.meson-*
├── syno-aarch64-7.2/
├── syno-armv7-7.2/
└── ...
Toolchain Naming¶
Toolchains follow the pattern syno-<arch>-<tcversion>:
- arch - Target architecture (x64, aarch64, armv7, etc.)
- tcversion - DSM version (7.2, 7.1, 6.2.4, etc.)
Examples:
- syno-x64-7.2 - Intel 64-bit for DSM 7.2
- syno-aarch64-7.1 - ARM 64-bit for DSM 7.1
- syno-armv7-6.2.4 - ARM 32-bit for DSM 6.2.4
Toolchain Makefile¶
Each toolchain directory contains a Makefile with:
TC_NAME = x86_64-pc-linux-gnu
TC_VERS = 7.2
TC_ARCH = x64
TC_DIST_SITE_PATH = $(SYNOLOGY_DOWNLOAD_URL)/toolchain/DSM$(TC_VERS)
TC_DIST_NAME = $(TC_DIST_SITE_PATH)/Intel%20x86%20Linux%204.4.302%20%28x86_64-GPL%29.txz
include ../../mk/spksrc.toolchain.mk
spksrc.toolchain.mk is the entry point; its implementation under mk/spksrc.toolchain/ is detailed in the Makefile System include hierarchy. The matching DSM development toolkit is covered on the Toolkit page.
Extra flags a toolchain can declare¶
A toolchain Makefile may add flags that then apply to every package built
with it. Assembled by mk/spksrc.toolchain/tc-flags.mk:
| Variable | Purpose |
|---|---|
TC_EXTRA_BUILD_FLAGS |
The target's ABI / arch flags (-march, -mcpu, -mfpu, -mfloat-abi, -mthumb, ...) |
TC_EXTRA_CFLAGS / TC_EXTRA_CPPFLAGS / TC_EXTRA_CXXFLAGS / TC_EXTRA_FFLAGS |
Per-language extra flags |
TC_EXTRA_LDFLAGS |
Extra link-time flags / libraries (-lrt, -latomic) |
TC_EXTRA_RUSTFLAGS |
Extra rustc flags (-Ctarget-cpu=...) |
TC_EXTRA_BUILD_FLAGS selects the ABI, so it must reach every language and
the link — not just C. Building C++ or Fortran objects with a different ABI than
the C objects they link against yields silently broken binaries, and the gcc link
driver reads these flags to pick the right multilib and startfiles. The framework
therefore folds TC_EXTRA_BUILD_FLAGS once into each TC_EXTRA_<LANG>FLAGS (and
into TC_EXTRA_LDFLAGS). Each per-language variable is then the single residual
list that language reads — the ABI first, then anything the toolchain adds for
that language, always last in the chain, so it stays a clean place to extend. A
package's own ADDITIONAL_<LANG>FLAGS are separate and package-scoped.
TC_EXTRA_RUSTFLAGS is deliberately not fed from TC_EXTRA_BUILD_FLAGS: rustc
takes its ABI through -Ctarget-cpu (already in TC_EXTRA_RUSTFLAGS), and the C
dependencies of a rust crate receive the ABI through
CFLAGS_<target> = TC_EXTRA_CFLAGS.
TC_EXTRA_LDFLAGS — -lrt vs -latomic, auto-detected. Two link-time needs
were previously hand-carried as per-package architecture lists:
-lrt— glibc < 2.17 keepsclock_gettimein a separatelibrt.-latomic— targets without native 64-bit atomics (ARMv5, PowerPC e500v2) make gcc emit calls intolibatomicthat the link must resolve.
-latomic is kept only when the toolchain's gcc actually ships the library: the
framework asks gcc -print-file-name=libatomic.so (TC_HAS_LIBATOMIC) rather than
tabulating architectures. That is the exact criterion — a gcc old enough to lack
libatomic (before 4.7) also predates the __atomic_* builtins, emits __sync_*
instead, and so never needs the library — and handing -latomic to such a gcc is a
fatal "cannot find -latomic". A toolchain lists -latomic in its
TC_EXTRA_LDFLAGS; the framework drops it where it would not resolve.
These libraries are declared toolchain-wide, so they reach every link even though
most binaries call neither clock_gettime nor an atomic builtin. They are
nonetheless passed plainly, not wrapped in -Wl,--as-needed.
--as-needed keeps a library only if, at the point the linker sees it, it
resolves an already-undefined symbol. LDFLAGS is placed before the objects
and libraries being linked, so at that point nothing is undefined yet: a
front-placed -lrt inside an --as-needed bracket is always discarded, and a
later -lsrt that needs clock_gettime then fails with undefined reference.
The wrap was therefore not a refinement of the declaration but a silent deletion
of it — exactly equivalent to declaring nothing at all.
Linking them plainly costs one DT_NEEDED entry on binaries that do not call
into them. That is the correct trade: librt and libatomic are part of the
toolchain's own runtime and are present on every target that has them.
tc_vars Files¶
The toolchain build generates several tc_vars*.mk files that configure cross-compilation:
tc_vars.mk¶
Core toolchain identity:
TC_NAME = x86_64-pc-linux-gnu
TC_PREFIX = /path/to/toolchain/work/x86_64-pc-linux-gnu/bin/
TC_PATH = $(TC_PREFIX)/bin
TC_SYSROOT = $(TC_PREFIX)/x86_64-pc-linux-gnu/sysroot
TC_CC = $(TC_PREFIX)x86_64-pc-linux-gnu-gcc
TC_CXX = $(TC_PREFIX)x86_64-pc-linux-gnu-g++
TC_AR = $(TC_PREFIX)x86_64-pc-linux-gnu-ar
TC_LD = $(TC_PREFIX)x86_64-pc-linux-gnu-ld
TC_STRIP = $(TC_PREFIX)x86_64-pc-linux-gnu-strip
tc_vars.autotools.mk¶
Autotools (configure) adapter:
tc_vars.flags.mk¶
Compiler and linker flags:
CFLAGS = -I$(STAGING_INSTALL_PREFIX)/include
CXXFLAGS = $(CFLAGS)
LDFLAGS = -L$(STAGING_INSTALL_PREFIX)/lib -Wl,-rpath,$(INSTALL_PREFIX)/lib
Each language's flags end with its own TC_EXTRA_<LANG>FLAGS — CFLAGS ends with
TC_EXTRA_CFLAGS, CXXFLAGS with TC_EXTRA_CXXFLAGS, and so on — and LDFLAGS
ends with TC_EXTRA_LDFLAGS. Each of those already carries the toolchain-declared
ABI (TC_EXTRA_BUILD_FLAGS, folded in as described under
Extra flags a toolchain can declare), so
the ABI reaches every compiler and the link driver.
tc_vars.cmake¶
CMake toolchain file for cross-compilation:
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
set(CMAKE_C_COMPILER /path/to/x86_64-pc-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER /path/to/x86_64-pc-linux-gnu-g++)
set(CMAKE_SYSROOT /path/to/sysroot)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
tc_vars.meson-cross and tc_vars.meson-native¶
Meson cross and native files:
# tc_vars.meson-cross
[binaries]
c = '/path/to/x86_64-pc-linux-gnu-gcc'
cpp = '/path/to/x86_64-pc-linux-gnu-g++'
ar = '/path/to/x86_64-pc-linux-gnu-ar'
strip = '/path/to/x86_64-pc-linux-gnu-strip'
pkgconfig = '/usr/bin/pkg-config'
[host_machine]
system = 'linux'
cpu_family = 'x86_64'
cpu = 'x86_64'
endian = 'little'
tc_vars.rust.mk¶
Rust cross-compilation settings:
Supported Architectures¶
The complete list of architecture families, platform codenames, architecture groups (ARM_ARCHS, x64_ARCHS, 64bit_ARCHS, ...) and the Synology models they map to is maintained in the reference:
Adding New Toolchain Support¶
When Synology releases a new DSM version:
-
Create toolchain directory:
-
Create Makefile:
-
Create digests file with SHA256 checksums
-
Test build:
Toolchain Build Process¶
The toolchain build follows this process:
- Download - Fetches toolchain archive from Synology
- Checksum - Verifies archive integrity
- Extract - Unpacks to
work/directory - Normalize - Applies patches for compatibility
- Rust - Installs Rust toolchain components if needed
- tcvars - Generates tc_vars*.mk files
Caching¶
Toolchains are cached in the distrib/ directory:
distrib/
└── toolchain/
├── Intel x86 Linux 4.4.302 (x86_64-GPL).txz
├── Realtek RTD1296 Linux 4.4.180 (aarch64-GPL).txz
└── ...
Once downloaded, toolchains are reused across builds. Delete from distrib/ to force re-download.
Custom From-Source Rust Toolchains¶
A few legacy archs cannot use a stock rustup std: Tier-3 PowerPC e500 (ppc853x, qoriq) has no prebuilt std at all, and ARMv5 88f6281 / x86-5.2 only ship one built against a newer glibc than the DSM toolchain. For these, spksrc builds Rust from source (rustc + cargo + host/target std, LLVM from the bundled source) against the arch's own gcc.
Producer / consumer split¶
| Piece | Role |
|---|---|
native/rustc-<vers>/ |
Producer — builds the rust-<id>-<rev>.txz. |
toolchain/syno-<arch>-<dsm>_rust-<vers>_gcc-<gcc>/ |
Consumer — downloads + extracts that .txz (the base toolchain DEPENDS on it). |
toolchain/syno-<arch>-<dsm>_binutils-2.30_gcc-<gcc>/ |
binutils 2.30 overlay used for the Rust link only (RUST_LINK_VIA_BINUTILS, default ON). |
A base toolchain (syno-<arch>-<dsm>/) opts in simply by having a rust consumer dir beside it (TC_OVERLAY_RUSTC); the Synology-vendored triple (…-unknown-… → …-synology-…) is derived from the central arch map. The C toolchain keeps its stock vendor gcc+ld; only the Rust link routes through binutils 2.30.
Each overlay is self-contained: it unpacks into its own consumer directory (…_rust-…/work/, …_binutils-…/work/), never into the base toolchain's. That is what lets a second build of a component sit beside the first — only the generated pointers then decide which one is used.
Overlay switches¶
Every overlay decision is resolved in one place, mk/spksrc.common/overlay.mk, read by the toolchain and the package side alike. It keeps three questions apart:
| Variable | Meaning | |
|---|---|---|
| Available | TC_OVERLAY_<c> |
the consumer dir, empty when the arch ships none |
| Requested | OVERLAY_<c> |
the switch |
| Active | OVERLAY_<c>_ON |
both of the above |
| Switch | Default | Effect |
|---|---|---|
OVERLAY_RUSTC |
1 |
Custom from-source rustc + Synology triple. 0 falls back to stock rustup — diagnostic only: the archs that ship an overlay do so precisely because the stock std does not fit them. |
OVERLAY_BINUTILS |
0 |
Global: overlay as/ld for every compile. Only safe under a matched modern gcc, hence off. |
RUST_LINK_VIA_BINUTILS |
1 where a rust overlay exists |
Narrow: only the Rust link takes the overlay ld; C keeps the vendor as/ld. |
OVERLAY_RUSTC_VERS |
1.82 |
Which build to select, matching the consumer dir name. |
OVERLAY_BINUTILS_VERS |
2.30 |
Idem. |
make setup writes the first two into local.mk, the tree-wide source of truth. It is read before mk/spksrc.common/overlay.mk and uses ?=, which gives:
command line > environment > local.mk > the defaults above
Note the ?=: a plain = in local.mk would win over an environment prefix, silently ignoring the one-off override below.
A request that cannot be honored never fails the build: it degrades to the stock tools and says so, in a banner on the tcvars path (the switches are a per-package choice, and the toolchain's own _all is skipped once its cookie exists). You get one for a component the arch does not ship, one for a version it does not have, and one for the global overlay driving a vendor gcc it is not matched to.
Each generated tc_vars.mk records what the build actually resolved to:
TC_OVERLAY_RUSTC := …/syno-qoriq-6.2.4_rust-1.82_gcc-4.9.3
TC_OVERLAY_BINUTILS := # empty: the narrow rust link only, not the global overlay
Both carry active semantics — the path when that overlay drives the build, empty otherwise. The narrow rust-link use shows up instead as a -Clink-arg=-B<shim> in the Rust link flags. The OVERLAY_<c> switches are deliberately not exported: a package includes tc_vars.mk, so it would inherit the previous run's choice and the switch would go sticky.
(Re)building and publishing¶
make -C native/rustc-1.82 arch-ppc853x-5.2 # one arch
make -C native/rustc-1.82 all-5.2 # every rust arch of a DSM version
You do not build binutils separately: the Rust build co-builds native/binutils-2.30 for the same (arch, DSM) automatically (the rustc_binutils_cobuild step, gated on RUST_LINK_VIA_BINUTILS). It is intentionally not a DEPENDS — a DEPENDS cannot carry the per-arch parametrization — so make arch-<arch>-<vers> is self-contained. The build prints a Co-building binutils … banner when it does so.
The .txz name carries a revision. When re-publishing a rebuilt archive under the same id, bump the rev so caches don't serve the stale artifact — pass PKG_REV=vN (then rename). The current rev lives statically in each consumer Makefile (PKG_REV ?= vN), not in a shared file. To publish a rebuild:
- Build (optionally with
PKG_REV=vN). - Upload the
.txzto the release (pre-releases, or therust/…asset path). - Bump
PKG_REV ?= vNin the consumer Makefile and refresh itsdigests(make -C toolchain/syno-<arch>-<dsm>_rust-<vers>_gcc-<gcc> digests).
make clean on the base toolchain cascades to its rust + binutils overlay consumers, so a rebuild re-extracts them fresh.
Troubleshooting¶
Toolchain Download Fails¶
Check if the URL is still valid:
tc_vars Not Generated¶
Remove the tcvars cookie and rebuild:
Cross-Compiler Not Found¶
Ensure the toolchain is fully extracted:
Related Documentation¶
- Architecture - Build pipeline overview
- Makefile System - mk/*.mk file details
- Reference: Architectures - Complete architecture reference