spn build compiles your project and all dependencies. Compilation targets a triple (e.g. x86_64-windows-gnu) and uses a profile (e.g. release, O2, statically linked).
Build output
By default, spn outputs to build/. If you explicitly pass a triple, it uses build/triple/profile. Otherwise, it uses build/profile. If you were to run this:
spn build
spn build --target x86_64-windows-gnuIt would produce this:
build/
├── debug/
└── x86_64-windows-gnu/
└── debug/Inside a given build, executables are placed at the top level for easy running. All other artifacts (headers, libraries, your dependencies’ artifacts) are placed in store/ in the usual way. Finally, a JSONL file with a detailed trace of the build is in .spn/build.jsonl
build/debug/
├── store/
│ ├── bin/
│ │ ├── main.exe
│ ├── lib/
│ │ ├── libwhatever.a
│ ├── include/
│ │ ├── whatever.h
│ │ └── ...
└── .spn/
└── build.jsonlSelecting targets
Profiles
Packages are compiled against a profile, which contains the target triple, toolchain, build mode, optimization level, sanitizers, etc.
[profile.default]
toolchain = "zig"
linkage = "static"
standard = "c11"
mode = "debug"Modes and optimization
Sanitizers
Cross compilation
Target triples
—os, —arch, —abi
Caching
spn has a built in build cache which caches any intermediate artifact that your build creates, like ccache or sccache. Builds are incremental by default. That doesn’t mean “incremental on my machine”. That means incremental. Build foo.exe on one machine, and the cache is designed such that any subsequent machine building foo.exe can see a fully cached build.
That’s because, at its core, spn is a content addressed DAG. If you’ve never seen the terms, let’s take a ride!
Incremental builds
This means that everything in your build is turned into nodes in a graph. If something in the middle of the graph changes, we know exactly what needs to be rebuilt and in what order. This is like every other incremental build system that has ever existed.
Content addressing
If you use CMake, you’ve probably encountered this:
# Build once. After this, builds are incremental. You are happy.
make
# Ah, but you gotta check out another branch
git checkout whatever
git checkout main
# This is a full rebuild, because Git touched every file and made it
# appear to have been edited since the last build. You are sad.
makeThat’s because CMake, and in fact most build systems, lie to you. They tell you that they know when foo.c changed. But they have no god damn idea! None whatsoever! Now, they have proxies which in practice are…fine. Like, for example, a file’s mtime. Last build was at 3:00, foo.c says 3:05, let’s rebuild it.
But files get touched all the time! For no reason! Sometimes, they even go backwards, like when you decompress an archive, and then your build is wrong instead of just slow. This is the first problem: Traditional build systems have trouble knowing when something changed.
There’s a beautiful solution to both of these things at once! If you’re having trouble giving everything a unique identity, and you’re having trouble figuring out when a given thing changed, make their identity be their content. When we build foo.o, we hash its bytes. Let’s say that hash comes out to, miraculously, 0x69. Now, there’s no such thing as foo.o; there’s just a file called 0x69 in the cache.
That’s great, but the next build still needs to be able to know that, ah, yes, we need the cache entry keyed at 0x69. To do this, we look at all of the inputs to foo.o:
foo.c, of course- Let’s say that the compiler reported that
stdint.hwas used, too
We hash their content too, and get 0x420 and 0x5F3759DF. Then, all we do is write down a fact:
If the inputs to the compiler are
0x420and0x5F3759DF, then the output will be0x69
Next time, when we’re ready to build foo.o, we have all of its inputs ready to go. Take those inputs, and ask the fact machine if it knows the answer for that set of inputs. If foo.c was edited, the inputs are no longer (0x420, 0x5F3759DF). If spum.h got added to the build, then there are now three inputs rather than two. And, if the inputs are the same but 0x69 isn’t in the cache, all you have to do is rebuild!
This is beautiful. Identity is content; content is identity. A file doesn’t have a name. It simply is. There are, of course, many kinks to work out in such a system, but they’re all tractable.
This is the exact principle behind Bazel, BuildXL, and Nix, and it’s the fundamental reason why spn is so good at caching your builds across machines.