Skip to content

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 keeps clock_gettime in a separate librt.
  • -latomic — targets without native 64-bit atomics (ARMv5, PowerPC e500v2) make gcc emit calls into libatomic that 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:

GNU_CONFIGURE = 1
CONFIGURE_ARGS = --host=$(TC_NAME) --build=$(TC_BUILD)

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>FLAGSCFLAGS 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:

RUST_TARGET = x86_64-unknown-linux-gnu
RUSTFLAGS = -C linker=$(TC_CC)

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:

  1. Create toolchain directory:

    mkdir toolchain/syno-x64-8.0
    

  2. Create Makefile:

    TC_NAME  = x86_64-pc-linux-gnu
    TC_VERS  = 8.0
    TC_ARCH  = x64
    TC_DIST_SITE_PATH = $(SYNOLOGY_DOWNLOAD_URL)/toolchain/DSM$(TC_VERS)
    TC_DIST_NAME = $(TC_DIST_SITE_PATH)/Intel%20x86%20Linux%20...%20%28x86_64-GPL%29.txz
    
    include ../../mk/spksrc.toolchain.mk
    

  3. Create digests file with SHA256 checksums

  4. Test build:

    make -C spk/transmission ARCH=x64 TCVERSION=8.0
    

Toolchain Build Process

The toolchain build follows this process:

  1. Download - Fetches toolchain archive from Synology
  2. Checksum - Verifies archive integrity
  3. Extract - Unpacks to work/ directory
  4. Normalize - Applies patches for compatibility
  5. Rust - Installs Rust toolchain components if needed
  6. 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.

OVERLAY_BINUTILS=1 make -C cross/bat-0.25 arch-qoriq-6.2.4   # just this build

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:

  1. Build (optionally with PKG_REV=vN).
  2. Upload the .txz to the release (pre-releases, or the rust/… asset path).
  3. Bump PKG_REV ?= vN in the consumer Makefile and refresh its digests (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:

grep TC_DIST_NAME toolchain/syno-x64-7.2/Makefile
# Verify URL accessibility

tc_vars Not Generated

Remove the tcvars cookie and rebuild:

rm toolchain/syno-x64-7.2/work/.tcvars_done
make -C toolchain/syno-x64-7.2 tcvars

Cross-Compiler Not Found

Ensure the toolchain is fully extracted:

ls toolchain/syno-x64-7.2/work/x86_64-pc-linux-gnu/bin/