Link Order, Link Groups & Whole Archive
When multiple static libraries depend on each other, order matters to the linker. Xmake gives you three knobs to fix order-related link errors without hand-crafting -Wl,... flags:
add_linkorders— declare that library A must come before library B on the command line.add_linkgroups— wrap a set of libraries in--start-group/--end-group(Linux) or the equivalent so the linker resolves circular references.whole_archive = trueoption onadd_linkgroups— force-include every object, even ones that look unreferenced (plugin registration, static initializers).
1. The underlying problem
A static library is a collection of object files. The GNU linker (and most others) resolves symbols in a single left-to-right pass: when it sees -lfoo, it picks up objects that satisfy currently-undefined symbols. Anything not needed yet is discarded.
That causes two common bugs:
- Wrong order.
-lbar -lfoowherebardepends onfoo→ undefined symbols frombar, becausefoowas consumed beforebarneeded it. - Circular dependency.
foocalls intobarandbarcalls intofoo→ impossible with one-pass linking. - Missing static registration. Plugin/factory patterns where a global constructor registers itself, but no other code directly references it → linker strips the object.
2. add_linkorders — fix order
target("app")
set_kind("binary")
add_files("src/main.cpp")
add_deps("foo", "bar", "baz")
add_linkorders("bar", "foo") -- bar depends on foo → bar first, then foo
add_linkorders("baz", "foo", "bar")
add_linkorders(A, B) means A must appear before B on the linker command line (-lA ... -lB). List the dependents before their dependencies. Multiple calls accumulate.
Xmake performs a topo-sort over the declared orders and emits the final link line. If you declare a cycle, xmake raises at configure time.
Order applies to add_links too
target("app")
add_links("foo", "bar", "baz")
add_linkorders("bar", "foo")
Works for both library dependencies (add_deps) and raw links (add_links / add_syslinks).
3. add_linkgroups — group libraries for circular deps
target("app")
set_kind("binary")
add_files("src/main.cpp")
add_linkgroups("foo", "bar", {group = true})
Emits:
-Wl,--start-group -lfoo -lbar -Wl,--end-group
On Linux/MinGW GNU ld. macOS ld64 and Windows link.exe handle circular references differently — xmake normalizes the behavior.
Use when:
foo.aandbar.amutually reference each other's symbols.- You'd otherwise have to list one of them twice (
-lfoo -lbar -lfoo) —add_linkgroupsis the clean fix.
4. Whole-archive — force-include every object
target("plugin_host")
set_kind("binary")
add_files("src/main.cpp")
add_linkgroups("plugin_a", "plugin_b", {whole_archive = true})
Emits:
-Wl,--whole-archive -lplugin_a -lplugin_b -Wl,--no-whole-archive -- Linux
-Wl,-force_load,<path-to-plugin_a.a> -- macOS
/WHOLEARCHIVE:plugin_a.lib -- MSVC
Every object in those archives is pulled in — even ones with no outside references. Needed for:
- Plugin registries where a global constructor does
register("name", factory)and there's no other reference. - Kernel modules / linker sections (
__attribute__((constructor)),__attribute__((section("...")))). - Language runtimes that rely on static initializers.
5. Combining order + groups + whole archive
target("app")
set_kind("binary")
add_files("src/main.cpp")
-- regular link deps, in order
add_deps("core", "utils")
add_linkorders("core", "utils")
-- circularly-dependent math libs, grouped
add_linkgroups("math_core", "math_ext", {group = true})
-- plugin archives, whole-linked
add_linkgroups("plugin_json", "plugin_yaml", {whole_archive = true})
Xmake arranges these so core utils come first (topo order), then the grouped math libs, then the plugin whole-archives. Run xmake -v to see the final link command.
6. Platform notes
Linux (GNU ld, lld, mold)
All four features work natively — --start-group/--end-group, --whole-archive/--no-whole-archive.
macOS (Apple ld64)
--start-groupis a no-op (Apple linker resolves multi-pass by default), soadd_linkgroups(..., {group = true})is cheap but unnecessary for circular deps on macOS.--whole-archivemaps to-force_load <path>. Xmake resolves the archive path automatically when you useadd_linkgroups(..., {whole_archive = true}).
Windows (MSVC link.exe)
- Group semantics don't exist; link.exe resolves circular static-lib deps natively.
- Whole-archive maps to
/WHOLEARCHIVE:libname.lib. Xmake handles it. - Order is usually not an issue on MSVC;
add_linkordersstill works for cases where it matters.
Windows (MinGW / gcc)
- Same as Linux GNU ld.
7. Diagnosing link errors
xmake -v # show actual link command
xmake -vD 2>&1 | grep -A5 linking
xmake --rebuild # force re-link
Classic symptoms → fix:
| Symptom | Fix |
|---|---|
undefined reference to 'bar_func' and bar is linked |
add_linkorders("dependent", "bar") |
undefined reference in both A and B (both are linked) |
add_linkgroups("A", "B", {group = true}) |
Plugin registers at runtime but factory_not_found |
add_linkgroups("plugin_a", {whole_archive = true}) |
| Works on macOS, fails on Linux | Likely order — Linux GNU ld is stricter |
Works with --start-group manually, clean it up |
Use add_linkgroups(..., {group = true}) |
8. Avoiding the problem entirely
- Merge tightly-coupled libraries into one static lib. If
fooandbarare mutually dependent, maybe they should be one target. - Use shared libraries for plugin systems.
.so/.dlldon't get stripped —dlopen/LoadLibraryloads them wholesale. - Move the registration to the host binary. Avoids whole-archive entirely by keeping factories in code the host definitely references.
- Use
set_kind("object")+add_files. Object libraries sidestep archive strip-off rules: object files are always fully linked.
9. Pitfalls
add_linkorderscycle. Raises at configure. Check the declared pairs for a cycle.whole_archivebloats the binary. Every object goes in — even test/debug helpers you never wanted. Split the archive before whole-loading.- macOS
-force_loadneeds absolute paths. Xmake fills them in; if you build the flags by hand, you must pass the full path. - MSVC
/WHOLEARCHIVEneeds the library's base name. Matching is case-insensitive on Windows, case-sensitive on Linux — stick to lowercase. - System libraries.
add_linkorders/add_linkgroupsoperate on named libraries (fromadd_deps/add_links/add_syslinks). For raw-Wl,...flags, fall back toadd_ldflags.
When to branch out
- Target layout /
add_links/add_syslinksbasics →xmake-targets - Cross-compile specific linker flags (
--sdk/--cross) →xmake-cross-compilation - Rules that customize the link step →
xmake-rules - Debug "why is this symbol missing" with actual link command →
xmake-troubleshooting